diff --git a/.appveyor.yml b/.appveyor.yml new file mode 100644 index 000000000..a58675259 --- /dev/null +++ b/.appveyor.yml @@ -0,0 +1,162 @@ +image: + - MacOS + - Visual Studio 2015 + - Visual Studio 2019 + - Ubuntu + +build: off + +init: + - git config --global core.autocrlf input + +for: +- + matrix: + only: + - image: Ubuntu + + branches: + only: + - build-graal + artifacts: + - path: client/target/bin/sbtn + name: sbtn + + install: + - curl -sL https://github.com/sbt/sbt/releases/download/v1.3.10/sbt-1.3.10.tgz > ~/sbt-bin.tgz + - mkdir ~/sbt + - tar -xf ~/sbt-bin.tgz --directory ~/sbt + - curl -sL https://raw.githubusercontent.com/shyiko/jabba/0.11.0/install.sh | bash && . ~/.jabba/jabba.sh + - jabba install adopt@1.8.0-275 + - jabba use adopt@1.8.0-275 + - curl -sL https://github.com/graalvm/graalvm-ce-builds/releases/download/vm-20.1.0/graalvm-ce-java8-linux-amd64-20.1.0.tar.gz > graalvm.tar.gz + - tar -xf graalvm.tar.gz + - export PATH="~/sbt/sbt/bin:$PATH" + - export PATH="$PATH:~/.jabba/jdk/adopt@1.8.0-275/bin" + - export JAVA_HOME="~/.jabba/jdk/adopt@1.8.0-275" + + test_script: + - export PATH="$PATH:~/.jabba/jdk/adopt@1.8.0-275/bin" + - export PATH="$PATH:graalvm-ce-java8-20.1.0/bin" + - gu install native-image + - sbt "-Dsbt.io.virtual=false" "-Dsbt.native-image=$(pwd)/graalvm-ce-java8-20.1.0/bin/native-image" "sbtClientProj/buildNativeThinClient" + +- + matrix: + only: + - image: MacOS + + branches: + only: + - build-graal + artifacts: + - path: client/target/bin/sbtn + name: mac-native-sbt-client + + install: + - curl -sL https://github.com/sbt/sbt/releases/download/v1.3.10/sbt-1.3.10.tgz > ~/sbt-bin.tgz + - mkdir ~/sbt + - tar -xf ~/sbt-bin.tgz --directory ~/sbt + - curl -sL https://raw.githubusercontent.com/shyiko/jabba/0.11.0/install.sh | bash && . ~/.jabba/jabba.sh + - jabba install adopt@1.8.0-222 + - jabba use adopt@1.8.0-222 + - curl -sL https://github.com/graalvm/graalvm-ce-builds/releases/download/vm-20.1.0/graalvm-ce-java8-darwin-amd64-20.1.0.tar.gz > graalvm.tar.gz + - tar -xf graalvm.tar.gz + - export PATH="~/sbt/sbt/bin:$PATH" + - export PATH="$PATH:~/.jabba/jdk/adopt@1.8.0-222/bin" + - export JAVA_HOME="~/.jabba/jdk/adopt@1.8.0-222" + + test_script: + - export PATH="$PATH:~/.jabba/jdk/adopt@1.8.0-222/Contents/Home/bin" + - export PATH="$PATH:graalvm-ce-java8-20.1.0/Contents/Home/bin" + - gu install native-image + - sbt "-Dsbt.io.virtual=false" "-Dsbt.native-image=$(pwd)/graalvm-ce-java8-20.1.0/Contents/Home/bin/native-image" "sbtClientProj/buildNativeThinClient" + +- + matrix: + only: + - image: Visual Studio 2015 + branches: + only: + - build-graal + + artifacts: + - path: client\target\bin\sbtn.exe + name: sbtn.exe + install: + - cinst adoptopenjdk8 -params 'installdir=C:\\jdk8' + - SET CI=true + #- choco install windows-sdk-7.1 kb2519277 + - call "C:\Program Files\Microsoft SDKs\Windows\v7.1\Bin\SetEnv.cmd" + + - ps: | + Add-Type -AssemblyName System.IO.Compression.FileSystem + if (!(Test-Path -Path "C:\sbt" )) { + (new-object System.Net.WebClient).DownloadFile( + 'https://github.com/sbt/sbt/releases/download/v1.3.10/sbt-1.3.10.zip', + 'C:\sbt-bin.zip' + ) + [System.IO.Compression.ZipFile]::ExtractToDirectory("C:\sbt-bin.zip", "C:\sbt") + } + if (!(Test-Path -Path "C:\graalvm-ce-java8-20.2.0-dev" )) { + (new-object System.Net.WebClient).DownloadFile( + 'https://github.com/graalvm/graalvm-ce-builds/releases/download/vm-20.1.0/graalvm-ce-java8-windows-amd64-20.1.0.zip', + 'C:\graalvm-ce-java8-20.1.0.zip' + ) + [System.IO.Compression.ZipFile]::ExtractToDirectory("C:\graalvm-ce-java8-20.1.0.zip", "C:\") + } + if (!(Test-Path -Path "C:\zulu-jdk7" )) { + (new-object System.Net.WebClient).DownloadFile( + 'https://cdn.azul.com/zulu/bin/zulu7.38.0.11-ca-jdk7.0.262-win_x64.zip', + 'C:\zulu-jdk7.zip' + ) + [System.IO.Compression.ZipFile]::ExtractToDirectory("C:\zulu-jdk7.zip", "C:\") + } + - SET PATH=C:\graalvm-ce-java8-20.1.0\bin;%PATH% + - SET PATH=C:\sbt\sbt\bin;%PATH% + - SET JAVA_HOME=C:\jdk8 + - gu install native-image + - rm .sbtopts + + cache: + - '%USERPROFILE%\.ivy2\cache' + - '%LOCALAPPDATA%\Coursier\Cache\v1' + - '%USERPROFILE%\.sbt' + + test_script: + - sbt "-Dsbt.io.virtual=false" "-Dsbt.native-image=C:\graalvm-ce-java8-20.1.0\bin\native-image.cmd" "sbtClientProj/buildNativeThinClient" +- + matrix: + only: + - image: Visual Studio 2019 + branches: + except: + - build-graal + install: + - cinst adoptopenjdk8 -params 'installdir=C:\\jdk8' + - SET JAVA_HOME=C:\jdk8 + - SET PATH=C:\jdk8\bin;%PATH% + - SET CI=true + + - ps: | + Add-Type -AssemblyName System.IO.Compression.FileSystem + if (!(Test-Path -Path "C:\sbt" )) { + (new-object System.Net.WebClient).DownloadFile( + 'https://github.com/sbt/sbt/releases/download/v1.3.10/sbt-1.3.10.zip', + 'C:\sbt-bin.zip' + ) + [System.IO.Compression.ZipFile]::ExtractToDirectory("C:\sbt-bin.zip", "C:\sbt") + } + - SET PATH=C:\sbt\sbt\bin;%PATH% + - SET SBT_OPTS=-Xmx4g -Dsbt.supershell=never -Dfile.encoding=UTF8 + - rm .sbtopts + + cache: + - '%USERPROFILE%\.ivy2\cache' + - '%LOCALAPPDATA%\Coursier\Cache\v1' + - '%USERPROFILE%\.sbt' + + test_script: + # The server tests often fail in CI when run together so just run a single test to ensure + # that the thin client works on windows + - sbt "-Dsbt.io.virtual=false" "scripted actions/* reporter/source-mapper classloader-cache/* nio/* watch/*" "serverTestProj/testOnly testpkg.ClientTest" diff --git a/.gitattributes b/.gitattributes index a5d9c6403..11d44bd7f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,7 +1,4 @@ -# Set default behaviour, in case users don't have core.autocrlf set. -* text=auto - -# Explicitly declare text files we want to always be normalized and converted -# to native line endings on checkout. -*.scala text -*.java text +# Exclude contraband generated files from diff (by default - you can see it if you want) +**/contraband-scala/**/* -diff merge=ours +**/contraband-scala/**/* linguist-generated=true +**/contraband-scala/**/* diff diff --git a/.github/ISSUE_TEMPLATE/---bug-report.md b/.github/ISSUE_TEMPLATE/---bug-report.md new file mode 100644 index 000000000..c2f43e7b9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/---bug-report.md @@ -0,0 +1,22 @@ +--- +name: "\U0001F41B Bug report" +about: Create a report to help us improve +title: '' +labels: Bug +assignees: '' + +--- + +## steps + + + +## problem + + + +## expectation + + + +## notes diff --git a/.github/ISSUE_TEMPLATE/--feature-request.md b/.github/ISSUE_TEMPLATE/--feature-request.md new file mode 100644 index 000000000..5ef8d3776 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/--feature-request.md @@ -0,0 +1,10 @@ +--- +name: "\U0001F389 Feature request" +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +Please use https://discuss.lightbend.com/c/tooling including a specific user story instead of posting them to the issue tracker. diff --git a/.github/ISSUE_TEMPLATE/--question.md b/.github/ISSUE_TEMPLATE/--question.md new file mode 100644 index 000000000..8d1dd089f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/--question.md @@ -0,0 +1,10 @@ +--- +name: "❓ Question" +about: Please use https://stackoverflow.com/questions/tagged/sbt for questions +title: '' +labels: '' +assignees: '' + +--- + +Please use https://stackoverflow.com/questions/tagged/sbt for questions instead of posting them to the issue tracker. diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..5ace4600a --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..ca77b2d48 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,108 @@ +name: CI +on: + pull_request: + push: + +jobs: + test: + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + java: 11 + jobtype: 1 + - os: ubuntu-latest + java: 11 + jobtype: 2 + - os: ubuntu-latest + java: 11 + jobtype: 3 + - os: ubuntu-latest + java: 11 + jobtype: 4 + - os: ubuntu-latest + java: 11 + jobtype: 5 + - os: ubuntu-latest + java: 8 + jobtype: 6 + runs-on: ${{ matrix.os }} + env: + JAVA_OPTS: -Xms800M -Xmx2G -Xss6M -XX:ReservedCodeCacheSize=128M -server -Dsbt.io.virtual=false -Dfile.encoding=UTF-8 + SCALA_212: 2.12.13 + SCALA_213: 2.13.3 + UTIL_TESTS: utilCache/test;utilControl/test;utilInterface/test;utilLogging/test;utilPosition/test;utilRelation/test;utilScripted/test;utilTracking/test + SBT_LOCAL: false + steps: + - name: Checkout sbt/sbt + uses: actions/checkout@v2 + - name: Checkout sbt/io + uses: actions/checkout@v2 + with: + repository: sbt/io + ref: develop + path: io + - name: Checkout sbt/librarymanagement + uses: actions/checkout@v2 + with: + repository: sbt/librarymanagement + ref: develop + path: librarymanagement + - name: Checkout sbt/zinc + uses: actions/checkout@v2 + with: + repository: sbt/zinc + ref: develop + path: zinc + - name: Setup + uses: olafurpg/setup-scala@v10 + with: + java-version: "adopt@1.${{ matrix.java }}" + - name: Coursier cache + uses: coursier/cache-action@v6 + - name: Cache sbt + uses: actions/cache@v2.1.3 + with: + path: ~/.sbt + key: ${{ runner.os }}-sbt-cache-${{ hashFiles('**/*.sbt') }}-${{ hashFiles('project/build.properties') }} + - name: Build and test + run: | + rm -rf "$HOME/.sbt/scripted/" || true + case ${{ matrix.jobtype }} in + 1) + sbt -v "mimaReportBinaryIssues ; javafmtCheck ; Test / javafmtCheck; scalafmtCheckAll ; scalafmtSbtCheck; serverTestProj/scalafmtCheckAll; headerCheck ;test:headerCheck ;whitesourceOnPush ;test:compile; publishLocal; test; serverTestProj/test; doc; $UTIL_TESTS; ++$SCALA_213; $UTIL_TESTS" + ;; + 2) + sbt -v "scripted actions/* apiinfo/* compiler-project/* ivy-deps-management/* reporter/* tests/* watch/* classloader-cache/* package/*" + ;; + 3) + sbt -v "dependencyTreeProj/publishLocal; scripted dependency-graph/* dependency-management/* plugins/* project-load/* java/* run/* nio/*" + ;; + 4) + sbt -v "repoOverrideTest:scripted dependency-management/*; scripted source-dependencies/* project/*" + ;; + 5) + sbt -v "++$SCALA_213!; test;" + ;; + 6) + # build from fresh IO, LM, and Zinc + BUILD_VERSION="1.5.0-SNAPSHOT" + cd io + sbt -v -Dsbt.build.version=${BUILD_VERSION} +publishLocal + cd ../ + sbt -Dsbtlm.path=$HOME/work/sbt/sbt/librarymanagement -Dsbtzinc.path=$HOME/work/sbt/sbt/zinc -Dsbt.build.version=$BUILD_VERSION -Dsbt.build.fatal=false "+lowerUtils/publishLocal; {librarymanagement}/publishLocal; {zinc}/publishLocal; upperModules/publishLocal" + rm -r $(find $HOME/.sbt/boot -name "*-SNAPSHOT") || true + sbt -v -Dsbt.version=$BUILD_VERSION "++$SCALA_213; $UTIL_TESTS; ++$SCALA_212; $UTIL_TESTS; scripted actions/* source-dependencies/*1of3 dependency-management/*1of4 java/*" + ;; + *) + echo unknown jobtype + exit 1 + esac + rm -rf "$HOME/.sbt/scripted/" || true + rm -rf "$HOME/.ivy2/local" || true + rm -r $(find $HOME/.sbt/boot -name "*-SNAPSHOT") || true + find $HOME/Library/Caches/Coursier/v1 -name "ivydata-*.properties" -delete || true + find $HOME/.ivy2/cache -name "ivydata-*.properties" -delete || true + find $HOME/.cache/coursier/v1 -name "ivydata-*.properties" -delete || true + find $HOME/.sbt -name "*.lock" -delete || true diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml new file mode 100644 index 000000000..7adaddb2a --- /dev/null +++ b/.github/workflows/nightly.yml @@ -0,0 +1,63 @@ +name: Nightly +on: + schedule: + # 08:00 UTC = 03:00 EST + - cron: '0 8 * * *' + +jobs: + deploy: + strategy: + matrix: + include: + - os: ubuntu-latest + java: 8 + runs-on: ${{ matrix.os }} + env: + JAVA_OPTS: -Xms800M -Xmx800M -Xss6M -XX:ReservedCodeCacheSize=128M -server -Dsbt.io.virtual=false -Dfile.encoding=UTF-8 + steps: + - name: Checkout sbt/sbt + uses: actions/checkout@v2 + - name: Checkout sbt/io + uses: actions/checkout@v2 + with: + repository: sbt/io + ref: develop + path: io + - name: Checkout sbt/librarymanagement + uses: actions/checkout@v2 + with: + repository: sbt/librarymanagement + ref: develop + path: librarymanagement + - name: Checkout sbt/zinc + uses: actions/checkout@v2 + with: + repository: sbt/zinc + ref: develop + path: zinc + - name: Setup + uses: olafurpg/setup-scala@v10 + with: + java-version: "adopt@1.${{ matrix.java }}" + - name: Coursier cache + uses: coursier/cache-action@v6 + - name: Build and deploy + run: | + # build from fresh IO, LM, and Zinc + TIMESTAMP=$(TZ=UTC date +%Y%m%dT%H%M%S) + export BUILD_VERSION="1.5.0-bin-${TIMESTAMP}" + cd io + sbt -v +publish + cd ../ + sbt -Dsbtlm.path=$HOME/work/sbt/sbt/librarymanagement -Dsbtzinc.path=$HOME/work/sbt/sbt/zinc -Dsbt.build.fatal=false "+lowerUtils/publish; {librarymanagement}/publish; {zinc}/publish; upperModules/publish; bundledLauncherProj/publish" + rm -rf "$HOME/.ivy2/local" || true + rm -r $(find $HOME/.sbt/boot -name "*-SNAPSHOT") || true + find $HOME/Library/Caches/Coursier/v1 -name "ivydata-*.properties" -delete || true + find $HOME/.ivy2/cache -name "ivydata-*.properties" -delete || true + find $HOME/.cache/coursier/v1 -name "ivydata-*.properties" -delete || true + find $HOME/.sbt -name "*.lock" -delete || true + env: + BINTRAY_USER: ${{ secrets.BINTRAY_USER }} + BINTRAY_PASS: ${{ secrets.BINTRAY_PASS }} + BINTRAY_REPOSITORY: maven-snapshots + BINTRAY_PACKAGE: sbt diff --git a/.gitignore b/.gitignore index e762de7f9..4901fa3a4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,14 @@ target/ __pycache__ +out +node_modules +vscode-sbt-scala/client/server +npm-debug.log +*.vsix +!sbt/src/server-test/completions/target +.big +.idea +.bloop +.metals +.bsp/ +metals.sbt diff --git a/.mailmap b/.mailmap new file mode 100644 index 000000000..dfe6ffc8c --- /dev/null +++ b/.mailmap @@ -0,0 +1,7 @@ +Andrea Peruffo +Ethan Atkins +Ethan Atkins +Eugene Yokota (eed3si9n) +Jorge Vicente Cantero +Kenji Yoshida (xuwei-k) <6b656e6a69@gmail.com> +Yasuhiro Tatsuno diff --git a/.sbtopts b/.sbtopts new file mode 100644 index 000000000..1ca6d0b39 --- /dev/null +++ b/.sbtopts @@ -0,0 +1 @@ +-J-Xmx2G diff --git a/.scalafmt.conf b/.scalafmt.conf new file mode 100644 index 000000000..213dea496 --- /dev/null +++ b/.scalafmt.conf @@ -0,0 +1,23 @@ +version = 2.3.2 +edition = 2019-10 +maxColumn = 100 +project.git = true +project.excludeFilters = [ "\\Wsbt-test\\W", "\\Winput_sources\\W", "\\Wcontraband-scala\\W" ] +lineEndings = preserve + +# https://docs.scala-lang.org/style/scaladoc.html recommends the JavaDoc style. +# scala/scala is written that way too https://github.com/scala/scala/blob/v2.12.2/src/library/scala/Predef.scala +docstrings = JavaDoc + +# This also seems more idiomatic to include whitespace in import x.{ yyy } +spaces.inImportCurlyBraces = true + +# This is more idiomatic Scala. +# https://docs.scala-lang.org/style/indentation.html#methods-with-numerous-arguments +align.openParenCallSite = false +align.openParenDefnSite = false + +# For better code clarity +danglingParentheses = true + +trailingCommas = preserve diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 20fc55510..000000000 --- a/.travis.yml +++ /dev/null @@ -1,51 +0,0 @@ -# Use Docker-based container (instead of OpenVZ) -sudo: false - -cache: - directories: - - $HOME/.ivy2/cache - - $HOME/.sbt/boot - -language: scala - -jdk: - - oraclejdk8 - -matrix: - fast_finish: true - -env: - matrix: - - SBT_CMD=";test:compile;scalariformCheck" - - SBT_CMD="mimaReportBinaryIssues" - - SBT_CMD="safeUnitTests" - - SBT_CMD="otherUnitTests" - - SBT_CMD="scripted actions/*" - - SBT_CMD="scripted apiinfo/*" - - SBT_CMD="scripted compiler-project/*" - - SBT_CMD="scripted dependency-management/*1of2" - - SBT_CMD="scripted dependency-management/*2of2" - - SBT_CMD="scripted ivy-deps-management/*" - - SBT_CMD="scripted java/*" - - SBT_CMD="scripted package/*" - - SBT_CMD="scripted project/*1of2" - - SBT_CMD="scripted project/*2of2" - - SBT_CMD="scripted reporter/*" - - SBT_CMD="scripted run/*" - - SBT_CMD="scripted source-dependencies/*1of3" - - SBT_CMD="scripted source-dependencies/*2of3" - - SBT_CMD="scripted source-dependencies/*3of3" - - SBT_CMD="scripted tests/*" - - SBT_CMD="scripted project-load/*" - - SBT_CMD="repoOverrideTest:scripted dependency-management/*" - -notifications: - email: - - sbt-dev-bot@googlegroups.com - -script: - - sbt -J-XX:ReservedCodeCacheSize=128m "$SBT_CMD" - -before_cache: - - find $HOME/.ivy2 -name "ivydata-*.properties" -print -delete - - find $HOME/.sbt -name "*.lock" -print -delete diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e36cea592..7a90de47c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,57 +1,72 @@ - [StackOverflow]: http://stackoverflow.com/tags/sbt - [ask]: https://stackoverflow.com/questions/ask?tags=sbt - [Setup]: http://www.scala-sbt.org/release/docs/Getting-Started/Setup + [StackOverflow]: https://stackoverflow.com/tags/sbt + [Setup]: https://www.scala-sbt.org/release/docs/Getting-Started/Setup [Issues]: https://github.com/sbt/sbt/issues - [sbt-dev]: https://groups.google.com/d/forum/sbt-dev - [subscriptions]: http://typesafe.com/how/subscription + [sbt-contrib]: https://gitter.im/sbt/sbt-contrib [327]: https://github.com/sbt/sbt/issues/327 + [documentation]: https://github.com/sbt/website + +Contributing +============ + +(For support, see [SUPPORT](./SUPPORT.md)) + +There are lots of ways to contribute to sbt ecosystem depending on your interests and skill level. + +- Help someone at work or online fix their build problem. +- Answer StackOverflow questions. +- Ask StackOverflow questions. +- Create plugins that extend sbt's features. +- Maintain and update [documentation]. +- Garden the issue tracker. +- Report issues. +- Patch the core (send pull requests to code). +- On-ramp other contributors. Issues and Pull Requests -======================== +------------------------ When you find a bug in sbt we want to hear about it. Your bug reports play an important part in making sbt more reliable and usable. Effective bug reports are more likely to be fixed. These guidelines explain how to write such reports and pull requests. -Preliminaries --------------- +Please open a GitHub issue when you are 90% sure it's an actual bug. + +If you have an enhancement idea, or a general discussion, bring it up to [sbt-contrib]. + +### Notes about Documentation + +Documentation fixes and contributions are as much welcome as to patching the core. Visit [sbt/website][documentation] to learn about how to contribute. + +### Preliminaries - Make sure your sbt version is up to date. - Search [StackOverflow] and [Issues] to see whether your bug has already been reported. - Open one case for each problem. - Proceed to the next steps for details. -Where to get help and/or file a bug report ------------------------------------------- - -sbt project uses GitHub Issues as a publicly visible todo list. Please open a GitHub issue only when asked to do so. - -- If you need help with sbt, please [ask] on StackOverflow with the tag "sbt" and the name of the sbt plugin if any. -- If you run into an issue, have an enhancement idea, or a general discussion, bring it up to [sbt-dev] Google Group first. -- If you need a faster response time, consider one of the [Typesafe subscriptions][subscriptions]. - -What to report --------------- +### What to report The developers need three things from you: **steps**, **problems**, and **expectations**. -### Steps +The most important thing to remember about bug reporting is to clearly distinguish facts and opinions. -The most important thing to remember about bug reporting is to clearly distinguish facts and opinions. What we need first is **the exact steps to reproduce your problems on our computers**. This is called *reproduction steps*, which is often shortened to "repro steps" or "steps." Describe your method of running sbt. Provide `build.sbt` that caused the problem and the version of sbt or Scala that was used. Provide sample Scala code if it's to do with incremental compilation. If possible, minimize the problem to reduce non-essential factors. +#### Steps + +What we need first is **the exact steps to reproduce your problems on our computers**. This is called *reproduction steps*, which is often shortened to "repro steps" or "steps." Describe your method of running sbt. Provide `build.sbt` that caused the problem and the version of sbt or Scala that was used. Provide sample Scala code if it's to do with incremental compilation. If possible, minimize the problem to reduce non-essential factors. Repro steps are the most important part of a bug report. If we cannot reproduce the problem in one way or the other, the problem can't be fixed. Telling us the error messages is not enough. -### Problems +#### Problems Next, describe the problems, or what *you think* is the problem. It might be "obvious" to you that it's a problem, but it could actually be an intentional behavior for some backward compatibility etc. For compilation errors, include the stack trace. The more raw info the better. -### Expectations +#### Expectations Same as the problems. Describe what *you think* should've happened. -### Notes +#### Notes -Add an optional notes section to describe your analysis. +Add any optional notes section to describe your analysis. ### Subject @@ -61,7 +76,7 @@ The subject of the bug report doesn't matter. A more descriptive subject is cert If possible, please format code or console outputs. -On Github it's: +On GitHub it's: ```scala name := "foo" @@ -81,14 +96,11 @@ Finally, thank you for taking the time to report a problem. Pull Requests ------------- -### Branch to work against - -Whether implementing a new feature, fixing a bug, or modifying documentation, please work against the latest development branch (currently, 0.13). -See below for instructions on building sbt from source. +See below for the branch to work against. ### Adding notes -All pull requests are required to include a "Notes" file which documents the change. This file should reside in the +Most pull requests should include a "Notes" file which documents the change. This file should reside in the directory: @@ -111,92 +123,38 @@ Make sure you document each commit and squash them appropriately. You can use th * Scala's documentation on [Git Hygiene](https://github.com/scala/scala/tree/v2.12.0-M3#git-hygiene) * Play's documentation on [Working with Git](https://www.playframework.com/documentation/2.4.4/WorkingWithGit#Squashing-commits) -Documentation -------------- - -Documentation fixes and contributions are as much welcome as to the source code itself. Visit [the website project](https://github.com/sbt/website) to learn about how to contribute. - Build from source -================= - -1. Install the current stable binary release of sbt (see [Setup]), which will be used to build sbt from source. -2. Get the source code. - - $ git clone git://github.com/sbt/sbt.git - $ cd sbt - -3. The default branch is the development branch [0.13](https://github.com/sbt/sbt/tree/0.13), which contains the latest code for the next major sbt release. To build a specific release or commit, switch to the associated tag. The tag for the latest stable release is [v0.13.9](https://github.com/sbt/sbt/tree/v0.13.9): - - $ git checkout v0.13.9 - - Note that sbt is always built with the previous stable release. For example, the [0.13](https://github.com/sbt/sbt/tree/0.13) branch is built with 0.13.9 and the [v0.13.9](https://github.com/sbt/sbt/tree/v0.13.9) tag is built with 0.13.8. - -4. To build the launcher and publish all components locally, - - $ sbt - > publishLocal - -5. To use this locally built version of sbt, copy your stable `~/bin/sbt` script to `~/bin/xsbt` and change it to use the launcher jar at `/launch/target/sbt-launch.jar`. - - Directory `target` is removed by `clean` command. Second solution is using the artifact stored in the local ivy repository. - - The launcher is located in: - - $HOME/.ivy2/local/org.scala-sbt/sbt-launch/0.13.9/jars/sbt-launch.jar - - for v0.13.9 tag, or in: - - $HOME/.ivy2/local/org.scala-sbt/sbt-launch/0.13.10-SNAPSHOT/jars/sbt-launch.jar - - for the development branch. - -## Modifying sbt - -1. When developing sbt itself, run `compile` when checking compilation only. - -2. To use your modified version of sbt in a project locally, run `publishLocal`. - -3. After each `publishLocal`, clean the `~/.sbt/boot/` directory. Alternatively, if sbt is running and the launcher hasn't changed, run `reboot full` to have sbt do this for you. - -4. If a project has `project/build.properties` defined, either delete the file or change `sbt.version` to `0.13.10-SNAPSHOT`. - -## Diagnosing build failures - -Globally included plugins can interfere building `sbt`; if you are getting errors building sbt, try disabling all globally included plugins and try again. - -Running Tests -============= - -sbt has an extensive test suite of Unit tests and Integration tests! - -Unit / Functional tests ------------------------ - -Various functional and unit tests are defined throughout the -project. To run all of them, run `sbt test`. You can run a single test -suite with `sbt testOnly` - -Integration tests ----------------- -Scripted integration tests reside in `sbt/src/sbt-test` and are -written using the same testing infrastructure sbt plugin authors can -use to test their own plugins with sbt. You can read more about this -style of tests [here](http://eed3si9n.com/testing-sbt-plugins). +See [DEVELOPING](./DEVELOPING.md) -You can run the integration tests with the `sbt scripted` sbt -command. To run a single test, such as the test in -`sbt/src/sbt-test/project/global-plugin`, simply run: +Profiling sbt +------------- - sbt "scripted project/global-plugin" +See [PROFILING](./PROFILING.md) -Please note that these tests run PAINFULLY slow if the version set in -`build.sbt` is set to SNAPSHOT, as every time the scripted test boots -up a test instance of sbt, remote mirrors are scanned for possible -updates. It is recommended that you set the version suffix to -`-devel`, as in `0.13.10-devel`. +Other notes for maintainers +--------------------------- -Building Documentation -====================== +### Publishing VS Code Extensions -The scala-sbt.org site documentation is a separate project [website](https://github.com/sbt/website). Follow [the steps in the README](https://github.com/sbt/website#scala-sbtorg) to generate the documentation. +Reference https://code.visualstudio.com/docs/extensions/publish-extension + +``` +$ sbt +> vscodePlugin/compile +> exit +cd vscode-sbt-scala/client +# update version number in vscode-sbt-scala/client/package.json +$ vsce package +$ vsce publish +``` + +## Signing the CLA + +Contributing to sbt requires you or your employer to sign the +[Lightbend Contributor License Agreement](https://www.lightbend.com/contribute/cla). + +To make it easier to respect our license agreements, we have added an sbt task +that takes care of adding the LICENSE headers to new files. Run `headerCreate` +and sbt will put a copyright notice into it. diff --git a/DEVELOPING.md b/DEVELOPING.md new file mode 100644 index 000000000..691720ea2 --- /dev/null +++ b/DEVELOPING.md @@ -0,0 +1,154 @@ +Developer guide +=============== + +### Branch to work against + +sbt uses two branches for development: + +- Development branch: `develop` (this is also called "master") +- Stable branch: `1.$MINOR.x`, where `$MINOR` is current minor version (e.g. `1.1.x` during 1.1.x series) + + +### Instruction to build just sbt + +If the change you are making is contained in sbt/sbt, you could publishLocal on sbt/sbt: + +``` +$ sbt +sbt:sbtRoot> publishLocal +``` + +### Instruction to build all modules from source + +1. Install the current stable binary release of sbt (see [Setup]), which will be used to build sbt from source. +2. Get the source code. + + ``` + $ mkdir sbt-modules + $ cd sbt-modules + $ for i in sbt io librarymanagement zinc; do \ + git clone https://github.com/sbt/$i.git && (cd $i; git checkout -b develop origin/develop) + done + $ cd sbt + $ ./sbt-allsources.sh + ``` + +3. To build and publish all components locally, + + ``` + $ ./sbt-allsources.sh + sbt:sbtRoot> publishLocalAllModule + ``` + +### Using the locally built sbt + +The `publishLocal` above will build and publish version `1.$MINOR.$PATCH-SNAPSHOT` (e.g. 1.1.2-SNAPSHOT) to your local ivy repository. + +To use the locally built sbt, set the version in `build.properties` file in your project to `1.$MINOR.$PATCH-SNAPSHOT` then launch `sbt` (this can be the `sbt` launcher installed in your machine). + +``` +$ cd $YOUR_OWN_PROJECT +$ sbt +> compile +``` + +### Nightly builds + +The latest development versions are available as nightly builds on sbt-maven-snapshots () repo, which is a redirect proxy whose underlying repository is subject to change it could be Bintray, Linux box, etc. + +To use a nightly build: + +1. Find out a version from [/org/scala-sbt/sbt/](https://repo.scala-sbt.org/scalasbt/maven-snapshots/org/scala-sbt/sbt/). +2. Put the version, for example `sbt.version=1.5.0-bin-20201121T081131` in `project/build.properties`. + +sbt launcher will resolve the specified sbt core artifacts. Because of the aforementioned redirection, this resolution is going to be very slow for the first time you run sbt, and then it should be ok for subsequent runs. + +Unless you're debugging the `sbt` script or the launcher JAR, you should be able to use any recent stable version of sbt installation as the launcher following the [Setup][Setup] instructions first. + +If you're overriding the repositories via `~/.sbt/repositories`, make sure that there's a following entry: + +``` +[repositories] + ... + sbt-maven-snapshots: https://repo.scala-sbt.org/scalasbt/maven-snapshots/, bootOnly +``` + +### Clearing out boot and local cache + +When you run a locally built sbt, the JAR artifacts will be now cached under `$HOME/.sbt/boot/scala-2.12.6/org.scala-sbt/sbt/1.$MINOR.$PATCH-SNAPSHOT` directory. To clear this out run: `reboot dev` command from sbt's session of your test application. + +One drawback of `-SNAPSHOT` version is that it's slow to resolve as it tries to hit all the resolvers. You can workaround that by using a version name like `1.$MINOR.$PATCH-LOCAL1`. A non-SNAPSHOT artifacts will now be cached under `$HOME/.ivy/cache/` directory, so you need to clear that out using [sbt-dirty-money](https://github.com/sbt/sbt-dirty-money)'s `cleanCache` task. + +### Running sbt "from source" - `sbtOn` + +In addition to locally publishing a build of sbt, there is an alternative, experimental launcher within sbt/sbt +to be able to run sbt "from source", that is to compile sbt and run it from its resulting classfiles rather than +from published jar files. + +Such a launcher is available within sbt/sbt's build through a custom `sbtOn` command that takes as its first +argument the directory on which you want to run sbt, and the remaining arguments are passed _to_ that sbt +instance. For example: + +I have setup a minimal sbt build in the directory `/s/t`, to run sbt on that directory I call: + +```bash +> sbtOn /s/t +[info] Packaging /d/sbt/scripted/sbt/target/scala-2.12/scripted-sbt_2.12-1.2.0-SNAPSHOT.jar ... +[info] Done packaging. +[info] Running (fork) sbt.RunFromSourceMain /s/t +Listening for transport dt_socket at address: 5005 +[info] Loading settings from idea.sbt,global-plugins.sbt ... +[info] Loading global plugins from /Users/dnw/.dotfiles/.sbt/1.0/plugins +[info] Loading project definition from /s/t/project +[info] Set current project to t (in build file:/s/t/) +[info] sbt server started at local:///Users/dnw/.sbt/1.0/server/ce9baa494c7598e4d59b/sock +> show baseDirectory +[info] /s/t +> exit +[info] shutting down sbt server +[success] Total time: 19 s, completed 25-Apr-2018 15:04:58 +``` + +Please note that this alternative launcher does _not_ have feature parity with sbt/launcher. (Meta) +contributions welcome! :-D + +### Diagnosing build failures + +Globally included plugins can interfere building `sbt`; if you are getting errors building sbt, try disabling all globally included plugins and try again. + +### Running Tests + +sbt has a suite of unit tests and integration tests, also known as scripted tests. + +#### Unit / Functional tests + +Various functional and unit tests are defined throughout the +project. To run all of them, run `sbt test`. You can run a single test +suite with `sbt testOnly` + +#### Integration tests + +Scripted integration tests reside in `sbt/src/sbt-test` and are +written using the same testing infrastructure sbt plugin authors can +use to test their own plugins with sbt. You can read more about this +style of tests [here](https://www.scala-sbt.org/1.0/docs/Testing-sbt-plugins). + +You can run the integration tests with the `sbt scripted` sbt +command. To run a single test, such as the test in +`sbt/src/sbt-test/project/global-plugin`, simply run: + + sbt "scripted project/global-plugin" + +### Random tidbits + +#### Import statements + +You'd need alternative DSL import since you can't rely on sbt package object. + +```scala +// for slash syntax +import sbt.SlashSyntax0._ + +// for IO +import sbt.io.syntax._ +``` diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md deleted file mode 100644 index 748cd9fd1..000000000 --- a/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,16 +0,0 @@ -(See the guidelines for contributing, linked above) - -## steps - - - -## problem - - - -## expectation - - - -## notes - diff --git a/LICENSE b/LICENSE index d70192438..a5c4997c4 100644 --- a/LICENSE +++ b/LICENSE @@ -1,25 +1,203 @@ -Copyright (c) 2008-2014 Typesafe Inc, Mark Harrah, Grzegorz Kossakowski, Josh Suereth, Indrajit Raychaudhuri, Eugene Yokota, and other contributors. -All rights reserved. -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions -are met: -1. Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. -2. Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. -3. The name of the author may not be used to endorse or promote products - derived from this software without specific prior written permission. + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ -THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR -IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES -OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. -IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, -INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT -NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF -THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright (c) 2011 - 2018, Lightbend, Inc. + Copyright (c) 2008 - 2010, Mark Harrah + + 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/MIGRATION.md b/MIGRATION.md deleted file mode 100644 index 6f7d67952..000000000 --- a/MIGRATION.md +++ /dev/null @@ -1,21 +0,0 @@ -Migration notes -=============== - -- Build definition is based on Scala 2.11.8 -- Build.scala style builds are gone. Use multi-project `build.sbt`. -- `Project(...)` constructor is restricted down to two parameters. Use `project` instead. -- `sbt.Plugin` is also gone. Use auto plugins. -- The incremental compiler, called Zinc, uses class-based name hashing. -- Zinc drops support for Scala 2.8.x and 2.9.x. -- Removed the pre-0.13.7 *.sbt file parser (previously available under `-Dsbt.parser.simple=true`) -- Removed old, hyphen-separated key names (use `publishLocal` instead of `publish-local`) -- Removes no-longer-documented old operators `<<=`, `<+=`, and `<++=`. -- Renames early command feature from `--` to `early()`. -- Log options `-error`, `-warn`, `-info`, `-debug` are added as shorthand for `"early(error)"` etc. - -#### Additional import required - -Implicit conversions are moved to `sbt.syntax`. Add the following imports to auto plugins -or `project/*.scala`. - - import sbt._, syntax._, Keys._ diff --git a/NOTICE b/NOTICE index 55efecac8..82bd2c916 100644 --- a/NOTICE +++ b/NOTICE @@ -1,6 +1,7 @@ sbt -Copyright (c) 2008-2014 Typesafe Inc, Mark Harrah, Grzegorz Kossakowski, Josh Suereth, Indrajit Raychaudhuri, Eugene Yokota, and other contributors. -Licensed under BSD-style license (see LICENSE) +Copyright 2011 - 2017, Lightbend, Inc. +Copyright 2008 - 2010, Mark Harrah +Licensed under Apache v2 license (see LICENSE) Portions based on code from the Scala compiler. Portions of the Scala library are distributed with the launcher. @@ -33,4 +34,3 @@ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - \ No newline at end of file diff --git a/PROFILING.md b/PROFILING.md new file mode 100644 index 000000000..02b235e18 --- /dev/null +++ b/PROFILING.md @@ -0,0 +1,153 @@ +Profiling sbt +------------- + +There are several ways to profile sbt. The new hotness in profiling is FlameGraph. +You first collect stack trace samples, and then it is processed into svg graph. +See: + +- [Using FlameGraphs To Illuminate The JVM by Nitsan Wakart](https://www.youtube.com/watch?v=ugRrFdda_JQ) +- [USENIX ATC '17: Visualizing Performance with Flame Graphs](https://www.youtube.com/watch?v=D53T1Ejig1Q) + +### jvm-profiling-tools/async-profiler + +The first one I recommend is async-profiler. This is available for macOS and Linux, +and works fairly well. + +1. Download the installer from https://github.com/jvm-profiling-tools/async-profiler/releases/tag/v1.2 +2. Make symbolic link to `build/` and `profiler.sh` to `$HOME/bin`, assuming you have PATH to `$HOME/bin`: + `ln -s ~/Applications/async-profiler/profiler.sh $HOME/bin/profiler.sh` + `ln -s ~/Applications/async-profiler/build $HOME/bin/build` + +Next, close all Java applications and anything that may affect the profiling, and run sbt in one terminal: + +``` +$ sbt exit +``` + +In another terminal, run: + +``` +$ jps +92746 sbt-launch.jar +92780 Jps +``` + +This tells you the process ID of sbt. In this case, it's 92746. While it's running, run + +``` +$ profiler.sh -d 60 +Started [cpu] profiling +--- Execution profile --- +Total samples: 31602 +Non-Java: 3239 (10.25%) +GC active: 46 (0.15%) +Unknown (native): 14667 (46.41%) +Not walkable (native): 3 (0.01%) +Unknown (Java): 433 (1.37%) +Not walkable (Java): 8 (0.03%) +Thread exit: 1 (0.00%) +Deopt: 9 (0.03%) + +Frame buffer usage: 55.658% + +Total: 1932000000 (6.11%) samples: 1932 + [ 0] java.lang.ClassLoader$NativeLibrary.load + [ 1] java.lang.ClassLoader.loadLibrary0 + [ 2] java.lang.ClassLoader.loadLibrary + [ 3] java.lang.Runtime.loadLibrary0 + [ 4] java.lang.System.loadLibrary +.... +``` + +This should show a bunch of stacktraces that are useful. +To visualize this as a flamegraph, run: + +``` +$ profiler.sh -d 60 -f /tmp/flamegraph.svg +``` + +This should produce `/tmp/flamegraph.svg` at the end. + +![flamegraph](project/flamegraph_svg.png) + +See https://gist.github.com/eed3si9n/82d43acc95a002876d357bd8ad5f40d5 + +### running sbt with standby + +One of the tricky things you come across while profiling is figuring out the process ID, +while wnating to profile the beginning of the application. + +For this purpose, we've added `sbt.launcher.standby` JVM flag. +In the next version of sbt, you should be able to run: + +``` +$ sbt -J-Dsbt.launcher.standby=20s exit +``` + +This will count down for 20s before doing anything else. + +### jvm-profiling-tools/perf-map-agent + +If you want to try the mixed flamegraph, you can try perf-map-agent. +This uses `dtrace` on macOS and `perf` on Linux. + +You first have to compile https://github.com/jvm-profiling-tools/perf-map-agent. +For macOS, here to how to export `JAVA_HOME` before running `cmake .`: + +``` +$ export JAVA_HOME=$(/usr/libexec/java_home) +$ cmake . +-- The C compiler identification is AppleClang 9.0.0.9000039 +-- The CXX compiler identification is AppleClang 9.0.0.9000039 +... +$ make +``` + +In addition, you have to git clone https://github.com/brendangregg/FlameGraph + +In a fresh termimal, run sbt with `-XX:+PreserveFramePointer` flag: + +``` +$ sbt -J-Dsbt.launcher.standby=20s -J-XX:+PreserveFramePointer exit +``` + +In the terminal that you will run the perf-map: + +``` +$ cd quicktest/ +$ export JAVA_HOME=$(/usr/libexec/java_home) +$ export FLAMEGRAPH_DIR=$HOME/work/FlameGraph +$ jps +94592 Jps +94549 sbt-launch.jar +$ $HOME/work/perf-map-agent/bin/dtrace-java-flames 94549 +dtrace: system integrity protection is on, some features will not be available + +dtrace: description 'profile-99 ' matched 2 probes +Flame graph SVG written to DTRACE_FLAME_OUTPUT='/Users/xxx/work/quicktest/flamegraph-94549.svg'. +``` + +This would produce better flamegraph in theory, but the output looks too messy for `sbt exit` case. +See https://gist.github.com/eed3si9n/b5856ff3d987655513380d1a551aa0df +This might be because it assumes that the operations are already JITed. + +### ktoso/sbt-jmh + +https://github.com/ktoso/sbt-jmh + +Due to JIT warmup etc, benchmarking is difficult. JMH runs the same tests multiple times to +remove these effects and comes closer to measuring the performance of your code. + +There's also an integration with jvm-profiling-tools/async-profiler, apparently. + +### VisualVM + +I'd also mention traditional JVM profiling tool. Since VisualVM is opensource, +I'll mention this one: https://visualvm.github.io/ + +1. First VisualVM. +2. Start sbt from a terminal. +3. You should see `xsbt.boot.Boot` under Local. +4. Open it, and select either sampler or profiler, and hit CPU button at the point when you want to start. + +If you are familiar with YourKit, it also works similarly. diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index 0392fc0ee..000000000 --- a/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1 +0,0 @@ -(See the guidelines for contributing, linked above) diff --git a/README.md b/README.md index d71adbfeb..4224db130 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,16 @@ +[![Build Status](https://travis-ci.com/sbt/sbt.svg?branch=develop)](https://travis-ci.com/github/sbt/sbt) +[![Latest version](https://img.shields.io/github/tag/sbt/sbt.svg)](https://index.scala-lang.org/sbt/sbt) +[![Gitter Chat](https://badges.gitter.im/sbt/sbt.svg)](https://gitter.im/sbt/sbt) + [sbt/sbt-zero-seven]: https://github.com/sbt/sbt-zero-seven [CONTRIBUTING]: CONTRIBUTING.md - [Setup]: http://www.scala-sbt.org/release/docs/Getting-Started/Setup - [FAQ]: http://www.scala-sbt.org/release/docs/Faq.html + [Setup]: https://www.scala-sbt.org/release/docs/Getting-Started/Setup + [FAQ]: https://www.scala-sbt.org/release/docs/Faq.html [sbt-dev]: https://groups.google.com/d/forum/sbt-dev - [searching]: http://stackoverflow.com/tags/sbt + [searching]: https://stackoverflow.com/tags/sbt [asking]: https://stackoverflow.com/questions/ask?tags=sbt [LICENSE]: LICENSE [sbt/io]: https://github.com/sbt/io - [sbt/util]: https://github.com/sbt/util [sbt/librarymanagement]: https://github.com/sbt/librarymanagement [sbt/zinc]: https://github.com/sbt/zinc [sbt/sbt]: https://github.com/sbt/sbt @@ -17,16 +20,15 @@ sbt sbt is a build tool for Scala, Java, and more. -For general documentation, see http://www.scala-sbt.org/. +For general documentation, see https://www.scala-sbt.org/. -sbt 1.0.x +sbt 1.x --------- -This is the 1.0.x series of sbt. The source code of sbt is split across -several Github repositories, including this one. +This is the 1.x series of sbt. The source code of sbt is split across +several GitHub repositories, including this one. - [sbt/io][sbt/io] hosts `sbt.io` module. -- [sbt/util][sbt/util] hosts a collection of internally used modules. - [sbt/librarymanagement][sbt/librarymanagement] hosts `sbt.librarymanagement` module that wraps Ivy. - [sbt/zinc][sbt/zinc] hosts Zinc, an incremental compiler for Scala. - [sbt/sbt][sbt/sbt], this repository hosts modules that implements the build tool. @@ -42,7 +44,7 @@ Issues and Pull Requests Please read [CONTRIBUTING] carefully before opening a GitHub Issue. -The short version: try [searching] or [asking] on StackOverflow and [sbt-dev]. +The short version: try [searching] or [asking] on StackOverflow. license ------- diff --git a/SUPPORT.md b/SUPPORT.md new file mode 100644 index 000000000..c3c7d0c44 --- /dev/null +++ b/SUPPORT.md @@ -0,0 +1,20 @@ + [ask]: https://stackoverflow.com/questions/ask?tags=sbt + [Lightbend]: https://www.lightbend.com/ + [subscriptions]: https://www.lightbend.com/platform/subscription + [gitter]: https://gitter.im/sbt/sbt + +Support +======= + +[Lightbend] sponsors sbt and encourages contributions from the active community. Enterprises can adopt it for mission critical systems with confidence because Lightbend stands behind sbt with commercial support and services. + +For community support please [ask] on StackOverflow with the tag "sbt" (and the name of the sbt plugin(s) if any). + +- State the problem or question clearly and provide enough context. Code examples and `build.sbt` are often useful when appropriately edited. +- There's also [Gitter sbt/sbt room][gitter], but Stackoverflow is recommended so others can benefit from the answers. + +For professional support, for instance if you need faster response times, [Lightbend], the maintainer of Scala compiler and sbt, provides: + +- [Lightbend Subscriptions][subscriptions], which includes Expert Support +- Training +- Consulting diff --git a/build.sbt b/build.sbt index ffd4ae2f7..cea416b0d 100644 --- a/build.sbt +++ b/build.sbt @@ -1,375 +1,1562 @@ -import Util._ import Dependencies._ -import Sxr.sxr +import Util._ +import com.typesafe.tools.mima.core.ProblemFilters._ +import com.typesafe.tools.mima.core._ +import local.Scripted +import java.nio.file.{ Files, Path => JPath } -import com.typesafe.tools.mima.core._, ProblemFilters._ -import com.typesafe.tools.mima.plugin.MimaKeys.{ binaryIssueFilters, previousArtifact} -import com.typesafe.tools.mima.plugin.MimaPlugin.mimaDefaultSettings +import scala.util.Try // ThisBuild settings take lower precedence, // but can be shared across the multi projects. -def buildLevelSettings: Seq[Setting[_]] = inThisBuild(Seq( - organization := "org.scala-sbt", - version := "1.0.0-SNAPSHOT", - description := "sbt is an interactive build tool", - bintrayOrganization := Some("sbt"), - bintrayRepository := { - if (!isSnapshot.value) "maven-releases" - else "maven-snapshots" - }, - bintrayPackage := "sbt", - bintrayReleaseOnPublish := false, - licenses := List("BSD New" -> url("https://github.com/sbt/sbt/blob/0.13/LICENSE")), - developers := List( - Developer("harrah", "Mark Harrah", "@harrah", url("https://github.com/harrah")), - Developer("eed3si9n", "Eugene Yokota", "@eed3si9n", url("https://github.com/eed3si9n")), - Developer("jsuereth", "Josh Suereth", "@jsuereth", url("https://github.com/jsuereth")), - Developer("dwijnand", "Dale Wijnand", "@dwijnand", url("https://github.com/dwijnand")), - Developer("gkossakowski", "Grzegorz Kossakowski", "@gkossakowski", url("https://github.com/gkossakowski")), - Developer("Duhemm", "Martin Duhem", "@Duhemm", url("https://github.com/Duhemm")) +ThisBuild / version := { + val v = "1.5.0-SNAPSHOT" + nightlyVersion.getOrElse(v) +} +ThisBuild / version2_13 := "2.0.0-SNAPSHOT" +ThisBuild / versionScheme := Some("early-semver") +ThisBuild / scalafmtOnCompile := !(Global / insideCI).value +ThisBuild / Test / scalafmtOnCompile := !(Global / insideCI).value +ThisBuild / turbo := true +ThisBuild / usePipelining := false // !(Global / insideCI).value +ThisBuild / organization := "org.scala-sbt" +ThisBuild / description := "sbt is an interactive build tool" +ThisBuild / licenses := List("Apache-2.0" -> url("https://github.com/sbt/sbt/blob/develop/LICENSE")) +ThisBuild / javacOptions ++= Seq("-source", "1.8", "-target", "1.8") +ThisBuild / Compile / doc / javacOptions := Nil +ThisBuild / developers := List( + Developer("harrah", "Mark Harrah", "@harrah", url("https://github.com/harrah")), + Developer("eed3si9n", "Eugene Yokota", "@eed3si9n", url("https://github.com/eed3si9n")), + Developer("jsuereth", "Josh Suereth", "@jsuereth", url("https://github.com/jsuereth")), + Developer("dwijnand", "Dale Wijnand", "@dwijnand", url("https://github.com/dwijnand")), + Developer("eatkins", "Ethan Atkins", "@eatkins", url("https://github.com/eatkins")), + Developer( + "gkossakowski", + "Grzegorz Kossakowski", + "@gkossakowski", + url("https://github.com/gkossakowski") ), - homepage := Some(url("https://github.com/sbt/sbt")), - scmInfo := Some(ScmInfo(url("https://github.com/sbt/sbt"), "git@github.com:sbt/sbt.git")), - resolvers += Resolver.mavenLocal -)) + Developer("Duhemm", "Martin Duhem", "@Duhemm", url("https://github.com/Duhemm")) +) +ThisBuild / homepage := Some(url("https://github.com/sbt/sbt")) +ThisBuild / scmInfo := Some( + ScmInfo(url("https://github.com/sbt/sbt"), "git@github.com:sbt/sbt.git") +) +ThisBuild / resolvers += Resolver.mavenLocal -def commonSettings: Seq[Setting[_]] = Seq[SettingsDefinition]( - scalaVersion := scala211, +Global / semanticdbEnabled := !(Global / insideCI).value +Global / semanticdbVersion := "4.4.10" +val excludeLint = SettingKey[Set[Def.KeyedInitialize[_]]]("excludeLintKeys") +Global / excludeLint := (Global / excludeLint).?.value.getOrElse(Set.empty) +Global / excludeLint += componentID +Global / excludeLint += whitesourceIgnoredScopes +Global / excludeLint += scriptedBufferLog +Global / excludeLint += checkPluginCross + +def commonBaseSettings: Seq[Setting[_]] = Def.settings( + headerLicense := Some( + HeaderLicense.Custom( + """|sbt + |Copyright 2011 - 2018, Lightbend, Inc. + |Copyright 2008 - 2010, Mark Harrah + |Licensed under Apache License 2.0 (see LICENSE) + |""".stripMargin + ) + ), + scalaVersion := baseScalaVersion, componentID := None, - resolvers += Resolver.typesafeIvyRepo("releases"), + resolvers += Resolver.typesafeIvyRepo("releases").withName("typesafe-sbt-build-ivy-releases"), resolvers += Resolver.sonatypeRepo("snapshots"), - resolvers += "bintray-sbt-maven-releases" at "https://dl.bintray.com/sbt/maven-releases/", + resolvers += Resolver.url( + "bintray-scala-hedgehog", + url("https://dl.bintray.com/hedgehogqa/scala-hedgehog") + )(Resolver.ivyStylePatterns), + testFrameworks += TestFramework("hedgehog.sbt.Framework"), + testFrameworks += TestFramework("verify.runner.Framework"), concurrentRestrictions in Global += Util.testExclusiveRestriction, - testOptions += Tests.Argument(TestFrameworks.ScalaCheck, "-w", "1"), - javacOptions in compile ++= Seq("-target", "6", "-source", "6", "-Xlint", "-Xlint:-serial"), - incOptions := incOptions.value.withNameHashing(true), - crossScalaVersions := Seq(scala211), - bintrayPackage := (bintrayPackage in ThisBuild).value, - bintrayRepository := (bintrayRepository in ThisBuild).value, - mimaDefaultSettings, + testOptions in Test += Tests.Argument(TestFrameworks.ScalaCheck, "-w", "1"), + testOptions in Test += Tests.Argument(TestFrameworks.ScalaCheck, "-verbosity", "2"), + javacOptions in compile ++= Seq("-Xlint", "-Xlint:-serial"), + Compile / doc / scalacOptions ++= { + import scala.sys.process._ + val devnull = ProcessLogger(_ => ()) + val tagOrSha = ("git describe --exact-match" #|| "git rev-parse HEAD").lineStream(devnull).head + Seq( + "-sourcepath", + (baseDirectory in LocalRootProject).value.getAbsolutePath, + "-doc-source-url", + s"https://github.com/sbt/sbt/tree/$tagOrSha€{FILE_PATH}.scala" + ) + }, + Compile / javafmtOnCompile := Def + .taskDyn(if ((scalafmtOnCompile).value) Compile / javafmt else Def.task(())) + .value, + Test / javafmtOnCompile := Def + .taskDyn(if ((Test / scalafmtOnCompile).value) Test / javafmt else Def.task(())) + .value, + Compile / unmanagedSources / inputFileStamps := + (Compile / unmanagedSources / inputFileStamps).dependsOn(Compile / javafmtOnCompile).value, + Test / unmanagedSources / inputFileStamps := + (Test / unmanagedSources / inputFileStamps).dependsOn(Test / javafmtOnCompile).value, + crossScalaVersions := List(scala212, scala213), publishArtifact in Test := false, - previousArtifact := None, // Some(organization.value % moduleName.value % "1.0.0"), - binaryIssueFilters ++= Seq( - ) -) flatMap (_.settings) + fork in run := true, +) +def commonSettings: Seq[Setting[_]] = + commonBaseSettings :+ + addCompilerPlugin(kindProjector) +def utilCommonSettings: Seq[Setting[_]] = + baseSettings :+ (crossScalaVersions := (scala212 :: scala213 :: Nil)) def minimalSettings: Seq[Setting[_]] = commonSettings ++ customCommands ++ - publishPomSettings ++ Release.javaVersionCheckSettings + publishPomSettings def baseSettings: Seq[Setting[_]] = - minimalSettings ++ Seq(projectComponent) ++ baseScalacOptions ++ Licensed.settings ++ Formatting.settings + minimalSettings ++ Seq(projectComponent) ++ baseScalacOptions ++ Licensed.settings def testedBaseSettings: Seq[Setting[_]] = baseSettings ++ testDependencies -lazy val sbtRoot: Project = (project in file(".")). - enablePlugins(ScriptedPlugin). - configs(Sxr.sxrConf). - aggregate(nonRoots: _*). - settings( - buildLevelSettings, +val sbt13Plus = Seq("1.3.0") +val sbt10Plus = + Seq( + "1.0.0", + "1.0.1", + "1.0.2", + "1.0.3", + "1.0.4", + "1.1.0", + "1.1.1", + "1.1.2", + "1.1.3", + "1.1.4", + "1.1.5", + "1.1.6", + "1.2.0", + "1.2.1", + /*DOA,*/ "1.2.3", + "1.2.4", + /*DOA,*/ "1.2.6", + "1.2.7", + "1.2.8", + ) ++ sbt13Plus +val noUtilVersion = + Set("1.0.4", "1.1.4", "1.1.5", "1.1.6", "1.2.3", "1.2.4", "1.2.6", "1.2.7", "1.2.8") + +val mimaSettings = mimaSettingsSince(sbt10Plus) +val utilMimaSettings = mimaSettingsSince(sbt10Plus.filterNot(noUtilVersion)) +def mimaSettingsSince(versions: Seq[String]): Seq[Def.Setting[_]] = Def settings ( + mimaPreviousArtifacts := { + val crossVersion = if (crossPaths.value) CrossVersion.binary else CrossVersion.disabled + versions.map(v => organization.value % moduleName.value % v cross crossVersion).toSet + }, + mimaBinaryIssueFilters ++= Seq( + // Changes in the internal package + exclude[DirectMissingMethodProblem]("sbt.internal.*"), + exclude[FinalClassProblem]("sbt.internal.*"), + exclude[FinalMethodProblem]("sbt.internal.*"), + exclude[IncompatibleResultTypeProblem]("sbt.internal.*"), + exclude[ReversedMissingMethodProblem]("sbt.internal.*") + ), +) + +val scriptedSbtReduxMimaSettings = Def.settings(mimaPreviousArtifacts := Set()) + +lazy val sbtRoot: Project = (project in file(".")) +// .enablePlugins(ScriptedPlugin) + .aggregate(nonRoots: _*) + .settings( minimalSettings, - rootSettings, + onLoadMessage := { + val version = sys.props("java.specification.version") + """ __ __ + | _____/ /_ / /_ + | / ___/ __ \/ __/ + | (__ ) /_/ / /_ + | /____/_.___/\__/ + |Welcome to the build for sbt. + |""".stripMargin + + (if (version != "1.8") + s"""!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + | Java version is $version. We recommend java 8. + |!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!""".stripMargin + else "") + }, + Util.baseScalacOptions, + Docs.settings, + scalacOptions += "-Ymacro-expand:none", // for both sxr and doc + Util.publishPomSettings, + otherRootSettings, + Transform.conscriptSettings(bundledLauncherProj), publish := {}, - publishLocal := {} + publishLocal := {}, + skip in publish := true, + commands in Global += Command + .single("sbtOn")((state, dir) => s"sbtProj/test:runMain sbt.RunFromSourceMain $dir" :: state), + mimaSettings, + mimaPreviousArtifacts := Set.empty, + buildThinClient := (sbtClientProj / buildThinClient).evaluated, + buildNativeThinClient := (sbtClientProj / buildNativeThinClient).value, + installNativeThinClient := { + // nativeInstallDirectory can be set globally or in a gitignored local file + val dir = nativeInstallDirectory.?.value + val target = Def.spaceDelimited("").parsed.headOption match { + case Some(p) => file(p).toPath + case _ => + dir match { + case Some(d) => d / "sbtn" + case _ => + val msg = "Expected input parameter : installNativeExecutable /usr/local/bin" + throw new IllegalStateException(msg) + } + } + val base = baseDirectory.value.toPath + val exec = (sbtClientProj / buildNativeThinClient).value + streams.value.log.info(s"installing thin client ${base.relativize(exec)} to ${target}") + Files.copy(exec, target, java.nio.file.StandardCopyOption.REPLACE_EXISTING) + } ) // This is used to configure an sbt-launcher for this version of sbt. lazy val bundledLauncherProj = - (project in file("launch")). - settings( - minimalSettings, - inConfig(Compile)(Transform.configSettings), - Release.launcherSettings(sbtLaunchJar) - ). - enablePlugins(SbtLauncherPlugin). - settings( - name := "sbt-launch", - moduleName := "sbt-launch", - description := "sbt application launcher", - autoScalaLibrary := false, - crossPaths := false, - publish := Release.deployLauncher.value, - publishLauncher := Release.deployLauncher.value, - packageBin in Compile := sbtLaunchJar.value - ) + (project in file("launch")) + .enablePlugins(SbtLauncherPlugin) + .settings( + minimalSettings, + inConfig(Compile)(Transform.configSettings), + ) + .settings( + name := "sbt-launch", + moduleName := "sbt-launch", + description := "sbt application launcher", + autoScalaLibrary := false, + crossPaths := false, + packageBin in Compile := sbtLaunchJar.value, + mimaSettings, + mimaPreviousArtifacts := Set() + ) /* ** subproject declarations ** */ +val collectionProj = (project in file("internal") / "util-collection") + .settings( + testedBaseSettings, + utilCommonSettings, + Util.keywordsSettings, + name := "Collections", + libraryDependencies ++= Seq(sjsonNewScalaJson.value), + libraryDependencies ++= (CrossVersion.partialVersion(scalaVersion.value) match { + case Some((2, major)) if major <= 12 => Seq() + case _ => Seq("org.scala-lang.modules" %% "scala-parallel-collections" % "0.2.0") + }), + mimaSettings, + mimaBinaryIssueFilters ++= Seq( + // Added private[sbt] method to capture State attributes. + exclude[ReversedMissingMethodProblem]("sbt.internal.util.AttributeMap.setCond"), + // Dropped in favour of kind-projector's inline type lambda syntax + exclude[MissingClassProblem]("sbt.internal.util.TypeFunctions$P1of2"), + // Dropped in favour of kind-projector's polymorphic lambda literals + exclude[MissingClassProblem]("sbt.internal.util.Param"), + exclude[MissingClassProblem]("sbt.internal.util.Param$"), + // Dropped in favour of plain scala.Function, and its compose method + exclude[MissingClassProblem]("sbt.internal.util.Fn1"), + exclude[DirectMissingMethodProblem]("sbt.internal.util.TypeFunctions.toFn1"), + exclude[DirectMissingMethodProblem]("sbt.internal.util.Types.toFn1"), + // Instead of defining foldr in KList & overriding in KCons, + // it's now abstract in KList and defined in both KCons & KNil. + exclude[FinalMethodProblem]("sbt.internal.util.KNil.foldr"), + exclude[DirectAbstractMethodProblem]("sbt.internal.util.KList.foldr"), + exclude[IncompatibleSignatureProblem]("sbt.internal.util.Init*.*"), + exclude[IncompatibleSignatureProblem]("sbt.internal.util.Settings0.*"), + exclude[IncompatibleSignatureProblem]("sbt.internal.util.EvaluateSettings#INode.*"), + exclude[IncompatibleSignatureProblem]("sbt.internal.util.TypeFunctions.*"), + exclude[IncompatibleSignatureProblem]("sbt.internal.util.EvaluateSettings.*"), + exclude[IncompatibleSignatureProblem]("sbt.internal.util.Settings.*"), + exclude[IncompatibleSignatureProblem]("sbt.internal.util.EvaluateSettings#MixedNode.*"), + exclude[IncompatibleSignatureProblem]("sbt.internal.util.EvaluateSettings#BindNode.this"), + exclude[IncompatibleSignatureProblem]( + "sbt.internal.util.EvaluateSettings#BindNode.dependsOn" + ), + exclude[IncompatibleSignatureProblem]("sbt.internal.util.Types.some") + ), + ) + .dependsOn(utilPosition) + +// Command line-related utilities. +val completeProj = (project in file("internal") / "util-complete") + .dependsOn(collectionProj, utilControl, utilLogging) + .settings( + testedBaseSettings, + name := "Completion", + libraryDependencies += jline, + libraryDependencies += jline3Reader, + libraryDependencies += jline3Builtins, + mimaSettings, + // Parser is used publicly, so we can't break bincompat. + mimaBinaryIssueFilters := Seq( + exclude[DirectMissingMethodProblem]("sbt.internal.util.complete.SoftInvalid.apply"), + exclude[DirectMissingMethodProblem]("sbt.internal.util.complete.Invalid.apply"), + exclude[DirectMissingMethodProblem]("sbt.internal.util.complete.Finite.apply"), + exclude[DirectMissingMethodProblem]("sbt.internal.util.complete.Infinite.decrement"), + exclude[DirectMissingMethodProblem]("sbt.internal.util.complete.History.this"), + exclude[IncompatibleMethTypeProblem]("sbt.internal.util.complete.Completion.suggestion"), + exclude[IncompatibleMethTypeProblem]("sbt.internal.util.complete.Completion.token"), + exclude[IncompatibleMethTypeProblem]("sbt.internal.util.complete.Completion.displayOnly"), + exclude[IncompatibleSignatureProblem]("sbt.internal.util.complete.*"), + ), + ) + .configure(addSbtIO) + +// A logic with restricted negation as failure for a unique, stable model +val logicProj = (project in file("internal") / "util-logic") + .dependsOn(collectionProj, utilRelation) + .settings( + testedBaseSettings, + name := "Logic", + mimaSettings, + ) + +// defines Java structures used across Scala versions, such as the API structures and relationships extracted by +// the analysis compiler phases and passed back to sbt. The API structures are defined in a simple +// format from which Java sources are generated by the datatype generator Projproject +lazy val utilInterface = (project in file("internal") / "util-interface").settings( + utilCommonSettings, + crossScalaVersions := List(scala212), + javaOnlySettings, + crossPaths := false, + name := "Util Interface", + exportJars := true, + utilMimaSettings, +) + +lazy val utilControl = (project in file("internal") / "util-control").settings( + utilCommonSettings, + name := "Util Control", + utilMimaSettings, +) + +lazy val utilPosition = (project in file("internal") / "util-position") + .settings( + utilCommonSettings, + name := "Util Position", + scalacOptions += "-language:experimental.macros", + libraryDependencies ++= Seq(scalaReflect.value, scalatest % "test"), + utilMimaSettings, + ) + +lazy val utilLogging = (project in file("internal") / "util-logging") + .enablePlugins(ContrabandPlugin, JsonCodecPlugin) + .dependsOn(utilInterface, collectionProj, coreMacrosProj) + .settings( + utilCommonSettings, + name := "Util Logging", + libraryDependencies ++= + Seq( + jline, + jline3Terminal, + jline3JNA, + jline3Jansi, + log4jApi, + log4jCore, + disruptor, + sjsonNewScalaJson.value, + scalaReflect.value + ), + libraryDependencies ++= Seq(scalacheck % "test", scalatest % "test"), + Compile / scalacOptions ++= (scalaVersion.value match { + case v if v.startsWith("2.12.") => List("-Ywarn-unused:-locals,-explicits,-privates") + case _ => List() + }), + sourceManaged in (Compile, generateContrabands) := baseDirectory.value / "src" / "main" / "contraband-scala", + managedSourceDirectories in Compile += + baseDirectory.value / "src" / "main" / "contraband-scala", + contrabandFormatsForType in generateContrabands in Compile := { tpe => + val old = (contrabandFormatsForType in generateContrabands in Compile).value + val name = tpe.removeTypeParameters.name + if (name == "Throwable") Nil + else old(tpe) + }, + utilMimaSettings, + mimaBinaryIssueFilters ++= Seq( + exclude[DirectMissingMethodProblem]("sbt.internal.util.SuccessEvent.copy*"), + exclude[DirectMissingMethodProblem]("sbt.internal.util.TraceEvent.copy*"), + exclude[DirectMissingMethodProblem]("sbt.internal.util.StringEvent.copy*"), + // Private final class constructors changed + exclude[DirectMissingMethodProblem]("sbt.util.InterfaceUtil#ConcretePosition.this"), + exclude[DirectMissingMethodProblem]("sbt.util.InterfaceUtil#ConcreteProblem.this"), + exclude[ReversedMissingMethodProblem]("sbt.internal.util.ConsoleOut.flush"), + // This affects Scala 2.11 only it seems, so it's ok? + exclude[InheritedNewAbstractMethodProblem]( + "sbt.internal.util.codec.JsonProtocol.LogOptionFormat" + ), + exclude[InheritedNewAbstractMethodProblem]( + "sbt.internal.util.codec.JsonProtocol.ProgressItemFormat" + ), + exclude[InheritedNewAbstractMethodProblem]( + "sbt.internal.util.codec.JsonProtocol.ProgressEventFormat" + ), + exclude[DirectMissingMethodProblem]("sbt.internal.util.MainAppender.*"), + exclude[IncompatibleMethTypeProblem]("sbt.internal.util.BufferedAppender.*"), + exclude[IncompatibleMethTypeProblem]("sbt.internal.util.ManagedLogger.this"), + exclude[IncompatibleMethTypeProblem]("sbt.internal.util.ManagedLogger.this"), + exclude[IncompatibleMethTypeProblem]("sbt.internal.util.MainAppender*"), + exclude[IncompatibleMethTypeProblem]("sbt.internal.util.GlobalLogging.*"), + exclude[IncompatibleSignatureProblem]("sbt.internal.util.GlobalLogging.*"), + exclude[IncompatibleSignatureProblem]("sbt.internal.util.MainAppender*"), + exclude[MissingTypesProblem]("sbt.internal.util.ConsoleAppender"), + exclude[MissingTypesProblem]("sbt.internal.util.BufferedAppender"), + ), + ) + .configure(addSbtIO) + +lazy val utilRelation = (project in file("internal") / "util-relation") + .settings( + utilCommonSettings, + name := "Util Relation", + libraryDependencies ++= Seq(scalacheck % "test"), + utilMimaSettings, + ) + +// Persisted caching based on sjson-new +lazy val utilCache = (project in file("util-cache")) + .settings( + utilCommonSettings, + name := "Util Cache", + libraryDependencies ++= + Seq(sjsonNewScalaJson.value, sjsonNewMurmurhash.value, scalaReflect.value), + libraryDependencies ++= Seq(scalatest % "test"), + utilMimaSettings, + mimaBinaryIssueFilters ++= Seq( + // Added a method to a sealed trait, technically not a problem for Scala + exclude[ReversedMissingMethodProblem]("sbt.util.HashFileInfo.hashArray"), + ) + ) + .configure(addSbtIO) + +// Builds on cache to provide caching for filesystem-related operations +lazy val utilTracking = (project in file("util-tracking")) + .dependsOn(utilCache) + .settings( + utilCommonSettings, + name := "Util Tracking", + libraryDependencies ++= Seq(scalatest % "test"), + utilMimaSettings, + mimaBinaryIssueFilters ++= Seq( + // Private final class constructors changed + ProblemFilters.exclude[IncompatibleMethTypeProblem]("sbt.util.Tracked#CacheHelp.this"), + ) + ) + .configure(addSbtIO) + +lazy val utilScripted = (project in file("internal") / "util-scripted") + .dependsOn(utilLogging, utilInterface) + .settings( + utilCommonSettings, + name := "Util Scripted", + libraryDependencies += scalaParsers, + utilMimaSettings, + ) + .configure(addSbtIO) /* **** Intermediate-level Modules **** */ // Runner for uniform test interface -lazy val testingProj = (project in file("testing")). - dependsOn(testAgentProj). - settings( +lazy val testingProj = (project in file("testing")) + .enablePlugins(ContrabandPlugin, JsonCodecPlugin) + .dependsOn(testAgentProj, utilLogging) + .settings( baseSettings, name := "Testing", - libraryDependencies ++= Seq(sbtIO, testInterface,launcherInterface, compilerClasspath, utilLogging) + libraryDependencies ++= Seq( + scalaXml, + testInterface, + launcherInterface, + sjsonNewScalaJson.value + ), + Compile / scalacOptions += "-Ywarn-unused:-locals,-explicits,-privates", + managedSourceDirectories in Compile += + baseDirectory.value / "src" / "main" / "contraband-scala", + sourceManaged in (Compile, generateContrabands) := baseDirectory.value / "src" / "main" / "contraband-scala", + contrabandFormatsForType in generateContrabands in Compile := ContrabandConfig.getFormats, + mimaSettings, + mimaBinaryIssueFilters ++= Seq( + // private[sbt] + exclude[IncompatibleMethTypeProblem]("sbt.TestStatus.write"), + exclude[IncompatibleResultTypeProblem]("sbt.TestStatus.read"), + // copy method was never meant to be public + exclude[DirectMissingMethodProblem]("sbt.protocol.testing.EndTestGroupErrorEvent.copy"), + exclude[DirectMissingMethodProblem]( + "sbt.protocol.testing.EndTestGroupErrorEvent.copy$default$*" + ), + exclude[DirectMissingMethodProblem]("sbt.protocol.testing.EndTestGroupEvent.copy"), + exclude[DirectMissingMethodProblem]("sbt.protocol.testing.EndTestGroupEvent.copy$default$*"), + exclude[DirectMissingMethodProblem]("sbt.protocol.testing.StartTestGroupEvent.copy"), + exclude[DirectMissingMethodProblem]( + "sbt.protocol.testing.StartTestGroupEvent.copy$default$*" + ), + exclude[DirectMissingMethodProblem]("sbt.protocol.testing.TestCompleteEvent.copy"), + exclude[DirectMissingMethodProblem]("sbt.protocol.testing.TestCompleteEvent.copy$default$*"), + exclude[DirectMissingMethodProblem]("sbt.protocol.testing.TestInitEvent.copy"), + exclude[DirectMissingMethodProblem]("sbt.protocol.testing.TestItemDetail.copy"), + exclude[DirectMissingMethodProblem]("sbt.protocol.testing.TestItemDetail.copy$default$*"), + exclude[DirectMissingMethodProblem]("sbt.protocol.testing.TestItemEvent.copy"), + exclude[DirectMissingMethodProblem]("sbt.protocol.testing.TestItemEvent.copy$default$*"), + exclude[DirectMissingMethodProblem]("sbt.protocol.testing.TestStringEvent.copy"), + exclude[DirectMissingMethodProblem]("sbt.protocol.testing.TestStringEvent.copy$default$1"), + //no reason to use + exclude[DirectMissingMethodProblem]("sbt.JUnitXmlTestsListener.testSuite"), + ) ) + .configure(addSbtIO, addSbtCompilerClasspath) // Testing agent for running tests in a separate process. -lazy val testAgentProj = (project in file("testing") / "agent"). - settings( +lazy val testAgentProj = (project in file("testing") / "agent") + .settings( minimalSettings, - crossScalaVersions := Seq(scala211), + crossScalaVersions := Seq(baseScalaVersion), crossPaths := false, autoScalaLibrary := false, name := "Test Agent", - libraryDependencies += testInterface + libraryDependencies += testInterface, + mimaSettings, ) // Basic task engine -lazy val taskProj = (project in file("tasks")). - settings( +lazy val taskProj = (project in file("tasks")) + .dependsOn(collectionProj, utilControl) + .settings( testedBaseSettings, name := "Tasks", - libraryDependencies ++= Seq(utilControl, utilCollection) + mimaSettings, + mimaBinaryIssueFilters ++= Seq( + exclude[IncompatibleSignatureProblem]("sbt.Triggers.this"), + exclude[IncompatibleSignatureProblem]("sbt.Triggers.runBefore"), + exclude[IncompatibleSignatureProblem]("sbt.Triggers.injectFor"), + exclude[IncompatibleSignatureProblem]("sbt.Triggers.onComplete"), + exclude[DirectMissingMethodProblem]("sbt.Inc.apply"), + // ok because sbt.ExecuteProgress has been under private[sbt] + exclude[IncompatibleResultTypeProblem]("sbt.ExecuteProgress.initial"), + exclude[DirectMissingMethodProblem]("sbt.ExecuteProgress.*"), + exclude[ReversedMissingMethodProblem]("sbt.ExecuteProgress.*"), + exclude[IncompatibleSignatureProblem]("sbt.ExecuteProgress.*"), + // ok because sbt.Execute has been under private[sbt] + exclude[IncompatibleSignatureProblem]("sbt.Execute.*"), + exclude[IncompatibleSignatureProblem]("sbt.Execute#CyclicException.*"), + exclude[IncompatibleSignatureProblem]("sbt.NodeView.*"), + ) ) // Standard task system. This provides map, flatMap, join, and more on top of the basic task model. -lazy val stdTaskProj = (project in file("tasks-standard")). - dependsOn (taskProj % "compile;test->test"). - settings( +lazy val stdTaskProj = (project in file("tasks-standard")) + .dependsOn(collectionProj, utilLogging, utilCache) + .dependsOn(taskProj % "compile;test->test") + .settings( testedBaseSettings, name := "Task System", testExclusive, - libraryDependencies ++= Seq(utilCollection, utilLogging, sbtIO) + mimaSettings, + mimaBinaryIssueFilters ++= Seq( + // unused private[sbt] + exclude[DirectMissingMethodProblem]("sbt.Task.mapTask"), + ), ) + .configure(addSbtIO) // Embedded Scala code runner -lazy val runProj = (project in file("run")). - settings( +lazy val runProj = (project in file("run")) + .enablePlugins(ContrabandPlugin) + .dependsOn(collectionProj, utilLogging, utilControl) + .settings( testedBaseSettings, name := "Run", - libraryDependencies ++= Seq(sbtIO, - utilLogging, compilerClasspath) + Compile / scalacOptions += "-Ywarn-unused:-locals,-explicits,-privates", + managedSourceDirectories in Compile += + baseDirectory.value / "src" / "main" / "contraband-scala", + sourceManaged in (Compile, generateContrabands) := baseDirectory.value / "src" / "main" / "contraband-scala", + mimaSettings, + mimaBinaryIssueFilters ++= Seq( + // copy method was never meant to be public + exclude[DirectMissingMethodProblem]("sbt.ForkOptions.copy"), + exclude[DirectMissingMethodProblem]("sbt.ForkOptions.copy$default$*"), + exclude[DirectMissingMethodProblem]("sbt.OutputStrategy#BufferedOutput.copy"), + exclude[DirectMissingMethodProblem]("sbt.OutputStrategy#BufferedOutput.copy$default$*"), + exclude[DirectMissingMethodProblem]("sbt.OutputStrategy#CustomOutput.copy"), + exclude[DirectMissingMethodProblem]("sbt.OutputStrategy#CustomOutput.copy$default$*"), + exclude[DirectMissingMethodProblem]("sbt.OutputStrategy#LoggedOutput.copy"), + exclude[DirectMissingMethodProblem]("sbt.OutputStrategy#LoggedOutput.copy$default$*"), + ) + ) + .configure(addSbtIO, addSbtCompilerClasspath) + +val sbtProjDepsCompileScopeFilter = + ScopeFilter( + inDependencies(LocalProject("sbtProj"), includeRoot = false), + inConfigurations(Compile) ) -lazy val scriptedSbtProj = (project in scriptedPath / "sbt"). - dependsOn(commandProj). - settings( +lazy val scriptedSbtReduxProj = (project in file("scripted-sbt-redux")) + .dependsOn(sbtProj % "compile;test->test", commandProj, utilLogging, utilScripted) + .settings( + baseSettings, + name := "Scripted sbt Redux", + libraryDependencies ++= Seq(launcherInterface % "provided"), + mimaSettings, + scriptedSbtReduxMimaSettings, + ) + .configure(addSbtIO, addSbtCompilerInterface, addSbtLmCore) + +lazy val scriptedSbtOldProj = (project in file("scripted-sbt-old")) + .dependsOn(scriptedSbtReduxProj) + .settings( baseSettings, name := "Scripted sbt", - libraryDependencies ++= Seq(launcherInterface % "provided", - sbtIO, utilLogging, compilerInterface, utilScripted) + mimaSettings, + mimaBinaryIssueFilters ++= Seq( + // sbt.test package is renamed to sbt.scriptedtest. + exclude[MissingClassProblem]("sbt.test.*"), + exclude[DirectMissingMethodProblem]("sbt.test.*"), + exclude[IncompatibleMethTypeProblem]("sbt.test.*"), + exclude[IncompatibleSignatureProblem]("sbt.test.*"), + ), ) -lazy val scriptedPluginProj = (project in scriptedPath / "plugin"). - dependsOn(sbtProj). - settings( +lazy val scriptedPluginProj = (project in file("scripted-plugin")) + .settings( baseSettings, name := "Scripted Plugin", - libraryDependencies ++= Seq(compilerClasspath) + mimaSettings, + mimaBinaryIssueFilters ++= Seq( + // scripted plugin has moved into sbt mothership. + exclude[MissingClassProblem]("sbt.ScriptedPlugin*") + ), + ) + +lazy val dependencyTreeProj = (project in file("dependency-tree")) + .dependsOn(sbtProj) + .settings( + sbtPlugin := true, + baseSettings, + name := "sbt-dependency-tree", + publishMavenStyle := true, + // mimaSettings, + mimaPreviousArtifacts := Set.empty, ) // Implementation and support code for defining actions. -lazy val actionsProj = (project in file("main-actions")). - dependsOn(runProj, stdTaskProj, taskProj, testingProj). - settings( +lazy val actionsProj = (project in file("main-actions")) + .dependsOn( + completeProj, + runProj, + stdTaskProj, + taskProj, + testingProj, + utilLogging, + utilRelation, + utilTracking, + ) + .settings( testedBaseSettings, name := "Actions", - libraryDependencies ++= Seq(compilerClasspath, utilCompletion, compilerApiInfo, - zinc, compilerIvyIntegration, compilerInterface, - sbtIO, utilLogging, utilRelation, libraryManagement, utilTracking) + libraryDependencies += sjsonNewScalaJson.value, + libraryDependencies += jline3Terminal, + mimaSettings, + mimaBinaryIssueFilters ++= Seq( + // Removed unused private[sbt] nested class + exclude[MissingClassProblem]("sbt.Doc$Scaladoc"), + // Removed no longer used private[sbt] method + exclude[DirectMissingMethodProblem]("sbt.Doc.generate"), + exclude[DirectMissingMethodProblem]("sbt.compiler.Eval.filesModifiedBytes"), + exclude[DirectMissingMethodProblem]("sbt.compiler.Eval.fileModifiedBytes"), + ), + ) + .configure( + addSbtIO, + addSbtCompilerInterface, + addSbtCompilerClasspath, + addSbtCompilerApiInfo, + addSbtLmCore, + addSbtZinc + ) + +lazy val protocolProj = (project in file("protocol")) + .enablePlugins(ContrabandPlugin, JsonCodecPlugin) + .dependsOn(collectionProj, utilLogging) + .settings( + testedBaseSettings, + name := "Protocol", + libraryDependencies ++= Seq(sjsonNewScalaJson.value, ipcSocket), + Compile / scalacOptions += "-Ywarn-unused:-locals,-explicits,-privates", + managedSourceDirectories in Compile += + baseDirectory.value / "src" / "main" / "contraband-scala", + sourceManaged in (Compile, generateContrabands) := baseDirectory.value / "src" / "main" / "contraband-scala", + contrabandFormatsForType in generateContrabands in Compile := ContrabandConfig.getFormats, + mimaSettings, + mimaBinaryIssueFilters ++= Seq( + // copy method was never meant to be public + exclude[DirectMissingMethodProblem]("sbt.protocol.ChannelAcceptedEvent.copy"), + exclude[DirectMissingMethodProblem]("sbt.protocol.ChannelAcceptedEvent.copy$default$1"), + exclude[DirectMissingMethodProblem]("sbt.protocol.ExecCommand.copy"), + exclude[DirectMissingMethodProblem]("sbt.protocol.ExecCommand.copy$default$1"), + exclude[DirectMissingMethodProblem]("sbt.protocol.ExecCommand.copy$default$2"), + exclude[DirectMissingMethodProblem]("sbt.protocol.ExecStatusEvent.copy"), + exclude[DirectMissingMethodProblem]("sbt.protocol.ExecStatusEvent.copy$default$*"), + exclude[DirectMissingMethodProblem]("sbt.protocol.ExecutionEvent.copy"), + exclude[DirectMissingMethodProblem]("sbt.protocol.ExecutionEvent.copy$default$*"), + exclude[DirectMissingMethodProblem]("sbt.protocol.InitCommand.copy"), + exclude[DirectMissingMethodProblem]("sbt.protocol.InitCommand.copy$default$*"), + exclude[DirectMissingMethodProblem]("sbt.protocol.LogEvent.copy"), + exclude[DirectMissingMethodProblem]("sbt.protocol.LogEvent.copy$default$*"), + exclude[DirectMissingMethodProblem]("sbt.protocol.SettingQuery.copy"), + exclude[DirectMissingMethodProblem]("sbt.protocol.SettingQuery.copy$default$1"), + exclude[DirectMissingMethodProblem]("sbt.protocol.SettingQueryFailure.copy"), + exclude[DirectMissingMethodProblem]("sbt.protocol.SettingQueryFailure.copy$default$*"), + exclude[DirectMissingMethodProblem]("sbt.protocol.SettingQuerySuccess.copy"), + exclude[DirectMissingMethodProblem]("sbt.protocol.SettingQuerySuccess.copy$default$*"), + // ignore missing or incompatible methods in sbt.internal + exclude[IncompatibleMethTypeProblem]("sbt.internal.*"), + exclude[DirectMissingMethodProblem]("sbt.internal.*"), + exclude[MissingTypesProblem]("sbt.internal.protocol.JsonRpcResponseError"), + ) ) // General command support and core commands not specific to a build system -lazy val commandProj = (project in file("main-command")). - enablePlugins(DatatypePlugin, JsonCodecPlugin). - settings( +lazy val commandProj = (project in file("main-command")) + .enablePlugins(ContrabandPlugin, JsonCodecPlugin) + .dependsOn(protocolProj, completeProj, utilLogging) + .settings( testedBaseSettings, name := "Command", - libraryDependencies ++= Seq(launcherInterface, compilerInterface, - sbtIO, utilLogging, utilCompletion, compilerClasspath, sjsonNewScalaJson), - sourceManaged in (Compile, generateDatatypes) := baseDirectory.value / "src" / "main" / "datatype-scala" + libraryDependencies ++= Seq(launcherInterface, sjsonNewScalaJson.value, templateResolverApi), + Compile / scalacOptions += "-Ywarn-unused:-locals,-explicits,-privates", + managedSourceDirectories in Compile += + baseDirectory.value / "src" / "main" / "contraband-scala", + sourceManaged in (Compile, generateContrabands) := baseDirectory.value / "src" / "main" / "contraband-scala", + contrabandFormatsForType in generateContrabands in Compile := ContrabandConfig.getFormats, + mimaSettings, + mimaBinaryIssueFilters ++= Vector( + exclude[DirectMissingMethodProblem]("sbt.Exit.apply"), + exclude[DirectMissingMethodProblem]("sbt.Reboot.apply"), + exclude[DirectMissingMethodProblem]("sbt.TemplateResolverInfo.apply"), + // dropped private[sbt] method + exclude[DirectMissingMethodProblem]("sbt.BasicCommands.compatCommands"), + // dropped mainly internal command strings holder + exclude[MissingClassProblem]("sbt.BasicCommandStrings$Compat$"), + exclude[DirectMissingMethodProblem]("sbt.BasicCommands.rebootOptionParser"), + // Changed the signature of Server method. nacho cheese. + exclude[DirectMissingMethodProblem]("sbt.internal.server.Server.*"), + // Added method to ServerInstance. This is also internal. + exclude[ReversedMissingMethodProblem]("sbt.internal.server.ServerInstance.*"), + // Added method to CommandChannel. internal. + exclude[ReversedMissingMethodProblem]("sbt.internal.CommandChannel.*"), + // Added an overload to reboot. The overload is private[sbt]. + exclude[ReversedMissingMethodProblem]("sbt.StateOps.reboot"), + // Replace nailgun socket stuff + exclude[MissingClassProblem]("sbt.internal.NG*"), + exclude[MissingClassProblem]("sbt.internal.ReferenceCountedFileDescriptor"), + // made private[sbt] method private[this] + exclude[DirectMissingMethodProblem]("sbt.State.handleException"), + // copy method was never meant to be public + exclude[DirectMissingMethodProblem]("sbt.CommandSource.copy"), + exclude[DirectMissingMethodProblem]("sbt.CommandSource.copy$default$*"), + exclude[DirectMissingMethodProblem]("sbt.Exec.copy"), + exclude[DirectMissingMethodProblem]("sbt.Exec.copy$default$*"), + // internal + exclude[ReversedMissingMethodProblem]("sbt.internal.client.ServerConnection.*"), + exclude[MissingTypesProblem]("sbt.internal.server.ServerConnection*"), + exclude[IncompatibleSignatureProblem]("sbt.internal.server.ServerConnection.*") + ), + unmanagedSources in (Compile, headerCreate) := { + val old = (unmanagedSources in (Compile, headerCreate)).value + old filterNot { x => + (x.getName startsWith "NG") || (x.getName == "ReferenceCountedFileDescriptor.java") + } + }, + ) + .configure( + addSbtIO, + addSbtCompilerInterface, + addSbtCompilerClasspath, + addSbtLmCore, + addSbtZinc + ) + +// The core macro project defines the main logic of the DSL, abstracted +// away from several sbt implementors (tasks, settings, et cetera). +lazy val coreMacrosProj = (project in file("core-macros")) + .dependsOn(collectionProj) + .settings( + baseSettings :+ (crossScalaVersions := (scala212 :: scala213 :: Nil)), + name := "Core Macros", + libraryDependencies += "org.scala-lang" % "scala-compiler" % scalaVersion.value, + SettingKey[Boolean]("exportPipelining") := false, + mimaSettings, ) // Fixes scope=Scope for Setting (core defined in collectionProj) to define the settings system used in build definitions -lazy val mainSettingsProj = (project in file("main-settings")). - dependsOn(commandProj, stdTaskProj). - settings( +lazy val mainSettingsProj = (project in file("main-settings")) + .dependsOn( + completeProj, + commandProj, + stdTaskProj, + coreMacrosProj, + utilLogging, + utilCache, + utilRelation, + ) + .settings( testedBaseSettings, name := "Main Settings", - libraryDependencies ++= Seq(utilCache, utilApplyMacro, compilerInterface, utilRelation, - utilLogging, sbtIO, utilCompletion, compilerClasspath, libraryManagement) + testOptions in Test ++= { + val cp = (Test / fullClasspathAsJars).value.map(_.data).mkString(java.io.File.pathSeparator) + val framework = TestFrameworks.ScalaTest + Tests.Argument(framework, s"-Dsbt.server.classpath=$cp") :: + Tests.Argument(framework, s"-Dsbt.server.version=${version.value}") :: + Tests.Argument(framework, s"-Dsbt.server.scala.version=${scalaVersion.value}") :: Nil + }, + mimaSettings, + mimaBinaryIssueFilters ++= Seq( + exclude[IncompatibleSignatureProblem]("sbt.Previous#References.getReferences"), + exclude[IncompatibleSignatureProblem]("sbt.Def.delegate"), + exclude[IncompatibleSignatureProblem]("sbt.Def.add"), + exclude[IncompatibleSignatureProblem]("sbt.Def.grouped"), + exclude[IncompatibleSignatureProblem]("sbt.Def.compile"), + exclude[IncompatibleSignatureProblem]("sbt.Def.asTransform"), + exclude[DirectMissingMethodProblem]("sbt.Def.StaticScopes"), + exclude[IncompatibleSignatureProblem]("sbt.Previous.this"), + exclude[DirectMissingMethodProblem]("sbt.BuildRef.apply"), + exclude[DirectMissingMethodProblem]("sbt.ScopeMask.apply"), + exclude[DirectMissingMethodProblem]("sbt.Def.intersect"), + exclude[DirectMissingMethodProblem]("sbt.LocalProject.apply"), + exclude[DirectMissingMethodProblem]("sbt.std.InitializeInstance.pure"), + exclude[DirectMissingMethodProblem]("sbt.std.InitializeInstance.flatten"), + exclude[DirectMissingMethodProblem]("sbt.std.InitializeInstance.map"), + exclude[DirectMissingMethodProblem]("sbt.std.InitializeInstance.app"), + exclude[DirectMissingMethodProblem]("sbt.std.ParserInstance.pure"), + exclude[DirectMissingMethodProblem]("sbt.std.ParserInstance.map"), + exclude[DirectMissingMethodProblem]("sbt.std.ParserInstance.app"), + exclude[DirectMissingMethodProblem]("sbt.std.ParserInstance.pure"), + exclude[DirectMissingMethodProblem]("sbt.std.TaskInstance.pure"), + exclude[DirectMissingMethodProblem]("sbt.std.TaskInstance.flatten"), + exclude[DirectMissingMethodProblem]("sbt.std.TaskInstance.map"), + exclude[DirectMissingMethodProblem]("sbt.std.TaskInstance.app"), + exclude[DirectMissingMethodProblem]("sbt.std.FullInstance.flatten"), + exclude[DirectMissingMethodProblem]("sbt.Scope.display012StyleMasked"), + // added a method to a sealed trait + exclude[InheritedNewAbstractMethodProblem]("sbt.Scoped.canEqual"), + exclude[InheritedNewAbstractMethodProblem]("sbt.ScopedTaskable.canEqual"), + // widened ScopedTaskable parameter to (new) supertype Taskable + exclude[IncompatibleSignatureProblem]("sbt.Scoped#RichTaskable*.this"), + exclude[IncompatibleSignatureProblem]("sbt.TupleSyntax.t*ToTable*"), + ), + ) + .configure( + addSbtIO, + addSbtCompilerInterface, + addSbtCompilerClasspath, + addSbtLmCore ) -// The main integration project for sbt. It brings all of the Projsystems together, configures them, and provides for overriding conventions. -lazy val mainProj = (project in file("main")). - dependsOn(actionsProj, mainSettingsProj, runProj, commandProj). - settings( +lazy val zincLmIntegrationProj = (project in file("zinc-lm-integration")) + .settings( + name := "Zinc LM Integration", + testedBaseSettings, + testOptions in Test += + Tests.Argument(TestFrameworks.ScalaTest, s"-Dsbt.zinc.version=$zincVersion"), + mimaSettingsSince(sbt13Plus), + mimaBinaryIssueFilters ++= Seq( + exclude[IncompatibleMethTypeProblem]("sbt.internal.inc.ZincComponentCompiler*"), + exclude[IncompatibleSignatureProblem]("sbt.internal.inc.ZincComponentCompiler*"), + exclude[IncompatibleSignatureProblem]("sbt.internal.inc.ZincLMHelper.update"), + ), + libraryDependencies += launcherInterface, + ) + .configure(addSbtZincCompileCore, addSbtLmCore, addSbtLmIvyTest) + +// The main integration project for sbt. It brings all of the projects together, configures them, and provides for overriding conventions. +lazy val mainProj = (project in file("main")) + .enablePlugins(ContrabandPlugin) + .dependsOn( + logicProj, + actionsProj, + mainSettingsProj, + runProj, + commandProj, + collectionProj, + scriptedPluginProj, + zincLmIntegrationProj, + utilLogging, + ) + .settings( testedBaseSettings, name := "Main", - libraryDependencies ++= scalaXml.value ++ Seq(launcherInterface, compilerInterface, - sbtIO, utilLogging, utilLogic, libraryManagement, zincCompile) + checkPluginCross := { + val sv = scalaVersion.value + val f = baseDirectory.value / "src" / "main" / "scala" / "sbt" / "PluginCross.scala" + if (sv.startsWith("2.12") && !IO.readLines(f).exists(_.contains(s""""$sv""""))) { + sys.error(s"PluginCross.scala does not match up with the scalaVersion $sv") + } + }, + libraryDependencies ++= + (Seq(scalaXml, launcherInterface, caffeine, lmCoursierShaded) ++ log4jModules), + libraryDependencies ++= (scalaVersion.value match { + case v if v.startsWith("2.12.") => List() + case _ => List(scalaPar) + }), + managedSourceDirectories in Compile += + baseDirectory.value / "src" / "main" / "contraband-scala", + sourceManaged in (Compile, generateContrabands) := baseDirectory.value / "src" / "main" / "contraband-scala", + testOptions in Test += Tests + .Argument(TestFrameworks.ScalaCheck, "-minSuccessfulTests", "1000"), + SettingKey[Boolean]("usePipelining") := false, + mimaSettings, + mimaBinaryIssueFilters ++= Vector( + // New and changed methods on KeyIndex. internal. + exclude[ReversedMissingMethodProblem]("sbt.internal.KeyIndex.*"), + // internal + exclude[IncompatibleMethTypeProblem]("sbt.internal.*"), + // Changed signature or removed private[sbt] methods + exclude[DirectMissingMethodProblem]("sbt.Classpaths.unmanagedLibs0"), + exclude[DirectMissingMethodProblem]("sbt.Defaults.allTestGroupsTask"), + exclude[DirectMissingMethodProblem]("sbt.Plugins.topologicalSort"), + exclude[IncompatibleMethTypeProblem]("sbt.Defaults.allTestGroupsTask"), + exclude[DirectMissingMethodProblem]("sbt.StandardMain.shutdownHook"), + exclude[DirectMissingMethodProblem]("sbt.nio.Keys.compileBinaryFileInputs"), + exclude[DirectMissingMethodProblem]("sbt.nio.Keys.compileSourceFileInputs"), + exclude[MissingClassProblem]("sbt.internal.ResourceLoaderImpl"), + exclude[IncompatibleSignatureProblem]("sbt.internal.ConfigIndex.*"), + exclude[IncompatibleSignatureProblem]("sbt.internal.Inspect.*"), + exclude[IncompatibleSignatureProblem]("sbt.internal.ProjectIndex.*"), + exclude[IncompatibleSignatureProblem]("sbt.internal.BuildIndex.*"), + exclude[IncompatibleSignatureProblem]("sbt.internal.server.BuildServerReporter.*"), + exclude[VirtualStaticMemberProblem]("sbt.internal.server.LanguageServerProtocol.*"), + exclude[IncompatibleSignatureProblem]("sbt.internal.librarymanagement.IvyXml.*"), + exclude[IncompatibleSignatureProblem]("sbt.ScriptedPlugin.*Settings"), + exclude[IncompatibleSignatureProblem]("sbt.plugins.SbtPlugin.*Settings"), + // Removed private internal classes + exclude[MissingClassProblem]("sbt.internal.ReverseLookupClassLoaderHolder$BottomClassLoader"), + exclude[MissingClassProblem]( + "sbt.internal.ReverseLookupClassLoaderHolder$ReverseLookupClassLoader$ResourceLoader" + ), + exclude[MissingClassProblem]("sbt.internal.ReverseLookupClassLoaderHolder$ClassLoadingLock"), + exclude[MissingClassProblem]( + "sbt.internal.ReverseLookupClassLoaderHolder$ReverseLookupClassLoader" + ), + exclude[MissingClassProblem]("sbt.internal.LayeredClassLoaderImpl"), + exclude[MissingClassProblem]("sbt.internal.FileManagement"), + exclude[MissingClassProblem]("sbt.internal.FileManagement$"), + exclude[MissingClassProblem]("sbt.internal.FileManagement$CopiedFileTreeRepository"), + exclude[MissingClassProblem]("sbt.internal.server.LanguageServerReporter*"), + exclude[MissingClassProblem]("sbt.internal.ExternalHooks"), + exclude[MissingClassProblem]("sbt.internal.ExternalHooks$"), + // false positives + exclude[DirectMissingMethodProblem]("sbt.plugins.IvyPlugin.requires"), + exclude[DirectMissingMethodProblem]("sbt.plugins.JUnitXmlReportPlugin.requires"), + exclude[DirectMissingMethodProblem]("sbt.plugins.Giter8TemplatePlugin.requires"), + exclude[DirectMissingMethodProblem]("sbt.plugins.JvmPlugin.requires"), + exclude[DirectMissingMethodProblem]("sbt.plugins.SbtPlugin.requires"), + exclude[DirectMissingMethodProblem]("sbt.ResolvedClasspathDependency.apply"), + exclude[DirectMissingMethodProblem]("sbt.ClasspathDependency.apply"), + exclude[IncompatibleSignatureProblem]("sbt.plugins.SemanticdbPlugin.globalSettings"), + // File -> Source + exclude[DirectMissingMethodProblem]("sbt.Defaults.cleanFilesTask"), + exclude[IncompatibleSignatureProblem]("sbt.Defaults.resourceConfigPaths"), + exclude[IncompatibleSignatureProblem]("sbt.Defaults.sourceConfigPaths"), + exclude[IncompatibleSignatureProblem]("sbt.Defaults.configPaths"), + exclude[IncompatibleSignatureProblem]("sbt.Defaults.paths"), + exclude[IncompatibleSignatureProblem]("sbt.Keys.csrPublications"), + exclude[IncompatibleSignatureProblem]( + "sbt.coursierint.CoursierArtifactsTasks.coursierPublicationsTask" + ), + exclude[IncompatibleSignatureProblem]( + "sbt.coursierint.CoursierArtifactsTasks.coursierPublicationsTask" + ), + exclude[IncompatibleSignatureProblem]("sbt.coursierint.LMCoursier.coursierConfiguration"), + exclude[IncompatibleSignatureProblem]("sbt.coursierint.LMCoursier.publicationsSetting"), + exclude[IncompatibleSignatureProblem]("sbt.Project.inThisBuild"), + exclude[IncompatibleSignatureProblem]("sbt.Project.inConfig"), + exclude[IncompatibleSignatureProblem]("sbt.Project.inTask"), + exclude[IncompatibleSignatureProblem]("sbt.Project.inScope"), + exclude[IncompatibleSignatureProblem]("sbt.ProjectExtra.inThisBuild"), + exclude[IncompatibleSignatureProblem]("sbt.ProjectExtra.inConfig"), + exclude[IncompatibleSignatureProblem]("sbt.ProjectExtra.inTask"), + exclude[IncompatibleSignatureProblem]("sbt.ProjectExtra.inScope"), + exclude[MissingTypesProblem]("sbt.internal.Load*"), + exclude[IncompatibleSignatureProblem]("sbt.internal.Load*"), + exclude[MissingTypesProblem]("sbt.internal.server.NetworkChannel"), + // IvyConfiguration was replaced by InlineIvyConfiguration in the generic + // signature, this does not break compatibility regardless of what + // cast a compiler might have inserted based on the old signature + // since we're returning the same values as before. + exclude[IncompatibleSignatureProblem]("sbt.Classpaths.mkIvyConfiguration"), + exclude[IncompatibleMethTypeProblem]("sbt.internal.server.Definition*"), + exclude[IncompatibleTemplateDefProblem]("sbt.internal.server.LanguageServerProtocol"), + exclude[DirectMissingMethodProblem]("sbt.Classpaths.warnInsecureProtocol"), + exclude[DirectMissingMethodProblem]("sbt.Classpaths.warnInsecureProtocolInModules"), + exclude[MissingClassProblem]("sbt.internal.ExternalHooks*"), + // This seems to be a mima problem. The older constructor still exists but + // mima seems to incorrectly miss the secondary constructor that provides + // the binary compatible version. + exclude[IncompatibleMethTypeProblem]("sbt.internal.server.NetworkChannel.this"), + exclude[IncompatibleSignatureProblem]("sbt.internal.DeprecatedContinuous.taskDefinitions"), + exclude[MissingClassProblem]("sbt.internal.SettingsGraph*"), + // Tasks include non-Files, but it's ok + exclude[IncompatibleSignatureProblem]("sbt.Defaults.outputConfigPaths"), + // private[sbt] + exclude[DirectMissingMethodProblem]("sbt.Classpaths.trackedExportedProducts"), + exclude[DirectMissingMethodProblem]("sbt.Classpaths.trackedExportedJarProducts"), + exclude[DirectMissingMethodProblem]("sbt.Classpaths.unmanagedDependencies0"), + exclude[DirectMissingMethodProblem]("sbt.Classpaths.internalDependenciesImplTask"), + exclude[DirectMissingMethodProblem]("sbt.Classpaths.internalDependencyJarsImplTask"), + exclude[DirectMissingMethodProblem]("sbt.Classpaths.interDependencies"), + exclude[DirectMissingMethodProblem]("sbt.Classpaths.productsTask"), + exclude[DirectMissingMethodProblem]("sbt.Classpaths.jarProductsTask"), + exclude[DirectMissingMethodProblem]("sbt.StandardMain.cache"), + // internal logging apis, + exclude[IncompatibleSignatureProblem]("sbt.internal.LogManager*"), + exclude[MissingTypesProblem]("sbt.internal.RelayAppender"), + exclude[MissingClassProblem]("sbt.internal.TaskProgress$ProgressThread"), + // internal implementation + exclude[MissingClassProblem]( + "sbt.internal.XMainConfiguration$ModifiedConfiguration$ModifiedAppProvider$ModifiedScalaProvider$" + ), + // internal impl + exclude[IncompatibleSignatureProblem]("sbt.internal.Act.configIdent"), + exclude[IncompatibleSignatureProblem]("sbt.internal.Act.taskAxis"), + // private[sbt] method, used to call the correct sourcePositionMapper + exclude[DirectMissingMethodProblem]("sbt.Defaults.foldMappers"), + ) + ) + .configure( + addSbtIO, + addSbtLmCore, + addSbtLmIvy, + addSbtCompilerInterface, + addSbtZincCompile ) // Strictly for bringing implicits and aliases from subsystems into the top-level sbt namespace through a single package object // technically, we need a dependency on all of mainProj's dependencies, but we don't do that since this is strictly an integration project // with the sole purpose of providing certain identifiers without qualification (with a package object) -lazy val sbtProj = (project in file("sbt")). - dependsOn(mainProj, scriptedSbtProj % "test->test"). - settings( - baseSettings, +lazy val sbtProj = (project in file("sbt")) + .dependsOn(mainProj) + .settings( + testedBaseSettings, name := "sbt", normalizedName := "sbt", - crossScalaVersions := Seq(scala211), + version := { + if (scalaVersion.value == baseScalaVersion) version.value + else version2_13.value + }, + crossScalaVersions := Seq(baseScalaVersion), crossPaths := false, - libraryDependencies ++= Seq(compilerBrdige) + crossTarget := { target.value / scalaVersion.value }, + javaOptions ++= Seq("-Xdebug", "-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005"), + mimaSettings, + mimaBinaryIssueFilters ++= sbtIgnoredProblems, + ) + .settings( + Test / run / connectInput := true, + Test / run / outputStrategy := Some(StdoutOutput), + Test / run / fork := true, + testOptions in Test ++= { + val cp = (Test / fullClasspathAsJars).value.map(_.data).mkString(java.io.File.pathSeparator) + val framework = TestFrameworks.ScalaTest + Tests.Argument(framework, s"-Dsbt.server.classpath=$cp") :: + Tests.Argument(framework, s"-Dsbt.server.version=${version.value}") :: + Tests.Argument(framework, s"-Dsbt.server.scala.version=${scalaVersion.value}") :: Nil + }, + ) + .configure(addSbtIO, addSbtCompilerBridge) + +lazy val serverTestProj = (project in file("server-test")) + .dependsOn(sbtProj % "compile->test", scriptedSbtReduxProj % "compile->test") + .settings( + testedBaseSettings, + crossScalaVersions := Seq(baseScalaVersion), + publish / skip := true, + // make server tests serial + Test / watchTriggers += baseDirectory.value.toGlob / "src" / "server-test" / **, + Test / parallelExecution := false, + Test / run / connectInput := true, + Test / run / outputStrategy := Some(StdoutOutput), + Test / run / fork := true, + Test / sourceGenerators += Def.task { + val rawClasspath = + (Compile / fullClasspathAsJars).value.map(_.data).mkString(java.io.File.pathSeparator) + val cp = + if (scala.util.Properties.isWin) rawClasspath.replaceAllLiterally("\\", "\\\\") + else rawClasspath + val content = { + s"""| + |package testpkg + | + |object TestProperties { + | val classpath = "$cp" + | val version = "${version.value}" + | val scalaVersion = "${scalaVersion.value}" + |} + """.stripMargin + } + val file = (Test / target).value / "generated" / "src" / "test" / "scala" / "testpkg" / "TestProperties.scala" + IO.write(file, content) + file :: Nil + }, ) -def scriptedTask: Def.Initialize[InputTask[Unit]] = Def.inputTask { - val result = scriptedSource(dir => (s: State) => Scripted.scriptedParser(dir)).parsed - publishLocalBinAll.value - // These two projects need to be visible in a repo even if the default - // local repository is hidden, so we publish them to an alternate location and add - // that alternate repo to the running scripted test (in Scripted.scriptedpreScripted). - // (altLocalPublish in interfaceProj).value - // (altLocalPublish in compileInterfaceProj).value - Scripted.doScripted((sbtLaunchJar in bundledLauncherProj).value, (fullClasspath in scriptedSbtProj in Test).value, - (scalaInstance in scriptedSbtProj).value, - scriptedSource.value, scriptedBufferLog.value, result, scriptedPrescripted.value, scriptedLaunchOpts.value) +val isWin = scala.util.Properties.isWin +val buildThinClient = + inputKey[JPath]("generate a java implementation of the thin client") +val thinClientClasspath = + taskKey[Seq[JPath]]("Generate the classpath for thin client (compacted for windows)") +val thinClientNativeImageCommand = taskKey[String]("The native image command") +val thinClientNativeImageOptions = settingKey[Seq[String]]("The native image options") +val thinClientNativeImageClass = settingKey[String]("The class for the native image") +val buildNativeThinClient = taskKey[JPath]("Generate a native executable") +// Use a TaskKey rather than SettingKey for nativeInstallDirectory so it can left unset by default +val nativeInstallDirectory = taskKey[JPath]("The install directory for the native executable") +val installNativeThinClient = inputKey[JPath]("Install the native executable") +val nativeThinClientPath = settingKey[JPath]("The location of the native executable") +lazy val sbtClientProj = (project in file("client")) + .dependsOn(commandProj) + .settings( + commonBaseSettings, + scalaVersion := "2.12.11", // The thin client does not build with 2.12.12 + publish / skip := true, + name := "sbt-client", + mimaPreviousArtifacts := Set.empty, + crossPaths := false, + exportJars := true, + libraryDependencies += jansi, + libraryDependencies += scalatest % "test", + /* + * On windows, the raw classpath is too large to be a command argument to an + * external process so we create symbolic links with short names to get the + * classpath length under the limit. + */ + thinClientClasspath := { + val original = (Compile / fullClasspathAsJars).value.map(_.data) + val outputDir = target.value / "thinclientcp" + IO.createDirectory(outputDir) + Files.walk(outputDir.toPath).forEach { + case f if f.getFileName.toString.endsWith(".jar") => Files.deleteIfExists(f) + case _ => + } + original.zipWithIndex.map { + case (f, i) => Files.createSymbolicLink(outputDir.toPath / s"$i.jar", f.toPath) + } + }, + thinClientNativeImageCommand := System.getProperty("sbt.native-image", "native-image").toString, + buildNativeThinClient / name := s"sbtn${if (isWin) ".exe" else ""}", + nativeThinClientPath := target.value.toPath / "bin" / (buildNativeThinClient / name).value, + thinClientNativeImageClass := "sbt.client.Client", + buildNativeThinClient := { + val hasChanges = thinClientClasspath.outputFileChanges.hasChanges + val cpString = + thinClientClasspath.value.map(_.getFileName).mkString(java.io.File.pathSeparator) + val prefix = Seq(thinClientNativeImageCommand.value, "-cp", cpString) + val full = prefix ++ thinClientNativeImageOptions.value :+ thinClientNativeImageClass.value + val dir = target.value + if (hasChanges || !Files.exists(nativeThinClientPath.value)) { + val pb = new java.lang.ProcessBuilder(full: _*) + pb.directory(dir / "thinclientcp") + val proc = pb.start() + val thread = new Thread { + setDaemon(true) + val is = proc.getInputStream + val es = proc.getErrorStream + + override def run(): Unit = { + Thread.sleep(100) + while (proc.isAlive) { + if (is.available > 0 || es.available > 0) { + while (is.available > 0) System.out.print(is.read.toChar) + while (es.available > 0) System.err.print(es.read.toChar) + } + if (proc.isAlive) Thread.sleep(10) + } + } + } + thread.start() + proc.waitFor(5, java.util.concurrent.TimeUnit.MINUTES) + assert(proc.exitValue == 0, s"Exit value ${proc.exitValue} was nonzero") + } + nativeThinClientPath.value + }, + thinClientNativeImageOptions := Seq( + "--no-fallback", + s"--initialize-at-run-time=sbt.client", + "--verbose", + "-H:IncludeResourceBundles=jline.console.completer.CandidateListCompletionHandler", + "-H:+ReportExceptionStackTraces", + "-H:-ParseRuntimeOptions", + s"-H:Name=${target.value / "bin" / "sbtn"}", + ), + buildThinClient := { + val isFish = Def.spaceDelimited("").parsed.headOption.fold(false)(_ == "--fish") + val ext = if (isWin) ".bat" else if (isFish) ".fish" else ".sh" + val output = target.value.toPath / "bin" / s"${if (isFish) "fish-" else ""}client$ext" + java.nio.file.Files.createDirectories(output.getParent) + val cp = (Compile / fullClasspathAsJars).value.map(_.data) + val args = + if (isWin) "%*" else if (isFish) s"$$argv" else s"$$*" + java.nio.file.Files.write( + output, + s""" + |${if (isWin) "@echo off" else s"#!/usr/bin/env ${if (isFish) "fish" else "sh"}"} + | + |java -cp ${cp.mkString(java.io.File.pathSeparator)} sbt.client.Client --jna $args + """.stripMargin.linesIterator.toSeq.tail.mkString("\n").getBytes + ) + output.toFile.setExecutable(true) + output + }, + ) + +/* +lazy val sbtBig = (project in file(".big")) + .dependsOn(sbtProj) + .settings( + name := "sbt-big", + normalizedName := "sbt-big", + crossPaths := false, + assemblyShadeRules.in(assembly) := { + val packagesToBeShaded = Seq( + "fastparse", + "jawn", + "scalapb", + ) + packagesToBeShaded.map( prefix => { + ShadeRule.rename(s"$prefix.**" -> s"sbt.internal.$prefix.@1").inAll + }) + }, + assemblyMergeStrategy in assembly := { + case "LICENSE" | "NOTICE" => MergeStrategy.first + case x => (assemblyMergeStrategy in assembly).value(x) + }, + artifact.in(Compile, packageBin) := artifact.in(Compile, assembly).value, + assemblyOption.in(assembly) ~= { _.copy(includeScala = false) }, + addArtifact(artifact.in(Compile, packageBin), assembly), + pomPostProcess := { node => + new RuleTransformer(new RewriteRule { + override def transform(node: XmlNode): XmlNodeSeq = node match { + case e: Elem if node.label == "dependency" => + Comment( + "the dependency that was here has been absorbed via sbt-assembly" + ) + case _ => node + } + }).transform(node).head + }, + ) + */ + +// util projects used by Zinc and Lm +lazy val lowerUtils = (project in (file("internal") / "lower")) + .aggregate(lowerUtilProjects.map(p => LocalProject(p.id)): _*) + .settings( + publish / skip := true, + crossScalaVersions := Nil, + ) + +lazy val upperModules = (project in (file("internal") / "upper")) + .aggregate( + ((allProjects diff lowerUtilProjects) + diff Seq(bundledLauncherProj)).map(p => LocalProject(p.id)): _* + ) + .settings( + publish / skip := true, + crossScalaVersions := Nil, + ) + +lazy val sbtIgnoredProblems = { + Vector( + exclude[IncompatibleSignatureProblem]("sbt.package.some"), + exclude[IncompatibleSignatureProblem]("sbt.package.inThisBuild"), + exclude[IncompatibleSignatureProblem]("sbt.package.inConfig"), + exclude[IncompatibleSignatureProblem]("sbt.package.inTask"), + exclude[IncompatibleSignatureProblem]("sbt.package.inScope"), + exclude[MissingClassProblem]("buildinfo.BuildInfo"), + exclude[MissingClassProblem]("buildinfo.BuildInfo$"), + // Added more items to Import trait. + exclude[ReversedMissingMethodProblem]("sbt.Import.sbt$Import$_setter_$WatchSource_="), + exclude[ReversedMissingMethodProblem]("sbt.Import.WatchSource"), + exclude[ReversedMissingMethodProblem]("sbt.Import.AnyPath"), + exclude[ReversedMissingMethodProblem]("sbt.Import.sbt$Import$_setter_$**_="), + exclude[ReversedMissingMethodProblem]("sbt.Import.sbt$Import$_setter_$*_="), + exclude[ReversedMissingMethodProblem]("sbt.Import.sbt$Import$_setter_$ChangedFiles_="), + exclude[ReversedMissingMethodProblem]("sbt.Import.sbt$Import$_setter_$AnyPath_="), + exclude[ReversedMissingMethodProblem]("sbt.Import.sbt$Import$_setter_$Glob_="), + exclude[ReversedMissingMethodProblem]("sbt.Import.sbt$Import$_setter_$RecursiveGlob_="), + exclude[ReversedMissingMethodProblem]("sbt.Import.sbt$Import$_setter_$RelativeGlob_="), + exclude[ReversedMissingMethodProblem]("sbt.Import.*"), + exclude[ReversedMissingMethodProblem]("sbt.Import.**"), + exclude[ReversedMissingMethodProblem]("sbt.Import.ChangedFiles"), + exclude[ReversedMissingMethodProblem]("sbt.Import.RecursiveGlob"), + exclude[ReversedMissingMethodProblem]("sbt.Import.Glob"), + exclude[ReversedMissingMethodProblem]("sbt.Import.RelativeGlob"), + // Dropped in favour of kind-projector's polymorphic lambda literals + exclude[DirectMissingMethodProblem]("sbt.Import.Param"), + exclude[DirectMissingMethodProblem]("sbt.package.Param"), + exclude[ReversedMissingMethodProblem]("sbt.Import.SemanticSelector"), + exclude[ReversedMissingMethodProblem]("sbt.Import.sbt$Import$_setter_$SemanticSelector_="), + // Dropped in favour of plain scala.Function, and its compose method + exclude[DirectMissingMethodProblem]("sbt.package.toFn1"), + ) } -def scriptedUnpublishedTask: Def.Initialize[InputTask[Unit]] = Def.inputTask { - val result = scriptedSource(dir => (s: State) => Scripted.scriptedParser(dir)).parsed - Scripted.doScripted((sbtLaunchJar in bundledLauncherProj).value, (fullClasspath in scriptedSbtProj in Test).value, - (scalaInstance in scriptedSbtProj).value, - scriptedSource.value, scriptedBufferLog.value, result, scriptedPrescripted.value, scriptedLaunchOpts.value) +def runNpm(command: String, base: File, log: sbt.internal.util.ManagedLogger) = { + import scala.sys.process._ + try { + val exitCode = Process(s"npm $command", Option(base)) ! log + if (exitCode != 0) throw new Exception("Process returned exit code: " + exitCode) + } catch { + case e: java.io.IOException => log.warn("failed to run npm " + e.getMessage) + } +} + +lazy val vscodePlugin = (project in file("vscode-sbt-scala")) + .settings( + bspEnabled := false, + crossPaths := false, + crossScalaVersions := Seq(baseScalaVersion), + skip in publish := true, + compile in Compile := { + val _ = update.value + runNpm("run compile", baseDirectory.value, streams.value.log) + sbt.internal.inc.Analysis.empty + }, + update := { + val old = update.value + val t = target.value / "updated" + val base = baseDirectory.value + val log = streams.value.log + if (t.exists) () + else { + runNpm("install", base, log) + IO.touch(t) + } + old + }, + cleanFiles ++= { + val base = baseDirectory.value + Vector( + target.value / "updated", + base / "node_modules", + base / "client" / "node_modules", + base / "client" / "server", + base / "client" / "out", + base / "server" / "node_modules" + ) filter { _.exists } + } + ) + +def scriptedTask(launch: Boolean): Def.Initialize[InputTask[Unit]] = Def.inputTask { + val _ = publishLocalBinAll.value + val launchJar = s"-Dsbt.launch.jar=${(bundledLauncherProj / Compile / packageBin).value}" + Scripted.doScripted( + (scalaInstance in scriptedSbtReduxProj).value, + scriptedSource.value, + scriptedBufferLog.value, + Def.setting(Scripted.scriptedParser(scriptedSource.value)).parsed, + scriptedPrescripted.value, + scriptedLaunchOpts.value ++ (if (launch) Some(launchJar) else None), + scalaVersion.value, + version.value, + (scriptedSbtReduxProj / Test / fullClasspathAsJars).value + .map(_.data) + .filterNot(_.getName.contains("scala-compiler")), + streams.value.log + ) } lazy val publishLauncher = TaskKey[Unit]("publish-launcher") -lazy val myProvided = config("provided") intransitive +def allProjects = + Seq( + logicProj, + completeProj, + testingProj, + testAgentProj, + taskProj, + stdTaskProj, + runProj, + scriptedSbtReduxProj, + scriptedSbtOldProj, + scriptedPluginProj, + dependencyTreeProj, + protocolProj, + actionsProj, + commandProj, + mainSettingsProj, + zincLmIntegrationProj, + mainProj, + sbtProj, + bundledLauncherProj, + sbtClientProj, + ) ++ lowerUtilProjects -def allProjects = Seq( - testingProj, testAgentProj, taskProj, stdTaskProj, runProj, - scriptedSbtProj, scriptedPluginProj, - actionsProj, commandProj, mainSettingsProj, mainProj, sbtProj, bundledLauncherProj) +// These need to be cross published to 2.12 and 2.13 for Zinc +lazy val lowerUtilProjects = + Seq( + collectionProj, + coreMacrosProj, + utilCache, + utilControl, + utilInterface, + utilLogging, + utilPosition, + utilRelation, + utilScripted, + utilTracking + ) -def projectsWithMyProvided = allProjects.map(p => p.copy(configurations = (p.configurations.filter(_ != Provided)) :+ myProvided)) -lazy val nonRoots = projectsWithMyProvided.map(p => LocalProject(p.id)) +lazy val nonRoots = allProjects.map(p => LocalProject(p.id)) -def rootSettings = fullDocSettings ++ - Util.publishPomSettings ++ otherRootSettings ++ Formatting.sbtFilesSettings ++ - Transform.conscriptSettings(bundledLauncherProj) -def otherRootSettings = Seq( - scripted <<= scriptedTask, - scriptedUnpublished <<= scriptedUnpublishedTask, - scriptedSource := (sourceDirectory in sbtProj).value / "sbt-test", - // scriptedPrescripted := { addSbtAlternateResolver _ }, - scriptedLaunchOpts := List("-XX:MaxPermSize=256M", "-Xmx1G"), - publishAll := { val _ = (publishLocal).all(ScopeFilter(inAnyProject)).value }, - publishLocalBinAll := { val _ = (publishLocalBin).all(ScopeFilter(inAnyProject)).value }, - aggregate in bintrayRelease := false -) ++ inConfig(Scripted.RepoOverrideTest)(Seq( - scriptedPrescripted := { _ => () }, - scriptedLaunchOpts := { - List("-XX:MaxPermSize=256M", "-Xmx1G", "-Dsbt.override.build.repos=true", - s"""-Dsbt.repository.config=${ scriptedSource.value / "repo.config" }""") - }, - scripted <<= scriptedTask, - scriptedUnpublished <<= scriptedUnpublishedTask, - scriptedSource := (sourceDirectory in sbtProj).value / "repo-override-test" -)) +ThisBuild / scriptedBufferLog := true +ThisBuild / scriptedPrescripted := { _ => +} -// def addSbtAlternateResolver(scriptedRoot: File) = { -// val resolver = scriptedRoot / "project" / "AddResolverPlugin.scala" -// if (!resolver.exists) { -// IO.write(resolver, s"""import sbt._ -// |import Keys._ -// | -// |object AddResolverPlugin extends AutoPlugin { -// | override def requires = sbt.plugins.JvmPlugin -// | override def trigger = allRequirements -// | -// | override lazy val projectSettings = Seq(resolvers += alternativeLocalResolver) -// | lazy val alternativeLocalResolver = Resolver.file("$altLocalRepoName", file("$altLocalRepoPath"))(Resolver.ivyStylePatterns) -// |} -// |""".stripMargin) -// } -// } +def otherRootSettings = + Seq( + scripted := scriptedTask(false).evaluated, + scriptedUnpublished := scriptedTask(false).evaluated, + scriptedSource := (sourceDirectory in sbtProj).value / "sbt-test", + watchTriggers in scripted += scriptedSource.value.toGlob / **, + watchTriggers in scriptedUnpublished := (watchTriggers in scripted).value, + scriptedLaunchOpts := List("-Xmx1500M", "-Xms512M", "-server") ::: + (sys.props.get("sbt.ivy.home") match { + case Some(home) => List(s"-Dsbt.ivy.home=$home") + case _ => Nil + }), + publishLocalBinAll := { + val _ = (Compile / publishLocalBin).all(scriptedProjects).value + }, + ) ++ inConfig(Scripted.RepoOverrideTest)( + Seq( + scriptedLaunchOpts := List( + "-Xmx1500M", + "-Xms512M", + "-server", + "-Dsbt.override.build.repos=true", + s"""-Dsbt.repository.config=${scriptedSource.value / "repo.config"}""" + ) ::: + (sys.props.get("sbt.ivy.home") match { + case Some(home) => List(s"-Dsbt.ivy.home=$home") + case _ => Nil + }), + scripted := scriptedTask(true).evaluated, + scriptedUnpublished := scriptedTask(true).evaluated, + scriptedSource := (sourceDirectory in sbtProj).value / "repo-override-test" + ) + ) lazy val docProjects: ScopeFilter = ScopeFilter( - inAnyProject -- inProjects(sbtRoot, sbtProj, scriptedSbtProj, scriptedPluginProj), + inAnyProject -- inProjects( + sbtRoot, + sbtProj, + scriptedSbtReduxProj, + scriptedSbtOldProj, + scriptedPluginProj, + upperModules, + lowerUtils, + ), inConfigurations(Compile) ) -def fullDocSettings = Util.baseScalacOptions ++ Docs.settings ++ Sxr.settings ++ Seq( - scalacOptions += "-Ymacro-no-expand", // for both sxr and doc - sources in sxr := { - val allSources = (sources ?? Nil).all(docProjects).value - allSources.flatten.distinct - }, //sxr - sources in (Compile, doc) := (sources in sxr).value, // doc - Sxr.sourceDirectories := { - val allSourceDirectories = (sourceDirectories ?? Nil).all(docProjects).value - allSourceDirectories.flatten - }, - fullClasspath in sxr := (externalDependencyClasspath in Compile in sbtProj).value, - dependencyClasspath in (Compile, doc) := (fullClasspath in sxr).value -) - -lazy val safeUnitTests = taskKey[Unit]("Known working tests (for both 2.10 and 2.11)") -lazy val safeProjects: ScopeFilter = ScopeFilter( - inProjects(mainSettingsProj, mainProj, - actionsProj, runProj, stdTaskProj), - inConfigurations(Test) -) -lazy val otherUnitTests = taskKey[Unit]("Unit test other projects") -lazy val otherProjects: ScopeFilter = ScopeFilter( - inProjects( - testingProj, testAgentProj, taskProj, - scriptedSbtProj, scriptedPluginProj, - commandProj, mainSettingsProj, mainProj, - sbtProj), - inConfigurations(Test) -) +lazy val javafmtOnCompile = taskKey[Unit]("Formats java sources before compile") +lazy val scriptedProjects = ScopeFilter(inAnyProject -- inProjects(vscodePlugin)) def customCommands: Seq[Setting[_]] = Seq( - commands += Command.command("setupBuildScala211") { state => - s"""set scalaVersion in ThisBuild := "$scala211" """ :: + commands += Command.command("setupBuildScala212") { state => + s"""set scalaVersion in ThisBuild := "$scala212" """ :: state }, - safeUnitTests := { - test.all(safeProjects).value - }, - otherUnitTests := { - test.all(otherProjects).value + commands += Command.command("whitesourceOnPush") { state => + sys.env.get("TRAVIS_EVENT_TYPE") match { + case Some("push") => + "whitesourceCheckPolicies" :: + "whitesourceUpdate" :: + state + case _ => state + } }, commands += Command.command("release-sbt-local") { state => "clean" :: - "so compile" :: - "so publishLocal" :: - "reload" :: - state + "so compile" :: + "so publishLocal" :: + "reload" :: + state }, - /** There are several complications with sbt's build. - * First is the fact that interface project is a Java-only project - * that uses source generator from datatype subproject in Scala 2.10.6. - * - * Second is the fact that all subprojects are released with crossPaths - * turned off for the sbt's Scala version 2.10.6, but some of them are also - * cross published against 2.11.1 with crossPaths turned on. - * - * `so compile` handles 2.10.x/2.11.x cross building. - */ - commands += Command.command("release-sbt") { state => + commands += Command.command("publishLocalAllModule") { state => + val extracted = Project.extract(state) + import extracted._ + val sv = get(scalaVersion) + val projs = structure.allProjectRefs + val ioOpt = projs find { case ProjectRef(_, id) => id == "ioRoot"; case _ => false } + val utilOpt = projs find { case ProjectRef(_, id) => id == "utilRoot"; case _ => false } + val lmOpt = projs find { case ProjectRef(_, id) => id == "lmRoot"; case _ => false } + val zincOpt = projs find { case ProjectRef(_, id) => id == "zincRoot"; case _ => false } + (ioOpt map { case ProjectRef(build, _) => "{" + build.toString + "}/publishLocal" }).toList ::: + (utilOpt map { case ProjectRef(build, _) => "{" + build.toString + "}/publishLocal" }).toList ::: + (lmOpt map { case ProjectRef(build, _) => "{" + build.toString + "}/publishLocal" }).toList ::: + (zincOpt map { + case ProjectRef(build, _) => + val zincSv = get(scalaVersion in ProjectRef(build, "zinc")) + val csv = get(crossScalaVersions in ProjectRef(build, "compilerBridge")).toList + (csv flatMap { bridgeSv => + s"++$bridgeSv" :: ("{" + build.toString + "}compilerBridge/publishLocal") :: Nil + }) ::: + List(s"++$zincSv", "{" + build.toString + "}/publishLocal") + }).getOrElse(Nil) ::: + List(s"++$sv", "publishLocal") ::: + state + }, + commands += Command.command("releaseLowerUtils") { state => // TODO - Any sort of validation "clean" :: - "conscript-configs" :: - "so compile" :: - "so publishSigned" :: + "+lowerUtils/compile" :: + "+lowerUtils/publishSigned" :: + s"++$scala212" :: + state + }, + commands += Command.command("release") { state => + // TODO - Any sort of validation + "clean" :: + "conscriptConfigs" :: + "upperModules/compile" :: + "upperModules/publishSigned" :: + "bundledLauncherProj/publishSigned" :: "bundledLauncherProj/publishLauncher" :: state }, - // stamp-version doesn't work with ++ or "so". - commands += Command.command("release-nightly") { state => - "stamp-version" :: - "clean" :: - "compile" :: - "publish" :: - "bintrayRelease" :: - state - } ) + +ThisBuild / pomIncludeRepository := { _ => + false +} +ThisBuild / publishTo := { + val nexus = "https://oss.sonatype.org/" + if (isSnapshot.value) Some("snapshots" at nexus + "content/repositories/snapshots") + else Some("releases" at nexus + "service/local/staging/deploy/maven2") +} +ThisBuild / publishMavenStyle := true +ThisBuild / whitesourceProduct := "Lightbend Reactive Platform" +ThisBuild / whitesourceAggregateProjectName := { + // note this can get detached on tag build etc + val b = sys.process.Process("git rev-parse --abbrev-ref HEAD").!!.trim + val Stable = """1\.([0-9]+)\.x""".r + b match { + case Stable(y) => "sbt-1." + y.toString + "-stable" + case _ => "sbt-master" + } +} +ThisBuild / whitesourceAggregateProjectToken := { + (ThisBuild / whitesourceAggregateProjectName).value match { + case "sbt-master" => "e7a1e55518c0489a98e9c7430c8b2ccd53d9f97c12ed46148b592ebe4c8bf128" + case "sbt-1.3-stable" => "7e38cbb4d2fc4599835cd5d2cfb41b150597a4147b15424bb65841664ab2ec0d" + case "sbt-1.2-stable" => "54f2313767aa47198971e65595670ee16e1ad0000d20458588e72d3ac2c34763" + case _ => "" // it's ok to fail here + } +} +ThisBuild / whitesourceIgnoredScopes ++= Seq("plugin", "scalafmt", "sxr") +ThisBuild / whitesourceFailOnError := sys.env.contains("WHITESOURCE_PASSWORD") // fail if pwd is present +ThisBuild / whitesourceForceCheckAllDependencies := true diff --git a/client/completions/_sbtn b/client/completions/_sbtn new file mode 100755 index 000000000..c388ee092 --- /dev/null +++ b/client/completions/_sbtn @@ -0,0 +1,5 @@ +#compdef sbtn + +COMPLETE="--completions=${words[@]}" +COMPLETIONS=($(sbtn --no-tab ${COMPLETE})) +_alternative 'arguments:custom arg:($COMPLETIONS)' diff --git a/client/completions/sbtn.bash b/client/completions/sbtn.bash new file mode 100644 index 000000000..6fcb9fff1 --- /dev/null +++ b/client/completions/sbtn.bash @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +_do_sbtn_completions() { + COMPREPLY=($(sbtn "--completions=${COMP_LINE}")) +} + +complete -F _do_sbtn_completions sbtn diff --git a/client/completions/sbtn.fish b/client/completions/sbtn.fish new file mode 100755 index 000000000..e5a2d7a02 --- /dev/null +++ b/client/completions/sbtn.fish @@ -0,0 +1,4 @@ +function __sbtcomp + sbtn --completions="$argv" +end +complete --command sbtn -f --arguments '(__sbtcomp (commandline -cp))' diff --git a/client/completions/sbtn.ps1 b/client/completions/sbtn.ps1 new file mode 100644 index 000000000..0e8b7d851 --- /dev/null +++ b/client/completions/sbtn.ps1 @@ -0,0 +1,10 @@ +$scriptblock = { + param($commandName, $line, $position) + $len = $line.ToString().length + $spaces = " " * ($position - $len) + $arg="--completions=$line$spaces" + & 'sbtn.exe' @('--no-tab', '--no-stderr', $arg) +} +Set-Alias -Name sbtn -Value sbtn.exe +Register-ArgumentCompleter -CommandName sbtn.exe -ScriptBlock $scriptBlock +Register-ArgumentCompleter -CommandName sbtn -ScriptBlock $scriptBlock diff --git a/client/src/main/java/sbt/client/Client.java b/client/src/main/java/sbt/client/Client.java new file mode 100644 index 000000000..bf9f120bb --- /dev/null +++ b/client/src/main/java/sbt/client/Client.java @@ -0,0 +1,26 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.client; + +import sbt.internal.client.NetworkClient; +import java.nio.file.Paths; +import org.fusesource.jansi.AnsiConsole; + +public class Client { + public static void main(final String[] args) { + boolean isWin = System.getProperty("os.name").toLowerCase().startsWith("win"); + try { + if (isWin) AnsiConsole.systemInstall(); + NetworkClient.main(args); + } catch (final Throwable t) { + t.printStackTrace(); + } finally { + if (isWin) AnsiConsole.systemUninstall(); + } + } +} diff --git a/client/src/main/resources/META-INF/native-image/reflect-config.json b/client/src/main/resources/META-INF/native-image/reflect-config.json new file mode 100644 index 000000000..a2b39abca --- /dev/null +++ b/client/src/main/resources/META-INF/native-image/reflect-config.json @@ -0,0 +1,18 @@ +[ + { + "name":"java.lang.ProcessBuilder", + "methods":[{"name":"redirectInput","parameterTypes":["java.lang.ProcessBuilder$Redirect"] }] + }, + { + "name":"java.lang.ProcessBuilder$Redirect", + "fields":[{"name":"INHERIT"}] + }, + { + "name":"java.lang.System", + "methods":[{"name":"console","parameterTypes":[] }] + }, + { + "name":"jline.UnixTerminal", + "methods":[{"name":"","parameterTypes":[] }] + } +] diff --git a/client/src/main/resources/META-INF/native-image/resource-config.json b/client/src/main/resources/META-INF/native-image/resource-config.json new file mode 100644 index 000000000..0bc65dde4 --- /dev/null +++ b/client/src/main/resources/META-INF/native-image/resource-config.json @@ -0,0 +1,22 @@ +{ + "resources":[ + {"pattern":"jline/console/completer/CandidateListCompletionHandler.properties"}, + {"pattern":"org/jline/utils/ansi.caps"}, + {"pattern":"org/jline/utils/capabilities.txt"}, + {"pattern":"org/jline/utils/colors.txt"}, + {"pattern":"org/jline/utils/dumb-color.caps"}, + {"pattern":"org/jline/utils/xterm.caps"}, + {"pattern":"org/jline/utils/xterm-256color.caps"}, + {"pattern":"org/jline/utils/windows-256color.caps"}, + {"pattern":"org/jline/utils/screen-256color.caps"}, + {"pattern":"org/jline/utils/windows.caps"}, + {"pattern":"org/jline/utils/windows-conemu.caps"}, + {"pattern":"org/jline/utils/dumb.caps"}, + {"pattern":"org/jline/utils/windows-vtp.caps"}, + {"pattern":"org/jline/utils/screen.caps"}, + {"pattern":"library.properties"}, + {"pattern":"darwin/x86_64/libsbtipcsocket.dylib"}, + {"pattern":"linux/x86_64/libsbtipcsocket.so"}, + {"pattern":"win32/x86_64/sbtipcsocket.dll"} + ] +} diff --git a/core-macros/src/main/scala/sbt/internal/util/appmacro/ContextUtil.scala b/core-macros/src/main/scala/sbt/internal/util/appmacro/ContextUtil.scala new file mode 100644 index 000000000..e87b13a21 --- /dev/null +++ b/core-macros/src/main/scala/sbt/internal/util/appmacro/ContextUtil.scala @@ -0,0 +1,322 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util +package appmacro + +import scala.reflect._ +import macros._ +import ContextUtil.{ DynamicDependencyError, DynamicReferenceError } + +object ContextUtil { + final val DynamicDependencyError = "Illegal dynamic dependency" + final val DynamicReferenceError = "Illegal dynamic reference" + + /** + * Constructs an object with utility methods for operating in the provided macro context `c`. + * Callers should explicitly specify the type parameter as `c.type` in order to preserve the path dependent types. + */ + def apply[C <: blackbox.Context with Singleton](c: C): ContextUtil[C] = new ContextUtil(c: C) + + /** + * Helper for implementing a no-argument macro that is introduced via an implicit. + * This method removes the implicit conversion and evaluates the function `f` on the target of the conversion. + * + * Given `myImplicitConversion(someValue).extensionMethod`, where `extensionMethod` is a macro that uses this + * method, the result of this method is `f()`. + */ + def selectMacroImpl[T: c.WeakTypeTag]( + c: blackbox.Context + )(f: (c.Expr[Any], c.Position) => c.Expr[T]): c.Expr[T] = { + import c.universe._ + + c.macroApplication match { + case s @ Select(Apply(_, t :: Nil), _) => f(c.Expr[Any](t), s.pos) + case a @ Apply(_, t :: Nil) => f(c.Expr[Any](t), a.pos) + case x => unexpectedTree(x) + } + } + + def unexpectedTree[C <: blackbox.Context](tree: C#Tree): Nothing = + sys.error("Unexpected macro application tree (" + tree.getClass + "): " + tree) +} + +/** + * Utility methods for macros. Several methods assume that the context's universe is a full compiler + * (`scala.tools.nsc.Global`). + * This is not thread safe due to the underlying Context and related data structures not being thread safe. + * Use `ContextUtil[c.type](c)` to construct. + */ +final class ContextUtil[C <: blackbox.Context](val ctx: C) { + import ctx.universe.{ Apply => ApplyTree, _ } + import internal.decorators._ + + val powerContext = ctx.asInstanceOf[reflect.macros.runtime.Context] + val global: powerContext.universe.type = powerContext.universe + def callsiteTyper: global.analyzer.Typer = powerContext.callsiteTyper + val initialOwner: Symbol = callsiteTyper.context.owner.asInstanceOf[ctx.universe.Symbol] + + lazy val alistType = ctx.typeOf[AList[KList]] + lazy val alist: Symbol = alistType.typeSymbol.companion + lazy val alistTC: Type = alistType.typeConstructor + + /** Modifiers for a local val.*/ + lazy val localModifiers = Modifiers(NoFlags) + + def getPos(sym: Symbol) = if (sym eq null) NoPosition else sym.pos + + /** + * Constructs a unique term name with the given prefix within this Context. + * (The current implementation uses Context.freshName, which increments + */ + def freshTermName(prefix: String) = TermName(ctx.freshName("$" + prefix)) + + /** + * Constructs a new, synthetic, local ValDef Type `tpe`, a unique name, + * Position `pos`, an empty implementation (no rhs), and owned by `owner`. + */ + def freshValDef(tpe: Type, pos: Position, owner: Symbol): ValDef = { + val SYNTHETIC = (1 << 21).toLong.asInstanceOf[FlagSet] + val sym = owner.newTermSymbol(freshTermName("q"), pos, SYNTHETIC) + setInfo(sym, tpe) + val vd = internal.valDef(sym, EmptyTree) + vd.setPos(pos) + vd + } + + lazy val parameterModifiers = Modifiers(Flag.PARAM) + + /** + * Collects all definitions in the tree for use in checkReferences. + * This excludes definitions in wrapped expressions because checkReferences won't allow nested dereferencing anyway. + */ + def collectDefs( + tree: Tree, + isWrapper: (String, Type, Tree) => Boolean + ): collection.Set[Symbol] = { + val defs = new collection.mutable.HashSet[Symbol] + // adds the symbols for all non-Ident subtrees to `defs`. + val process = new Traverser { + override def traverse(t: Tree) = t match { + case _: Ident => () + case ApplyTree(TypeApply(Select(_, nme), tpe :: Nil), qual :: Nil) + if isWrapper(nme.decodedName.toString, tpe.tpe, qual) => + () + case tree => + if (tree.symbol ne null) { + defs += tree.symbol + () + } + super.traverse(tree) + } + } + process.traverse(tree) + defs + } + + /** + * A reference is illegal if it is to an M instance defined within the scope of the macro call. + * As an approximation, disallow referenced to any local definitions `defs`. + */ + def illegalReference(defs: collection.Set[Symbol], sym: Symbol, mType: Type): Boolean = + sym != null && sym != NoSymbol && defs.contains(sym) && { + sym match { + case m: MethodSymbol => m.returnType.erasure <:< mType + case _ => sym.typeSignature <:< mType + } + } + + /** + * A reference is illegal if it is to an M instance defined within the scope of the macro call. + * As an approximation, disallow referenced to any local definitions `defs`. + */ + def illegalReference(defs: collection.Set[Symbol], sym: Symbol): Boolean = + illegalReference(defs, sym, weakTypeOf[Any]) + + type PropertyChecker = (String, Type, Tree) => Boolean + + /** + * A function that checks the provided tree for illegal references to M instances defined in the + * expression passed to the macro and for illegal dereferencing of M instances. + */ + def checkReferences( + defs: collection.Set[Symbol], + isWrapper: PropertyChecker, + mType: Type + ): Tree => Unit = { + case s @ ApplyTree(TypeApply(Select(_, nme), tpe :: Nil), qual :: Nil) => + if (isWrapper(nme.decodedName.toString, tpe.tpe, qual)) { + ctx.error(s.pos, DynamicDependencyError) + } + case id @ Ident(name) if illegalReference(defs, id.symbol, mType) => + ctx.error(id.pos, DynamicReferenceError + ": " + name) + case _ => () + } + + @deprecated("Use that variant that specifies the M instance types to exclude", since = "1.3.0") + /** + * A function that checks the provided tree for illegal references to M instances defined in the + * expression passed to the macro and for illegal dereferencing of M instances. + */ + def checkReferences(defs: collection.Set[Symbol], isWrapper: PropertyChecker): Tree => Unit = + checkReferences(defs, isWrapper, weakTypeOf[Any]) + + /** Constructs a ValDef with a parameter modifier, a unique name, with the provided Type and with an empty rhs. */ + def freshMethodParameter(tpe: Type): ValDef = + ValDef(parameterModifiers, freshTermName("p"), TypeTree(tpe), EmptyTree) + + /** Constructs a ValDef with local modifiers and a unique name. */ + def localValDef(tpt: Tree, rhs: Tree): ValDef = + ValDef(localModifiers, freshTermName("q"), tpt, rhs) + + /** Constructs a tuple value of the right TupleN type from the provided inputs.*/ + def mkTuple(args: List[Tree]): Tree = + global.gen.mkTuple(args.asInstanceOf[List[global.Tree]]).asInstanceOf[ctx.universe.Tree] + + def setSymbol[_Tree](t: _Tree, sym: Symbol): Unit = { + t.asInstanceOf[global.Tree].setSymbol(sym.asInstanceOf[global.Symbol]) + () + } + def setInfo(sym: Symbol, tpe: Type): Unit = { + sym.asInstanceOf[global.Symbol].setInfo(tpe.asInstanceOf[global.Type]) + () + } + + /** Creates a new, synthetic type variable with the specified `owner`. */ + def newTypeVariable(owner: Symbol, prefix: String = "T0"): TypeSymbol = + owner + .asInstanceOf[global.Symbol] + .newSyntheticTypeParam(prefix, 0L) + .asInstanceOf[ctx.universe.TypeSymbol] + + /** The type representing the type constructor `[X] X` */ + lazy val idTC: Type = { + val tvar = newTypeVariable(NoSymbol) + internal.polyType(tvar :: Nil, refVar(tvar)) + } + + /** A Type that references the given type variable. */ + def refVar(variable: TypeSymbol): Type = variable.toTypeConstructor + + /** Constructs a new, synthetic type variable that is a type constructor. For example, in type Y[L[x]], L is such a type variable. */ + def newTCVariable(owner: Symbol): TypeSymbol = { + val tc = newTypeVariable(owner) + val arg = newTypeVariable(tc, "x"); + tc.setInfo(internal.polyType(arg :: Nil, emptyTypeBounds)) + tc + } + + /** >: Nothing <: Any */ + def emptyTypeBounds: TypeBounds = + internal.typeBounds(definitions.NothingClass.toType, definitions.AnyClass.toType) + + /** Creates a new anonymous function symbol with Position `pos`. */ + def functionSymbol(pos: Position): Symbol = + callsiteTyper.context.owner + .newAnonymousFunctionValue(pos.asInstanceOf[global.Position]) + .asInstanceOf[ctx.universe.Symbol] + + def functionType(args: List[Type], result: Type): Type = { + val tpe = global.definitions + .functionType(args.asInstanceOf[List[global.Type]], result.asInstanceOf[global.Type]) + tpe.asInstanceOf[Type] + } + + /** Create a Tree that references the `val` represented by `vd`, copying attributes from `replaced`. */ + def refVal(replaced: Tree, vd: ValDef): Tree = + treeCopy.Ident(replaced, vd.name).setSymbol(vd.symbol) + + /** Creates a Function tree using `functionSym` as the Symbol and changing `initialOwner` to `functionSym` in `body`.*/ + def createFunction(params: List[ValDef], body: Tree, functionSym: Symbol): Tree = { + changeOwner(body, initialOwner, functionSym) + val f = Function(params, body) + setSymbol(f, functionSym) + f + } + + def changeOwner(tree: Tree, prev: Symbol, next: Symbol): Unit = + new ChangeOwnerAndModuleClassTraverser( + prev.asInstanceOf[global.Symbol], + next.asInstanceOf[global.Symbol] + ).traverse(tree.asInstanceOf[global.Tree]) + + // Workaround copied from scala/async:can be removed once https://github.com/scala/scala/pull/3179 is merged. + private[this] class ChangeOwnerAndModuleClassTraverser( + oldowner: global.Symbol, + newowner: global.Symbol + ) extends global.ChangeOwnerTraverser(oldowner, newowner) { + override def traverse(tree: global.Tree): Unit = { + tree match { + case _: global.DefTree => change(tree.symbol.moduleClass) + case _ => + } + super.traverse(tree) + } + } + + /** Returns the Symbol that references the statically accessible singleton `i`. */ + def singleton[T <: AnyRef with Singleton](i: T)(implicit it: ctx.TypeTag[i.type]): Symbol = + it.tpe match { + case SingleType(_, sym) if !sym.isFreeTerm && sym.isStatic => sym + case x => sys.error("Instance must be static (was " + x + ").") + } + + def select(t: Tree, name: String): Tree = Select(t, TermName(name)) + + /** Returns the symbol for the non-private method named `name` for the class/module `obj`. */ + def method(obj: Symbol, name: String): Symbol = { + val ts: Type = obj.typeSignature + val m: global.Symbol = ts.asInstanceOf[global.Type].nonPrivateMember(global.newTermName(name)) + m.asInstanceOf[Symbol] + } + + /** + * Returns a Type representing the type constructor tcp.. For example, given + * `object Demo { type M[x] = List[x] }`, the call `extractTC(Demo, "M")` will return a type representing + * the type constructor `[x] List[x]`. + */ + def extractTC(tcp: AnyRef with Singleton, name: String)( + implicit it: ctx.TypeTag[tcp.type] + ): ctx.Type = { + val itTpe = it.tpe.asInstanceOf[global.Type] + val m = itTpe.nonPrivateMember(global.newTypeName(name)) + val tc = itTpe.memberInfo(m).asInstanceOf[ctx.universe.Type] + assert(tc != NoType && tc.takesTypeArgs, "Invalid type constructor: " + tc) + tc + } + + /** + * Substitutes wrappers in tree `t` with the result of `subWrapper`. + * A wrapper is a Tree of the form `f[T](v)` for which isWrapper(, , .target) returns true. + * Typically, `f` is a `Select` or `Ident`. + * The wrapper is replaced with the result of `subWrapper(, , )` + */ + def transformWrappers( + t: Tree, + subWrapper: (String, Type, Tree, Tree) => Converted[ctx.type] + ): Tree = { + // the main tree transformer that replaces calls to InputWrapper.wrap(x) with + // plain Idents that reference the actual input value + object appTransformer extends Transformer { + override def transform(tree: Tree): Tree = + tree match { + case ApplyTree(TypeApply(Select(_, nme), targ :: Nil), qual :: Nil) => + subWrapper(nme.decodedName.toString, targ.tpe, qual, tree) match { + case Converted.Success(t, finalTx) => + changeOwner(qual, currentOwner, initialOwner) // Fixes https://github.com/sbt/sbt/issues/1150 + finalTx(t) + case Converted.Failure(p, m) => ctx.abort(p, m) + case _: Converted.NotApplicable[_] => super.transform(tree) + } + case _ => super.transform(tree) + } + } + appTransformer.atOwner(initialOwner) { + appTransformer.transform(t) + } + } +} diff --git a/core-macros/src/main/scala/sbt/internal/util/appmacro/Convert.scala b/core-macros/src/main/scala/sbt/internal/util/appmacro/Convert.scala new file mode 100644 index 000000000..a3e3a754b --- /dev/null +++ b/core-macros/src/main/scala/sbt/internal/util/appmacro/Convert.scala @@ -0,0 +1,51 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util +package appmacro + +import scala.reflect._ +import macros._ +import Types.idFun + +abstract class Convert { + def apply[T: c.WeakTypeTag](c: blackbox.Context)(nme: String, in: c.Tree): Converted[c.type] + def asPredicate(c: blackbox.Context): (String, c.Type, c.Tree) => Boolean = + (n, tpe, tree) => { + val tag = c.WeakTypeTag(tpe) + apply(c)(n, tree)(tag).isSuccess + } +} +sealed trait Converted[C <: blackbox.Context with Singleton] { + def isSuccess: Boolean + def transform(f: C#Tree => C#Tree): Converted[C] +} +object Converted { + def NotApplicable[C <: blackbox.Context with Singleton] = new NotApplicable[C] + final case class Failure[C <: blackbox.Context with Singleton]( + position: C#Position, + message: String + ) extends Converted[C] { + def isSuccess = false + def transform(f: C#Tree => C#Tree): Converted[C] = new Failure(position, message) + } + final class NotApplicable[C <: blackbox.Context with Singleton] extends Converted[C] { + def isSuccess = false + def transform(f: C#Tree => C#Tree): Converted[C] = this + } + final case class Success[C <: blackbox.Context with Singleton]( + tree: C#Tree, + finalTransform: C#Tree => C#Tree + ) extends Converted[C] { + def isSuccess = true + def transform(f: C#Tree => C#Tree): Converted[C] = Success(f(tree), finalTransform) + } + object Success { + def apply[C <: blackbox.Context with Singleton](tree: C#Tree): Success[C] = + Success(tree, idFun) + } +} diff --git a/core-macros/src/main/scala/sbt/internal/util/appmacro/Instance.scala b/core-macros/src/main/scala/sbt/internal/util/appmacro/Instance.scala new file mode 100644 index 000000000..35e2d7dc6 --- /dev/null +++ b/core-macros/src/main/scala/sbt/internal/util/appmacro/Instance.scala @@ -0,0 +1,230 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util +package appmacro + +import sbt.internal.util.Classes.Applicative +import sbt.internal.util.Types.Id + +/** + * The separate hierarchy from Applicative/Monad is for two reasons. + * + * 1. The type constructor is represented as an abstract type because a TypeTag cannot represent a type constructor directly. + * 2. The applicative interface is uncurried. + */ +trait Instance { + type M[x] + def app[K[L[x]], Z](in: K[M], f: K[Id] => Z)(implicit a: AList[K]): M[Z] + def map[S, T](in: M[S], f: S => T): M[T] + def pure[T](t: () => T): M[T] +} + +trait MonadInstance extends Instance { + def flatten[T](in: M[M[T]]): M[T] +} + +import scala.reflect.macros._ + +object Instance { + type Aux[M0[_]] = Instance { type M[x] = M0[x] } + type Aux2[M0[_], N[_]] = Instance { type M[x] = M0[N[x]] } + + final val ApplyName = "app" + final val FlattenName = "flatten" + final val PureName = "pure" + final val MapName = "map" + final val InstanceTCName = "M" + + final class Input[U <: Universe with Singleton]( + val tpe: U#Type, + val expr: U#Tree, + val local: U#ValDef + ) + trait Transform[C <: blackbox.Context with Singleton, N[_]] { + def apply(in: C#Tree): C#Tree + } + def idTransform[C <: blackbox.Context with Singleton]: Transform[C, Id] = in => in + + /** + * Implementation of a macro that provides a direct syntax for applicative functors and monads. + * It is intended to be used in conjunction with another macro that conditions the inputs. + * + * This method processes the Tree `t` to find inputs of the form `wrap[T]( input )` + * This form is typically constructed by another macro that pretends to be able to get a value of type `T` + * from a value convertible to `M[T]`. This `wrap(input)` form has two main purposes. + * First, it identifies the inputs that should be transformed. + * Second, it allows the input trees to be wrapped for later conversion into the appropriate `M[T]` type by `convert`. + * This wrapping is necessary because applying the first macro must preserve the original type, + * but it is useful to delay conversion until the outer, second macro is called. The `wrap` method accomplishes this by + * allowing the original `Tree` and `Type` to be hidden behind the raw `T` type. This method will remove the call to `wrap` + * so that it is not actually called at runtime. + * + * Each `input` in each expression of the form `wrap[T]( input )` is transformed by `convert`. + * This transformation converts the input Tree to a Tree of type `M[T]`. + * The original wrapped expression `wrap(input)` is replaced by a reference to a new local `val x: T`, where `x` is a fresh name. + * These converted inputs are passed to `builder` as well as the list of these synthetic `ValDef`s. + * The `TupleBuilder` instance constructs a tuple (Tree) from the inputs and defines the right hand side of the vals + * that unpacks the tuple containing the results of the inputs. + * + * The constructed tuple of inputs and the code that unpacks the results of the inputs are then passed to the `i`, + * which is an implementation of `Instance` that is statically accessible. + * An Instance defines a applicative functor associated with a specific type constructor and, if it implements MonadInstance as well, a monad. + * Typically, it will be either a top-level module or a stable member of a top-level module (such as a val or a nested module). + * The `with Singleton` part of the type verifies some cases at macro compilation time, + * while the full check for static accessibility is done at macro expansion time. + * Note: Ideally, the types would verify that `i: MonadInstance` when `t.isRight`. + * With the various dependent types involved, this is not worth it. + * + * The `t` argument is the argument of the macro that will be transformed as described above. + * If the macro that calls this method is for a multi-input map (app followed by map), + * `t` should be the argument wrapped in Left. + * If this is for multi-input flatMap (app followed by flatMap), + * this should be the argument wrapped in Right. + */ + def contImpl[T, N[_]]( + c: blackbox.Context, + i: Instance with Singleton, + convert: Convert, + builder: TupleBuilder, + linter: LinterDSL + )( + t: Either[c.Expr[T], c.Expr[i.M[T]]], + inner: Transform[c.type, N] + )( + implicit tt: c.WeakTypeTag[T], + nt: c.WeakTypeTag[N[T]], + it: c.TypeTag[i.type] + ): c.Expr[i.M[N[T]]] = { + import c.universe.{ Apply => ApplyTree, _ } + + val util = ContextUtil[c.type](c) + val mTC: Type = util.extractTC(i, InstanceTCName) + val mttpe: Type = appliedType(mTC, nt.tpe :: Nil).dealias + + // the tree for the macro argument + val (tree, treeType) = t match { + case Left(l) => (l.tree, nt.tpe.dealias) + case Right(r) => (r.tree, mttpe) + } + // the Symbol for the anonymous function passed to the appropriate Instance.map/flatMap/pure method + // this Symbol needs to be known up front so that it can be used as the owner of synthetic vals + val functionSym = util.functionSymbol(tree.pos) + + val instanceSym = util.singleton(i) + // A Tree that references the statically accessible Instance that provides the actual implementations of map, flatMap, ... + val instance = Ident(instanceSym) + + val isWrapper: (String, Type, Tree) => Boolean = convert.asPredicate(c) + + // Local definitions `defs` in the macro. This is used to ensure references are to M instances defined outside of the macro call. + // Also `refCount` is the number of references, which is used to create the private, synthetic method containing the body + val defs = util.collectDefs(tree, isWrapper) + val checkQual: Tree => Unit = util.checkReferences(defs, isWrapper, mttpe.erasure) + + type In = Input[c.universe.type] + var inputs = List[In]() + + // transforms the original tree into calls to the Instance functions pure, map, ..., + // resulting in a value of type M[T] + def makeApp(body: Tree): Tree = + inputs match { + case Nil => pure(body) + case x :: Nil => single(body, x) + case xs => arbArity(body, xs) + } + + // no inputs, so construct M[T] via Instance.pure or pure+flatten + def pure(body: Tree): Tree = { + val typeApplied = TypeApply(util.select(instance, PureName), TypeTree(treeType) :: Nil) + val f = util.createFunction(Nil, body, functionSym) + val p = ApplyTree(typeApplied, f :: Nil) + if (t.isLeft) p else flatten(p) + } + // m should have type M[M[T]] + // the returned Tree will have type M[T] + def flatten(m: Tree): Tree = { + val typedFlatten = TypeApply(util.select(instance, FlattenName), TypeTree(tt.tpe) :: Nil) + ApplyTree(typedFlatten, m :: Nil) + } + + // calls Instance.map or flatmap directly, skipping the intermediate Instance.app that is unnecessary for a single input + def single(body: Tree, input: In): Tree = { + val variable = input.local + val param = + treeCopy.ValDef(variable, util.parameterModifiers, variable.name, variable.tpt, EmptyTree) + val typeApplied = + TypeApply(util.select(instance, MapName), variable.tpt :: (TypeTree(treeType): Tree) :: Nil) + val f = util.createFunction(param :: Nil, body, functionSym) + val mapped = ApplyTree(typeApplied, input.expr :: f :: Nil) + if (t.isLeft) mapped else flatten(mapped) + } + + // calls Instance.app to get the values for all inputs and then calls Instance.map or flatMap to evaluate the body + def arbArity(body: Tree, inputs: List[In]): Tree = { + val result = builder.make(c)(mTC, inputs) + val param = util.freshMethodParameter(appliedType(result.representationC, util.idTC :: Nil)) + val bindings = result.extract(param) + val f = util.createFunction(param :: Nil, Block(bindings, body), functionSym) + val ttt = TypeTree(treeType) + val typedApp = + TypeApply(util.select(instance, ApplyName), TypeTree(result.representationC) :: ttt :: Nil) + val app = + ApplyTree(ApplyTree(typedApp, result.input :: f :: Nil), result.alistInstance :: Nil) + if (t.isLeft) app else flatten(app) + } + + // Called when transforming the tree to add an input. + // For `qual` of type M[A], and a `selection` qual.value, + // the call is addType(Type A, Tree qual) + // The result is a Tree representing a reference to + // the bound value of the input. + def addType(tpe: Type, qual: Tree, selection: Tree): Tree = { + qual.foreach(checkQual) + val vd = util.freshValDef(tpe, qual.pos, functionSym) + inputs ::= new Input(tpe, qual, vd) + util.refVal(selection, vd) + } + def sub(name: String, tpe: Type, qual: Tree, replace: Tree): Converted[c.type] = { + val tag = c.WeakTypeTag[T](tpe) + convert[T](c)(name, qual)(tag) transform { tree => + addType(tpe, tree, replace) + } + } + + // applies the transformation + linter.runLinter(c)(tree) + val tx = util.transformWrappers(tree, (n, tpe, t, replace) => sub(n, tpe, t, replace)) + // resetting attributes must be: a) local b) done here and not wider or else there are obscure errors + val tr = makeApp(inner(tx)) + val noWarn = q"""($tr: @_root_.scala.annotation.nowarn("cat=other-pure-statement"))""" + c.Expr[i.M[N[T]]](noWarn) + } + + import Types._ + + implicit def applicativeInstance[A[_]](implicit ap: Applicative[A]): Instance.Aux[A] = + new Instance { + type M[x] = A[x] + def app[K[L[x]], Z](in: K[A], f: K[Id] => Z)(implicit a: AList[K]) = a.apply[A, Z](in, f) + def map[S, T](in: A[S], f: S => T) = ap.map(f, in) + def pure[S](s: () => S): M[S] = ap.pure(s()) + } + + def compose[A[_], B[_]](implicit a: Aux[A], b: Aux[B]): Instance.Aux2[A, B] = + new Composed[A, B](a, b) + // made a public, named, unsealed class because of trouble with macros and inference when the Instance is not an object + class Composed[A[_], B[_]](a: Aux[A], b: Aux[B]) extends Instance { + type M[x] = A[B[x]] + def pure[S](s: () => S): A[B[S]] = a.pure(() => b.pure(s)) + def map[S, T](in: M[S], f: S => T): M[T] = a.map(in, (bv: B[S]) => b.map(bv, f)) + def app[K[L[x]], Z](in: K[M], f: K[Id] => Z)(implicit alist: AList[K]): A[B[Z]] = { + val g: K[B] => B[Z] = in => b.app[K, Z](in, f) + a.app[AList.SplitK[K, B]#l, B[Z]](in, g)(AList.asplit(alist)) + } + } +} diff --git a/core-macros/src/main/scala/sbt/internal/util/appmacro/KListBuilder.scala b/core-macros/src/main/scala/sbt/internal/util/appmacro/KListBuilder.scala new file mode 100644 index 000000000..6d2ae0a50 --- /dev/null +++ b/core-macros/src/main/scala/sbt/internal/util/appmacro/KListBuilder.scala @@ -0,0 +1,85 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util +package appmacro + +import scala.reflect._ +import macros._ + +/** A `TupleBuilder` that uses a KList as the tuple representation.*/ +object KListBuilder extends TupleBuilder { + def make( + c: blackbox.Context + )(mt: c.Type, inputs: Inputs[c.universe.type]): BuilderResult[c.type] = + new BuilderResult[c.type] { + val ctx: c.type = c + val util = ContextUtil[c.type](c) + import c.universe.{ Apply => ApplyTree, _ } + import util._ + + val knilType = c.typeOf[KNil] + val knil = Ident(knilType.typeSymbol.companion) + val kconsTpe = c.typeOf[KCons[Int, KNil, List]] + val kcons = kconsTpe.typeSymbol.companion + val mTC: Type = mt.asInstanceOf[c.universe.Type] + val kconsTC: Type = kconsTpe.typeConstructor + + /** This is the L in the type function [L[x]] ... */ + val tcVariable: TypeSymbol = newTCVariable(util.initialOwner) + + /** Instantiates KCons[h, t <: KList[L], L], where L is the type constructor variable */ + def kconsType(h: Type, t: Type): Type = + appliedType(kconsTC, h :: t :: refVar(tcVariable) :: Nil) + + def bindKList(prev: ValDef, revBindings: List[ValDef], params: List[ValDef]): List[ValDef] = + params match { + case (x @ ValDef(mods, name, tpt, _)) :: xs => + val rhs = select(Ident(prev.name), "head") + val head = treeCopy.ValDef(x, mods, name, tpt, rhs) + util.setSymbol(head, x.symbol) + val tail = localValDef(TypeTree(), select(Ident(prev.name), "tail")) + val base = head :: revBindings + bindKList(tail, if (xs.isEmpty) base else tail :: base, xs) + case Nil => revBindings.reverse + } + + private[this] def makeKList( + revInputs: Inputs[c.universe.type], + klist: Tree, + klistType: Type + ): Tree = + revInputs match { + case in :: tail => + val next = ApplyTree( + TypeApply( + Ident(kcons), + TypeTree(in.tpe) :: TypeTree(klistType) :: TypeTree(mTC) :: Nil + ), + in.expr :: klist :: Nil + ) + makeKList(tail, next, appliedType(kconsTC, in.tpe :: klistType :: mTC :: Nil)) + case Nil => klist + } + + /** The input trees combined in a KList */ + val klist = makeKList(inputs.reverse, knil, knilType) + + /** + * The input types combined in a KList type. The main concern is tracking the heterogeneous types. + * The type constructor is tcVariable, so that it can be applied to [X] X or M later. + * When applied to `M`, this type gives the type of the `input` KList. + */ + val klistType: Type = inputs.foldRight(knilType)((in, klist) => kconsType(in.tpe, klist)) + + val representationC = internal.polyType(tcVariable :: Nil, klistType) + val input = klist + val alistInstance: ctx.universe.Tree = + TypeApply(select(Ident(alist), "klist"), TypeTree(representationC) :: Nil) + def extract(param: ValDef) = bindKList(param, Nil, inputs.map(_.local)) + } +} diff --git a/core-macros/src/main/scala/sbt/internal/util/appmacro/LinterDSL.scala b/core-macros/src/main/scala/sbt/internal/util/appmacro/LinterDSL.scala new file mode 100644 index 000000000..fb42f2e2e --- /dev/null +++ b/core-macros/src/main/scala/sbt/internal/util/appmacro/LinterDSL.scala @@ -0,0 +1,20 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util.appmacro + +import scala.reflect.macros.blackbox + +trait LinterDSL { + def runLinter(ctx: blackbox.Context)(tree: ctx.Tree): Unit +} + +object LinterDSL { + object Empty extends LinterDSL { + override def runLinter(ctx: blackbox.Context)(tree: ctx.Tree): Unit = () + } +} diff --git a/core-macros/src/main/scala/sbt/internal/util/appmacro/MixedBuilder.scala b/core-macros/src/main/scala/sbt/internal/util/appmacro/MixedBuilder.scala new file mode 100644 index 000000000..abcde7c80 --- /dev/null +++ b/core-macros/src/main/scala/sbt/internal/util/appmacro/MixedBuilder.scala @@ -0,0 +1,27 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util +package appmacro + +import scala.reflect._ +import macros._ + +/** + * A builder that uses `TupleN` as the representation for small numbers of inputs (up to `TupleNBuilder.MaxInputs`) + * and `KList` for larger numbers of inputs. This builder cannot handle fewer than 2 inputs. + */ +object MixedBuilder extends TupleBuilder { + def make( + c: blackbox.Context + )(mt: c.Type, inputs: Inputs[c.universe.type]): BuilderResult[c.type] = { + val delegate = + if (inputs.size > TupleNBuilder.MaxInputs) (KListBuilder: TupleBuilder) + else (TupleNBuilder: TupleBuilder) + delegate.make(c)(mt, inputs) + } +} diff --git a/core-macros/src/main/scala/sbt/internal/util/appmacro/StringTypeTag.scala b/core-macros/src/main/scala/sbt/internal/util/appmacro/StringTypeTag.scala new file mode 100644 index 000000000..faeda9245 --- /dev/null +++ b/core-macros/src/main/scala/sbt/internal/util/appmacro/StringTypeTag.scala @@ -0,0 +1,27 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util.appmacro + +import scala.reflect.macros.blackbox + +object StringTypeTag { + def impl[A: c.WeakTypeTag](c: blackbox.Context): c.Tree = { + import c.universe._ + val tpe = weakTypeOf[A] + def typeToString(tpe: Type): String = tpe match { + case TypeRef(_, sym, args) if args.nonEmpty => + val typeCon = tpe.typeSymbol.fullName + val typeArgs = args map typeToString + s"""$typeCon[${typeArgs.mkString(",")}]""" + case _ => tpe.toString + } + + val key = Literal(Constant(typeToString(tpe))) + q"new sbt.internal.util.StringTypeTag[$tpe]($key)" + } +} diff --git a/core-macros/src/main/scala/sbt/internal/util/appmacro/TupleBuilder.scala b/core-macros/src/main/scala/sbt/internal/util/appmacro/TupleBuilder.scala new file mode 100644 index 000000000..14afc4eb4 --- /dev/null +++ b/core-macros/src/main/scala/sbt/internal/util/appmacro/TupleBuilder.scala @@ -0,0 +1,64 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util +package appmacro + +import scala.reflect._ +import macros._ + +/** + * A `TupleBuilder` abstracts the work of constructing a tuple data structure such as a `TupleN` or `KList` + * and extracting values from it. The `Instance` macro implementation will (roughly) traverse the tree of its argument + * and ultimately obtain a list of expressions with type `M[T]` for different types `T`. + * The macro constructs an `Input` value for each of these expressions that contains the `Type` for `T`, + * the `Tree` for the expression, and a `ValDef` that will hold the value for the input. + * + * `TupleBuilder.apply` is provided with the list of `Input`s and is expected to provide three values in the returned BuilderResult. + * First, it returns the constructed tuple data structure Tree in `input`. + * Next, it provides the type constructor `representationC` that, when applied to M, gives the type of tuple data structure. + * For example, a builder that constructs a `Tuple3` for inputs `M[Int]`, `M[Boolean]`, and `M[String]` + * would provide a Type representing `[L[x]] (L[Int], L[Boolean], L[String])`. The `input` method + * would return a value whose type is that type constructor applied to M, or `(M[Int], M[Boolean], M[String])`. + * + * Finally, the `extract` method provides a list of vals that extract information from the applied input. + * The type of the applied input is the type constructor applied to `Id` (`[X] X`). + * The returned list of ValDefs should be the ValDefs from `inputs`, but with non-empty right-hand sides. + */ +trait TupleBuilder { + + /** A convenience alias for a list of inputs (associated with a Universe of type U). */ + type Inputs[U <: Universe with Singleton] = List[Instance.Input[U]] + + /** Constructs a one-time use Builder for Context `c` and type constructor `tcType`. */ + def make( + c: blackbox.Context + )(tcType: c.Type, inputs: Inputs[c.universe.type]): BuilderResult[c.type] +} + +trait BuilderResult[C <: blackbox.Context with Singleton] { + val ctx: C + import ctx.universe._ + + /** + * Represents the higher-order type constructor `[L[x]] ...` where `...` is the + * type of the data structure containing the added expressions, + * except that it is abstracted over the type constructor applied to each heterogeneous part of the type . + */ + def representationC: PolyType + + /** The instance of AList for the input. For a `representationC` of `[L[x]]`, this `Tree` should have a `Type` of `AList[L]`*/ + def alistInstance: Tree + + /** Returns the completed value containing all expressions added to the builder. */ + def input: Tree + + /* The list of definitions that extract values from a value of type `$representationC[Id]`. + * The returned value should be identical to the `ValDef`s provided to the `TupleBuilder.make` method but with + * non-empty right hand sides. Each `ValDef` may refer to `param` and previous `ValDef`s in the list.*/ + def extract(param: ValDef): List[ValDef] +} diff --git a/core-macros/src/main/scala/sbt/internal/util/appmacro/TupleNBuilder.scala b/core-macros/src/main/scala/sbt/internal/util/appmacro/TupleNBuilder.scala new file mode 100644 index 000000000..e47e06b11 --- /dev/null +++ b/core-macros/src/main/scala/sbt/internal/util/appmacro/TupleNBuilder.scala @@ -0,0 +1,67 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util +package appmacro + +import scala.tools.nsc.Global +import scala.reflect._ +import macros._ + +/** + * A builder that uses a TupleN as the tuple representation. + * It is limited to tuples of size 2 to `MaxInputs`. + */ +object TupleNBuilder extends TupleBuilder { + + /** The largest number of inputs that this builder can handle. */ + final val MaxInputs = 11 + final val TupleMethodName = "tuple" + + def make( + c: blackbox.Context + )(mt: c.Type, inputs: Inputs[c.universe.type]): BuilderResult[c.type] = + new BuilderResult[c.type] { + val util = ContextUtil[c.type](c) + import c.universe._ + import util._ + + val global: Global = c.universe.asInstanceOf[Global] + + val ctx: c.type = c + val representationC: PolyType = { + val tcVariable: Symbol = newTCVariable(util.initialOwner) + val tupleTypeArgs = inputs.map( + in => internal.typeRef(NoPrefix, tcVariable, in.tpe :: Nil).asInstanceOf[global.Type] + ) + val tuple = global.definitions.tupleType(tupleTypeArgs) + internal.polyType(tcVariable :: Nil, tuple.asInstanceOf[Type]) + } + + val input: Tree = mkTuple(inputs.map(_.expr)) + val alistInstance: Tree = { + val selectTree = select(Ident(alist), TupleMethodName + inputs.size.toString) + TypeApply(selectTree, inputs.map(in => TypeTree(in.tpe))) + } + def extract(param: ValDef): List[ValDef] = bindTuple(param, Nil, inputs.map(_.local), 1) + + def bindTuple( + param: ValDef, + revBindings: List[ValDef], + params: List[ValDef], + i: Int + ): List[ValDef] = + params match { + case (x @ ValDef(mods, name, tpt, _)) :: xs => + val rhs = select(Ident(param.name), "_" + i.toString) + val newVal = treeCopy.ValDef(x, mods, name, tpt, rhs) + util.setSymbol(newVal, x.symbol) + bindTuple(param, newVal :: revBindings, xs, i + 1) + case Nil => revBindings.reverse + } + } +} diff --git a/dependency-tree/src/main/scala/sbt/plugins/DependencyTreePlugin.scala b/dependency-tree/src/main/scala/sbt/plugins/DependencyTreePlugin.scala new file mode 100644 index 000000000..1fbaabd70 --- /dev/null +++ b/dependency-tree/src/main/scala/sbt/plugins/DependencyTreePlugin.scala @@ -0,0 +1,26 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt +package plugins + +object DependencyTreePlugin extends AutoPlugin { + object autoImport extends DependencyTreeKeys + override def trigger = AllRequirements + override def requires = MiniDependencyTreePlugin + + val configurations = Vector(Compile, Test, IntegrationTest, Runtime, Provided, Optional) + + // MiniDependencyTreePlugin provides baseBasicReportingSettings for Compile and Test + override def projectSettings: Seq[Def.Setting[_]] = + ((configurations diff Vector(Compile, Test)) flatMap { config => + inConfig(config)(DependencyTreeSettings.baseBasicReportingSettings) + }) ++ + (configurations flatMap { config => + inConfig(config)(DependencyTreeSettings.baseFullReportingSettings) + }) +} diff --git a/internal/util-collection/NOTICE b/internal/util-collection/NOTICE new file mode 100644 index 000000000..db1ba7e33 --- /dev/null +++ b/internal/util-collection/NOTICE @@ -0,0 +1,4 @@ +sbt: Collection Component +Copyright 2011 - 2017, Lightbend, Inc. +Copyright 2008 - 2010, Mark Harrah +Licensed under BSD-3-Clause license (see LICENSE) diff --git a/internal/util-collection/src/main/scala-2.12/sbt/internal/util/Par.scala b/internal/util-collection/src/main/scala-2.12/sbt/internal/util/Par.scala new file mode 100644 index 000000000..34e939244 --- /dev/null +++ b/internal/util-collection/src/main/scala-2.12/sbt/internal/util/Par.scala @@ -0,0 +1,13 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util +import scala.collection.parallel.ParSeq + +private[util] object Par { + def apply[R](s: Seq[R]): ParSeq[R] = s.par +} diff --git a/internal/util-collection/src/main/scala-2.12/sbt/internal/util/WrappedMap.scala b/internal/util-collection/src/main/scala-2.12/sbt/internal/util/WrappedMap.scala new file mode 100644 index 000000000..6a8caf80f --- /dev/null +++ b/internal/util-collection/src/main/scala-2.12/sbt/internal/util/WrappedMap.scala @@ -0,0 +1,17 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util + +import scala.collection.JavaConverters._ +private[util] class WrappedMap[K, V](val jmap: java.util.Map[K, V]) extends Map[K, V] { + def +[V1 >: V](kv: (K, V1)): scala.collection.immutable.Map[K, V1] = + jmap.asScala.toMap + kv + def -(key: K): scala.collection.immutable.Map[K, V] = jmap.asScala.toMap - key + def get(key: K): Option[V] = Option(jmap.get(key)) + def iterator: Iterator[(K, V)] = jmap.asScala.iterator +} diff --git a/internal/util-collection/src/main/scala-2.13/sbt/internal/util/Par.scala b/internal/util-collection/src/main/scala-2.13/sbt/internal/util/Par.scala new file mode 100644 index 000000000..d7fa1d766 --- /dev/null +++ b/internal/util-collection/src/main/scala-2.13/sbt/internal/util/Par.scala @@ -0,0 +1,14 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util +import scala.collection.parallel.CollectionConverters._ +import scala.collection.parallel.ParSeq + +private[util] object Par { + def apply[R](s: Seq[R]): ParSeq[R] = s.par +} diff --git a/internal/util-collection/src/main/scala-2.13/sbt/internal/util/WrappedMap.scala b/internal/util-collection/src/main/scala-2.13/sbt/internal/util/WrappedMap.scala new file mode 100644 index 000000000..8d8a29e1d --- /dev/null +++ b/internal/util-collection/src/main/scala-2.13/sbt/internal/util/WrappedMap.scala @@ -0,0 +1,18 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util + +import scala.collection.JavaConverters._ +private[util] class WrappedMap[K, V](val jmap: java.util.Map[K, V]) extends Map[K, V] { + def removed(key: K): scala.collection.immutable.Map[K, V] = jmap.asScala.toMap.removed(key) + def updated[V1 >: V](key: K, value: V1): scala.collection.immutable.Map[K, V1] = + jmap.asScala.toMap.updated(key, value) + + def get(key: K): Option[V] = Option(jmap.get(key)) + def iterator: Iterator[(K, V)] = jmap.asScala.iterator +} diff --git a/internal/util-collection/src/main/scala/sbt/internal/util/AList.scala b/internal/util-collection/src/main/scala/sbt/internal/util/AList.scala new file mode 100644 index 000000000..ca10d2356 --- /dev/null +++ b/internal/util-collection/src/main/scala/sbt/internal/util/AList.scala @@ -0,0 +1,389 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util + +import Classes.Applicative +import Types._ + +/** + * An abstraction over a higher-order type constructor `K[x[y]]` with the purpose of abstracting + * over heterogeneous sequences like `KList` and `TupleN` with elements with a common type + * constructor as well as homogeneous sequences `Seq[M[T]]`. + */ +trait AList[K[L[x]]] { + def transform[M[_], N[_]](value: K[M], f: M ~> N): K[N] + def traverse[M[_], N[_], P[_]](value: K[M], f: M ~> (N ∙ P)#l)( + implicit np: Applicative[N] + ): N[K[P]] + def foldr[M[_], A](value: K[M], f: (M[_], A) => A, init: A): A + + def toList[M[_]](value: K[M]): List[M[_]] = foldr[M, List[M[_]]](value, _ :: _, Nil) + + def apply[M[_], C](value: K[M], f: K[Id] => C)(implicit a: Applicative[M]): M[C] = + a.map(f, traverse[M, M, Id](value, idK[M])(a)) +} + +object AList { + type Empty = AList[ConstK[Unit]#l] + + /** AList for Unit, which represents a sequence that is always empty.*/ + val empty: Empty = new Empty { + def transform[M[_], N[_]](in: Unit, f: M ~> N) = () + def foldr[M[_], T](in: Unit, f: (M[_], T) => T, init: T) = init + override def apply[M[_], C](in: Unit, f: Unit => C)(implicit app: Applicative[M]): M[C] = + app.pure(f(())) + def traverse[M[_], N[_], P[_]](in: Unit, f: M ~> (N ∙ P)#l)( + implicit np: Applicative[N] + ): N[Unit] = np.pure(()) + } + + type SeqList[T] = AList[λ[L[x] => List[L[T]]]] + + /** AList for a homogeneous sequence. */ + def seq[T]: SeqList[T] = new SeqList[T] { + def transform[M[_], N[_]](s: List[M[T]], f: M ~> N) = s.map(f.fn[T]) + def foldr[M[_], A](s: List[M[T]], f: (M[_], A) => A, init: A): A = + s.reverse.foldLeft(init)((t, m) => f(m, t)) + + override def apply[M[_], C](s: List[M[T]], f: List[T] => C)( + implicit ap: Applicative[M] + ): M[C] = { + def loop[V](in: List[M[T]], g: List[T] => V): M[V] = + in match { + case Nil => ap.pure(g(Nil)) + case x :: xs => + val h = (ts: List[T]) => (t: T) => g(t :: ts) + ap.apply(loop(xs, h), x) + } + loop(s, f) + } + + def traverse[M[_], N[_], P[_]](s: List[M[T]], f: M ~> (N ∙ P)#l)( + implicit np: Applicative[N] + ): N[List[P[T]]] = ??? + } + + /** AList for the arbitrary arity data structure KList. */ + def klist[KL[M[_]] <: KList.Aux[M, KL]]: AList[KL] = new AList[KL] { + def transform[M[_], N[_]](k: KL[M], f: M ~> N) = k.transform(f) + def foldr[M[_], T](k: KL[M], f: (M[_], T) => T, init: T): T = k.foldr(f, init) + override def apply[M[_], C](k: KL[M], f: KL[Id] => C)(implicit app: Applicative[M]): M[C] = + k.apply(f)(app) + def traverse[M[_], N[_], P[_]](k: KL[M], f: M ~> (N ∙ P)#l)( + implicit np: Applicative[N] + ): N[KL[P]] = k.traverse[N, P](f)(np) + override def toList[M[_]](k: KL[M]) = k.toList + } + + type Single[A] = AList[λ[L[x] => L[A]]] + + /** AList for a single value. */ + def single[A]: Single[A] = new Single[A] { + def transform[M[_], N[_]](a: M[A], f: M ~> N) = f(a) + def foldr[M[_], T](a: M[A], f: (M[_], T) => T, init: T): T = f(a, init) + def traverse[M[_], N[_], P[_]](a: M[A], f: M ~> (N ∙ P)#l)( + implicit np: Applicative[N] + ): N[P[A]] = f(a) + } + + /** Example: calling `AList.SplitK[K, Task]#l` returns the type lambda `A[x] => K[A[Task[x]]`. */ + sealed trait SplitK[K[L[x]], B[x]] { type l[A[x]] = K[(A ∙ B)#l] } + + type ASplit[K[L[x]], B[x]] = AList[SplitK[K, B]#l] + + /** AList that operates on the outer type constructor `A` of a composition `[x] A[B[x]]` for type constructors `A` and `B`. */ + def asplit[K[L[x]], B[x]](base: AList[K]): ASplit[K, B] = new ASplit[K, B] { + type Split[L[x]] = K[(L ∙ B)#l] + + def transform[M[_], N[_]](value: Split[M], f: M ~> N): Split[N] = + base.transform[(M ∙ B)#l, (N ∙ B)#l](value, nestCon[M, N, B](f)) + + def traverse[M[_], N[_], P[_]](value: Split[M], f: M ~> (N ∙ P)#l)( + implicit np: Applicative[N] + ): N[Split[P]] = { + val g = nestCon[M, (N ∙ P)#l, B](f) + base.traverse[(M ∙ B)#l, N, (P ∙ B)#l](value, g)(np) + } + + def foldr[M[_], A](value: Split[M], f: (M[_], A) => A, init: A): A = + base.foldr[(M ∙ B)#l, A](value, f, init) + } + + // TODO: auto-generate + sealed trait T2K[A, B] { type l[L[x]] = (L[A], L[B]) } + type T2List[A, B] = AList[T2K[A, B]#l] + def tuple2[A, B]: T2List[A, B] = new T2List[A, B] { + type T2[M[_]] = (M[A], M[B]) + def transform[M[_], N[_]](t: T2[M], f: M ~> N): T2[N] = (f(t._1), f(t._2)) + def foldr[M[_], T](t: T2[M], f: (M[_], T) => T, init: T): T = f(t._1, f(t._2, init)) + def traverse[M[_], N[_], P[_]](t: T2[M], f: M ~> (N ∙ P)#l)( + implicit np: Applicative[N] + ): N[T2[P]] = { + val g = (Tuple2.apply[P[A], P[B]] _).curried + np.apply(np.map(g, f(t._1)), f(t._2)) + } + } + + sealed trait T3K[A, B, C] { type l[L[x]] = (L[A], L[B], L[C]) } + type T3List[A, B, C] = AList[T3K[A, B, C]#l] + def tuple3[A, B, C]: T3List[A, B, C] = new T3List[A, B, C] { + type T3[M[_]] = (M[A], M[B], M[C]) + def transform[M[_], N[_]](t: T3[M], f: M ~> N) = (f(t._1), f(t._2), f(t._3)) + def foldr[M[_], T](t: T3[M], f: (M[_], T) => T, init: T): T = f(t._1, f(t._2, f(t._3, init))) + def traverse[M[_], N[_], P[_]](t: T3[M], f: M ~> (N ∙ P)#l)( + implicit np: Applicative[N] + ): N[T3[P]] = { + val g = (Tuple3.apply[P[A], P[B], P[C]] _).curried + np.apply(np.apply(np.map(g, f(t._1)), f(t._2)), f(t._3)) + } + } + + sealed trait T4K[A, B, C, D] { type l[L[x]] = (L[A], L[B], L[C], L[D]) } + type T4List[A, B, C, D] = AList[T4K[A, B, C, D]#l] + def tuple4[A, B, C, D]: T4List[A, B, C, D] = new T4List[A, B, C, D] { + type T4[M[_]] = (M[A], M[B], M[C], M[D]) + def transform[M[_], N[_]](t: T4[M], f: M ~> N) = (f(t._1), f(t._2), f(t._3), f(t._4)) + def foldr[M[_], T](t: T4[M], f: (M[_], T) => T, init: T): T = + f(t._1, f(t._2, f(t._3, f(t._4, init)))) + def traverse[M[_], N[_], P[_]](t: T4[M], f: M ~> (N ∙ P)#l)( + implicit np: Applicative[N] + ): N[T4[P]] = { + val g = (Tuple4.apply[P[A], P[B], P[C], P[D]] _).curried + np.apply(np.apply(np.apply(np.map(g, f(t._1)), f(t._2)), f(t._3)), f(t._4)) + } + } + + sealed trait T5K[A, B, C, D, E] { type l[L[x]] = (L[A], L[B], L[C], L[D], L[E]) } + type T5List[A, B, C, D, E] = AList[T5K[A, B, C, D, E]#l] + def tuple5[A, B, C, D, E]: T5List[A, B, C, D, E] = new T5List[A, B, C, D, E] { + type T5[M[_]] = (M[A], M[B], M[C], M[D], M[E]) + def transform[M[_], N[_]](t: T5[M], f: M ~> N) = (f(t._1), f(t._2), f(t._3), f(t._4), f(t._5)) + def foldr[M[_], T](t: T5[M], f: (M[_], T) => T, init: T): T = + f(t._1, f(t._2, f(t._3, f(t._4, f(t._5, init))))) + def traverse[M[_], N[_], P[_]](t: T5[M], f: M ~> (N ∙ P)#l)( + implicit np: Applicative[N] + ): N[T5[P]] = { + val g = (Tuple5.apply[P[A], P[B], P[C], P[D], P[E]] _).curried + np.apply(np.apply(np.apply(np.apply(np.map(g, f(t._1)), f(t._2)), f(t._3)), f(t._4)), f(t._5)) + } + } + + sealed trait T6K[A, B, C, D, E, F] { type l[L[x]] = (L[A], L[B], L[C], L[D], L[E], L[F]) } + type T6List[A, B, C, D, E, F] = AList[T6K[A, B, C, D, E, F]#l] + def tuple6[A, B, C, D, E, F]: T6List[A, B, C, D, E, F] = new T6List[A, B, C, D, E, F] { + type T6[M[_]] = (M[A], M[B], M[C], M[D], M[E], M[F]) + def transform[M[_], N[_]](t: T6[M], f: M ~> N) = + (f(t._1), f(t._2), f(t._3), f(t._4), f(t._5), f(t._6)) + def foldr[M[_], T](t: T6[M], f: (M[_], T) => T, init: T): T = + f(t._1, f(t._2, f(t._3, f(t._4, f(t._5, f(t._6, init)))))) + def traverse[M[_], N[_], P[_]](t: T6[M], f: M ~> (N ∙ P)#l)( + implicit np: Applicative[N] + ): N[T6[P]] = { + val g = (Tuple6.apply[P[A], P[B], P[C], P[D], P[E], P[F]] _).curried + np.apply( + np.apply( + np.apply(np.apply(np.apply(np.map(g, f(t._1)), f(t._2)), f(t._3)), f(t._4)), + f(t._5) + ), + f(t._6) + ) + } + } + + sealed trait T7K[A, B, C, D, E, F, G] { + type l[L[x]] = (L[A], L[B], L[C], L[D], L[E], L[F], L[G]) + } + type T7List[A, B, C, D, E, F, G] = AList[T7K[A, B, C, D, E, F, G]#l] + def tuple7[A, B, C, D, E, F, G]: T7List[A, B, C, D, E, F, G] = new T7List[A, B, C, D, E, F, G] { + type T7[M[_]] = (M[A], M[B], M[C], M[D], M[E], M[F], M[G]) + def transform[M[_], N[_]](t: T7[M], f: M ~> N) = + (f(t._1), f(t._2), f(t._3), f(t._4), f(t._5), f(t._6), f(t._7)) + def foldr[M[_], T](t: T7[M], f: (M[_], T) => T, init: T): T = + f(t._1, f(t._2, f(t._3, f(t._4, f(t._5, f(t._6, f(t._7, init))))))) + def traverse[M[_], N[_], P[_]](t: T7[M], f: M ~> (N ∙ P)#l)( + implicit np: Applicative[N] + ): N[T7[P]] = { + val g = (Tuple7.apply[P[A], P[B], P[C], P[D], P[E], P[F], P[G]] _).curried + np.apply( + np.apply( + np.apply( + np.apply(np.apply(np.apply(np.map(g, f(t._1)), f(t._2)), f(t._3)), f(t._4)), + f(t._5) + ), + f(t._6) + ), + f(t._7) + ) + } + } + + sealed trait T8K[A, B, C, D, E, F, G, H] { + type l[L[x]] = (L[A], L[B], L[C], L[D], L[E], L[F], L[G], L[H]) + } + type T8List[A, B, C, D, E, F, G, H] = AList[T8K[A, B, C, D, E, F, G, H]#l] + def tuple8[A, B, C, D, E, F, G, H]: T8List[A, B, C, D, E, F, G, H] = + new T8List[A, B, C, D, E, F, G, H] { + type T8[M[_]] = (M[A], M[B], M[C], M[D], M[E], M[F], M[G], M[H]) + def transform[M[_], N[_]](t: T8[M], f: M ~> N) = + (f(t._1), f(t._2), f(t._3), f(t._4), f(t._5), f(t._6), f(t._7), f(t._8)) + def foldr[M[_], T](t: T8[M], f: (M[_], T) => T, init: T): T = + f(t._1, f(t._2, f(t._3, f(t._4, f(t._5, f(t._6, f(t._7, f(t._8, init)))))))) + def traverse[M[_], N[_], P[_]](t: T8[M], f: M ~> (N ∙ P)#l)( + implicit np: Applicative[N] + ): N[T8[P]] = { + val g = (Tuple8.apply[P[A], P[B], P[C], P[D], P[E], P[F], P[G], P[H]] _).curried + np.apply( + np.apply( + np.apply( + np.apply( + np.apply(np.apply(np.apply(np.map(g, f(t._1)), f(t._2)), f(t._3)), f(t._4)), + f(t._5) + ), + f(t._6) + ), + f(t._7) + ), + f(t._8) + ) + } + } + + sealed trait T9K[A, B, C, D, E, F, G, H, I] { + type l[L[x]] = (L[A], L[B], L[C], L[D], L[E], L[F], L[G], L[H], L[I]) + } + type T9List[A, B, C, D, E, F, G, H, I] = AList[T9K[A, B, C, D, E, F, G, H, I]#l] + def tuple9[A, B, C, D, E, F, G, H, I]: T9List[A, B, C, D, E, F, G, H, I] = + new T9List[A, B, C, D, E, F, G, H, I] { + type T9[M[_]] = (M[A], M[B], M[C], M[D], M[E], M[F], M[G], M[H], M[I]) + def transform[M[_], N[_]](t: T9[M], f: M ~> N) = + (f(t._1), f(t._2), f(t._3), f(t._4), f(t._5), f(t._6), f(t._7), f(t._8), f(t._9)) + def foldr[M[_], T](t: T9[M], f: (M[_], T) => T, init: T): T = + f(t._1, f(t._2, f(t._3, f(t._4, f(t._5, f(t._6, f(t._7, f(t._8, f(t._9, init))))))))) + def traverse[M[_], N[_], P[_]](t: T9[M], f: M ~> (N ∙ P)#l)( + implicit np: Applicative[N] + ): N[T9[P]] = { + val g = (Tuple9.apply[P[A], P[B], P[C], P[D], P[E], P[F], P[G], P[H], P[I]] _).curried + np.apply( + np.apply( + np.apply( + np.apply( + np.apply( + np.apply(np.apply(np.apply(np.map(g, f(t._1)), f(t._2)), f(t._3)), f(t._4)), + f(t._5) + ), + f(t._6) + ), + f(t._7) + ), + f(t._8) + ), + f(t._9) + ) + } + } + + sealed trait T10K[A, B, C, D, E, F, G, H, I, J] { + type l[L[x]] = (L[A], L[B], L[C], L[D], L[E], L[F], L[G], L[H], L[I], L[J]) + } + type T10List[A, B, C, D, E, F, G, H, I, J] = AList[T10K[A, B, C, D, E, F, G, H, I, J]#l] + def tuple10[A, B, C, D, E, F, G, H, I, J]: T10List[A, B, C, D, E, F, G, H, I, J] = + new T10List[A, B, C, D, E, F, G, H, I, J] { + type T10[M[_]] = (M[A], M[B], M[C], M[D], M[E], M[F], M[G], M[H], M[I], M[J]) + def transform[M[_], N[_]](t: T10[M], f: M ~> N) = + (f(t._1), f(t._2), f(t._3), f(t._4), f(t._5), f(t._6), f(t._7), f(t._8), f(t._9), f(t._10)) + def foldr[M[_], T](t: T10[M], f: (M[_], T) => T, init: T): T = + f( + t._1, + f(t._2, f(t._3, f(t._4, f(t._5, f(t._6, f(t._7, f(t._8, f(t._9, f(t._10, init))))))))) + ) + def traverse[M[_], N[_], P[_]](t: T10[M], f: M ~> (N ∙ P)#l)( + implicit np: Applicative[N] + ): N[T10[P]] = { + val g = + (Tuple10.apply[P[A], P[B], P[C], P[D], P[E], P[F], P[G], P[H], P[I], P[J]] _).curried + np.apply( + np.apply( + np.apply( + np.apply( + np.apply( + np.apply( + np.apply(np.apply(np.apply(np.map(g, f(t._1)), f(t._2)), f(t._3)), f(t._4)), + f(t._5) + ), + f(t._6) + ), + f(t._7) + ), + f(t._8) + ), + f(t._9) + ), + f(t._10) + ) + } + } + + sealed trait T11K[A, B, C, D, E, F, G, H, I, J, K] { + type l[L[x]] = (L[A], L[B], L[C], L[D], L[E], L[F], L[G], L[H], L[I], L[J], L[K]) + } + type T11List[A, B, C, D, E, F, G, H, I, J, K] = AList[T11K[A, B, C, D, E, F, G, H, I, J, K]#l] + def tuple11[A, B, C, D, E, F, G, H, I, J, K]: T11List[A, B, C, D, E, F, G, H, I, J, K] = + new T11List[A, B, C, D, E, F, G, H, I, J, K] { + type T11[M[_]] = (M[A], M[B], M[C], M[D], M[E], M[F], M[G], M[H], M[I], M[J], M[K]) + def transform[M[_], N[_]](t: T11[M], f: M ~> N) = + ( + f(t._1), + f(t._2), + f(t._3), + f(t._4), + f(t._5), + f(t._6), + f(t._7), + f(t._8), + f(t._9), + f(t._10), + f(t._11) + ) + def foldr[M[_], T](t: T11[M], f: (M[_], T) => T, init: T): T = + f( + t._1, + f( + t._2, + f(t._3, f(t._4, f(t._5, f(t._6, f(t._7, f(t._8, f(t._9, f(t._10, f(t._11, init))))))))) + ) + ) + def traverse[M[_], N[_], P[_]](t: T11[M], f: M ~> (N ∙ P)#l)( + implicit np: Applicative[N] + ): N[T11[P]] = { + val g = (Tuple11 + .apply[P[A], P[B], P[C], P[D], P[E], P[F], P[G], P[H], P[I], P[J], P[K]] _).curried + np.apply( + np.apply( + np.apply( + np.apply( + np.apply( + np.apply( + np.apply( + np.apply(np.apply(np.apply(np.map(g, f(t._1)), f(t._2)), f(t._3)), f(t._4)), + f(t._5) + ), + f(t._6) + ), + f(t._7) + ), + f(t._8) + ), + f(t._9) + ), + f(t._10) + ), + f(t._11) + ) + } + } +} diff --git a/internal/util-collection/src/main/scala/sbt/internal/util/Attributes.scala b/internal/util-collection/src/main/scala/sbt/internal/util/Attributes.scala new file mode 100644 index 000000000..0c1d2cf2c --- /dev/null +++ b/internal/util-collection/src/main/scala/sbt/internal/util/Attributes.scala @@ -0,0 +1,278 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util + +import Types._ +import scala.reflect.Manifest +import sbt.util.OptJsonWriter + +// T must be invariant to work properly. +// Because it is sealed and the only instances go through AttributeKey.apply, +// a single AttributeKey instance cannot conform to AttributeKey[T] for different Ts + +/** + * A key in an [[AttributeMap]] that constrains its associated value to be of type `T`. + * The key is uniquely defined by its `label` and type `T`, represented at runtime by `manifest`. + */ +sealed trait AttributeKey[T] { + + /** The runtime evidence for `T`. */ + def manifest: Manifest[T] + + /** The label is the identifier for the key and is camelCase by convention. */ + def label: String + + /** An optional, brief description of the key. */ + def description: Option[String] + + /** + * In environments that support delegation, looking up this key when it has no associated value + * will delegate to the values associated with these keys. + * The delegation proceeds in order the keys are returned here. + */ + def extend: Seq[AttributeKey[_]] + + /** + * Specifies whether this key is a local, anonymous key (`true`) or not (`false`). + * This is typically only used for programmatic, intermediate keys that should not be referenced outside of a specific scope. + */ + def isLocal: Boolean + + /** Identifies the relative importance of a key among other keys.*/ + def rank: Int + + def optJsonWriter: OptJsonWriter[T] + +} + +private[sbt] abstract class SharedAttributeKey[T] extends AttributeKey[T] { + override final def toString = label + override final def hashCode = label.hashCode + override final def equals(o: Any) = + (this eq o.asInstanceOf[AnyRef]) || (o match { + case a: SharedAttributeKey[t] => a.label == this.label && a.manifest == this.manifest + case _ => false + }) + final def isLocal: Boolean = false +} + +object AttributeKey { + def apply[T: Manifest: OptJsonWriter](name: String): AttributeKey[T] = + make(name, None, Nil, Int.MaxValue) + + def apply[T: Manifest: OptJsonWriter](name: String, rank: Int): AttributeKey[T] = + make(name, None, Nil, rank) + + def apply[T: Manifest: OptJsonWriter](name: String, description: String): AttributeKey[T] = + apply(name, description, Nil) + + def apply[T: Manifest: OptJsonWriter]( + name: String, + description: String, + rank: Int + ): AttributeKey[T] = + apply(name, description, Nil, rank) + + def apply[T: Manifest: OptJsonWriter]( + name: String, + description: String, + extend: Seq[AttributeKey[_]] + ): AttributeKey[T] = + apply(name, description, extend, Int.MaxValue) + + def apply[T: Manifest: OptJsonWriter]( + name: String, + description: String, + extend: Seq[AttributeKey[_]], + rank: Int + ): AttributeKey[T] = + make(name, Some(description), extend, rank) + + private[sbt] def copyWithRank[T](a: AttributeKey[T], rank: Int): AttributeKey[T] = + make(a.label, a.description, a.extend, rank)(a.manifest, a.optJsonWriter) + + private[this] def make[T]( + name: String, + description0: Option[String], + extend0: Seq[AttributeKey[_]], + rank0: Int + )(implicit mf: Manifest[T], ojw: OptJsonWriter[T]): AttributeKey[T] = + new SharedAttributeKey[T] { + require( + name.headOption.exists(_.isLower), + s"A named attribute key must start with a lowercase letter: $name" + ) + + def manifest = mf + val label = Util.hyphenToCamel(name) + def description = description0 + def extend = extend0 + def rank = rank0 + def optJsonWriter = ojw + } + + private[sbt] def local[T](implicit mf: Manifest[T], ojw: OptJsonWriter[T]): AttributeKey[T] = + new AttributeKey[T] { + def manifest = mf + def label = LocalLabel + def description = None + def extend = Nil + override def toString = label + def isLocal: Boolean = true + def rank = Int.MaxValue + val optJsonWriter = ojw + } + + private[sbt] final val LocalLabel = "$" + "local" + +} + +/** + * An immutable map where a key is the tuple `(String,T)` for a fixed type `T` and can only be associated with values of type `T`. + * It is therefore possible for this map to contain mappings for keys with the same label but different types. + * Excluding this possibility is the responsibility of the client if desired. + */ +trait AttributeMap { + + /** + * Gets the value of type `T` associated with the key `k`. + * If a key with the same label but different type is defined, this method will fail. + */ + def apply[T](k: AttributeKey[T]): T + + /** + * Gets the value of type `T` associated with the key `k` or `None` if no value is associated. + * If a key with the same label but a different type is defined, this method will return `None`. + */ + def get[T](k: AttributeKey[T]): Option[T] + + /** + * Returns this map without the mapping for `k`. + * This method will not remove a mapping for a key with the same label but a different type. + */ + def remove[T](k: AttributeKey[T]): AttributeMap + + /** + * Returns true if this map contains a mapping for `k`. + * If a key with the same label but a different type is defined in this map, this method will return `false`. + */ + def contains[T](k: AttributeKey[T]): Boolean + + /** + * Adds the mapping `k -> value` to this map, replacing any existing mapping for `k`. + * Any mappings for keys with the same label but different types are unaffected. + */ + def put[T](k: AttributeKey[T], value: T): AttributeMap + + /** All keys with defined mappings. There may be multiple keys with the same `label`, but different types. */ + def keys: Iterable[AttributeKey[_]] + + /** Adds the mappings in `o` to this map, with mappings in `o` taking precedence over existing mappings.*/ + def ++(o: Iterable[AttributeEntry[_]]): AttributeMap + + /** Combines the mappings in `o` with the mappings in this map, with mappings in `o` taking precedence over existing mappings.*/ + def ++(o: AttributeMap): AttributeMap + + /** All mappings in this map. The [[AttributeEntry]] type preserves the typesafety of mappings, although the specific types are unknown.*/ + def entries: Iterable[AttributeEntry[_]] + + /** `true` if there are no mappings in this map, `false` if there are. */ + def isEmpty: Boolean + + /** + * Adds the mapping `k -> opt.get` if opt is Some. + * Otherwise, it returns this map without the mapping for `k`. + */ + private[sbt] def setCond[T](k: AttributeKey[T], opt: Option[T]): AttributeMap +} + +object AttributeMap { + + /** An [[AttributeMap]] without any mappings. */ + val empty: AttributeMap = new BasicAttributeMap(Map.empty) + + /** Constructs an [[AttributeMap]] containing the given `entries`. */ + def apply(entries: Iterable[AttributeEntry[_]]): AttributeMap = empty ++ entries + + /** Constructs an [[AttributeMap]] containing the given `entries`.*/ + def apply(entries: AttributeEntry[_]*): AttributeMap = empty ++ entries + + /** Presents an `AttributeMap` as a natural transformation. */ + implicit def toNatTrans(map: AttributeMap): AttributeKey ~> Id = λ[AttributeKey ~> Id](map(_)) +} + +private class BasicAttributeMap(private val backing: Map[AttributeKey[_], Any]) + extends AttributeMap { + + def isEmpty: Boolean = backing.isEmpty + def apply[T](k: AttributeKey[T]) = backing(k).asInstanceOf[T] + def get[T](k: AttributeKey[T]) = backing.get(k).asInstanceOf[Option[T]] + def remove[T](k: AttributeKey[T]): AttributeMap = new BasicAttributeMap(backing - k) + def contains[T](k: AttributeKey[T]) = backing.contains(k) + + def put[T](k: AttributeKey[T], value: T): AttributeMap = + new BasicAttributeMap(backing.updated(k, value: Any)) + + def keys: Iterable[AttributeKey[_]] = backing.keys + + def ++(o: Iterable[AttributeEntry[_]]): AttributeMap = + new BasicAttributeMap(o.foldLeft(backing)((b, e) => b.updated(e.key, e.value: Any))) + + def ++(o: AttributeMap): AttributeMap = o match { + case bam: BasicAttributeMap => + new BasicAttributeMap(Map(backing.toSeq ++ bam.backing.toSeq: _*)) + case _ => o ++ this + } + + def entries: Iterable[AttributeEntry[_]] = + backing.collect { + case (k: AttributeKey[kt], v) => AttributeEntry(k, v.asInstanceOf[kt]) + } + + private[sbt] def setCond[T](k: AttributeKey[T], opt: Option[T]): AttributeMap = + opt match { + case Some(v) => put(k, v) + case None => remove(k) + } + + override def toString = entries.mkString("(", ", ", ")") +} + +// type inference required less generality +/** A map entry where `key` is constrained to only be associated with a fixed value of type `T`. */ +final case class AttributeEntry[T](key: AttributeKey[T], value: T) { + override def toString = key.label + ": " + value +} + +/** Associates a `metadata` map with `data`. */ +final case class Attributed[D](data: D)(val metadata: AttributeMap) { + + /** Retrieves the associated value of `key` from the metadata. */ + def get[T](key: AttributeKey[T]): Option[T] = metadata.get(key) + + /** Defines a mapping `key -> value` in the metadata. */ + def put[T](key: AttributeKey[T], value: T): Attributed[D] = + Attributed(data)(metadata.put(key, value)) + + /** Transforms the data by applying `f`. */ + def map[T](f: D => T): Attributed[T] = Attributed(f(data))(metadata) + +} + +object Attributed { + + /** Extracts the underlying data from the sequence `in`. */ + def data[T](in: Seq[Attributed[T]]): Seq[T] = in.map(_.data) + + /** Associates empty metadata maps with each entry of `in`.*/ + def blankSeq[T](in: Seq[T]): Seq[Attributed[T]] = in map blank + + /** Associates an empty metadata map with `data`. */ + def blank[T](data: T): Attributed[T] = Attributed(data)(AttributeMap.empty) + +} diff --git a/internal/util-collection/src/main/scala/sbt/internal/util/Classes.scala b/internal/util-collection/src/main/scala/sbt/internal/util/Classes.scala new file mode 100644 index 000000000..2d7c7aeac --- /dev/null +++ b/internal/util-collection/src/main/scala/sbt/internal/util/Classes.scala @@ -0,0 +1,42 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util + +object Classes { + trait Applicative[M[_]] { + def apply[S, T](f: M[S => T], v: M[S]): M[T] + def pure[S](s: => S): M[S] + def map[S, T](f: S => T, v: M[S]): M[T] + } + + trait Selective[M[_]] extends Applicative[M] { + def select[A, B](fab: M[Either[A, B]])(fn: M[A => B]): M[B] + } + + trait Monad[M[_]] extends Applicative[M] { + def flatten[T](m: M[M[T]]): M[T] + } + + implicit val optionMonad: Monad[Option] = new Monad[Option] { + def apply[S, T](f: Option[S => T], v: Option[S]) = (f, v) match { + case (Some(fv), Some(vv)) => Some(fv(vv)) + case _ => None + } + + def pure[S](s: => S) = Some(s) + def map[S, T](f: S => T, v: Option[S]) = v map f + def flatten[T](m: Option[Option[T]]): Option[T] = m.flatten + } + + implicit val listMonad: Monad[List] = new Monad[List] { + def apply[S, T](f: List[S => T], v: List[S]) = for (fv <- f; vv <- v) yield fv(vv) + def pure[S](s: => S) = s :: Nil + def map[S, T](f: S => T, v: List[S]) = v map f + def flatten[T](m: List[List[T]]): List[T] = m.flatten + } +} diff --git a/internal/util-collection/src/main/scala/sbt/internal/util/Dag.scala b/internal/util-collection/src/main/scala/sbt/internal/util/Dag.scala new file mode 100644 index 000000000..f5f584290 --- /dev/null +++ b/internal/util-collection/src/main/scala/sbt/internal/util/Dag.scala @@ -0,0 +1,142 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util + +trait Dag[Node <: Dag[Node]] { self: Node => + + def dependencies: Iterable[Node] + def topologicalSort = Dag.topologicalSort(self)(_.dependencies) +} +object Dag { + import scala.collection.{ mutable, JavaConverters } + import JavaConverters.asScalaSetConverter + + def topologicalSort[T](root: T)(dependencies: T => Iterable[T]): List[T] = + topologicalSort(root :: Nil)(dependencies) + + def topologicalSort[T](nodes: Iterable[T])(dependencies: T => Iterable[T]): List[T] = { + val discovered = new mutable.HashSet[T] + val finished = (new java.util.LinkedHashSet[T]).asScala + + def visitAll(nodes: Iterable[T]) = nodes foreach visit + def visit(node: T): Unit = { + if (!discovered(node)) { + discovered(node) = true; + try { + visitAll(dependencies(node)); + } catch { case c: Cyclic => throw node :: c } + finished += node + () + } else if (!finished(node)) + throw new Cyclic(node) + } + + visitAll(nodes) + + finished.toList + } + + // doesn't check for cycles + def topologicalSortUnchecked[T](node: T)(dependencies: T => Iterable[T]): List[T] = + topologicalSortUnchecked(node :: Nil)(dependencies) + + def topologicalSortUnchecked[T](nodes: Iterable[T])(dependencies: T => Iterable[T]): List[T] = { + val discovered = new mutable.HashSet[T] + var finished: List[T] = Nil + + def visitAll(nodes: Iterable[T]) = nodes foreach visit + def visit(node: T): Unit = { + if (!discovered(node)) { + discovered(node) = true + visitAll(dependencies(node)) + finished ::= node + } + } + + visitAll(nodes); + finished; + } + + final class Cyclic(val value: Any, val all: List[Any], val complete: Boolean) + extends Exception( + "Cyclic reference involving " + + (if (complete) all.mkString("\n ", "\n ", "") else value) + ) { + def this(value: Any) = this(value, value :: Nil, false) + override def toString = getMessage + + def ::(a: Any): Cyclic = + if (complete) + this + else if (a == value) + new Cyclic(value, all, true) + else + new Cyclic(value, a :: all, false) + } + + /** A directed graph with edges labeled positive or negative. */ + private[sbt] trait DirectedSignedGraph[Node] { + + /** + * Directed edge type that tracks the sign and target (head) vertex. + * The sign can be obtained via [[isNegative]] and the target vertex via [[head]]. + */ + type Arrow + + /** List of initial nodes. */ + def nodes: List[Arrow] + + /** Outgoing edges for `n`. */ + def dependencies(n: Node): List[Arrow] + + /** `true` if the edge `a` is "negative", false if it is "positive". */ + def isNegative(a: Arrow): Boolean + + /** The target of the directed edge `a`. */ + def head(a: Arrow): Node + + } + + /** + * Traverses a directed graph defined by `graph` looking for a cycle that includes a "negative" edge. + * The directed edges are weighted by the caller as "positive" or "negative". + * If a cycle containing a "negative" edge is detected, its member edges are returned in order. + * Otherwise, the empty list is returned. + */ + private[sbt] def findNegativeCycle[Node](graph: DirectedSignedGraph[Node]): List[graph.Arrow] = { + import graph._ + val finished = new mutable.HashSet[Node] + val visited = new mutable.HashSet[Node] + + def visit(edges: List[Arrow], stack: List[Arrow]): List[Arrow] = edges match { + case Nil => Nil + case edge :: tail => + val node = head(edge) + if (!visited(node)) { + visited += node + visit(dependencies(node), edge :: stack) match { + case Nil => + finished += node + visit(tail, stack) + case cycle => cycle + } + } else if (!finished(node)) { + // cycle. If a negative edge is involved, it is an error. + val between = edge :: stack.takeWhile(f => head(f) != node) + if (between exists isNegative) + between + else + visit(tail, stack) + } else + visit(tail, stack) + } + + visit(graph.nodes, Nil) + } + +} diff --git a/internal/util-collection/src/main/scala/sbt/internal/util/HList.scala b/internal/util-collection/src/main/scala/sbt/internal/util/HList.scala new file mode 100644 index 000000000..f0922a079 --- /dev/null +++ b/internal/util-collection/src/main/scala/sbt/internal/util/HList.scala @@ -0,0 +1,40 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util + +import Types._ + +/** + * A minimal heterogeneous list type. For background, see + * https://apocalisp.wordpress.com/2010/07/06/type-level-programming-in-scala-part-6a-heterogeneous-list basics/ + */ +sealed trait HList { + type Wrap[M[_]] <: HList +} + +sealed trait HNil extends HList { + type Wrap[M[_]] = HNil + def :+:[G](g: G): G :+: HNil = HCons(g, this) + + override def toString = "HNil" +} + +object HNil extends HNil + +final case class HCons[H, T <: HList](head: H, tail: T) extends HList { + type Wrap[M[_]] = M[H] :+: T#Wrap[M] + def :+:[G](g: G): G :+: H :+: T = HCons(g, this) + + override def toString = head + " :+: " + tail.toString +} + +object HList { + // contains no type information: not even A + implicit def fromList[A](list: Traversable[A]): HList = + list.foldLeft(HNil: HList)((hl, v) => HCons(v, hl)) +} diff --git a/internal/util-collection/src/main/scala/sbt/internal/util/HListFormats.scala b/internal/util-collection/src/main/scala/sbt/internal/util/HListFormats.scala new file mode 100644 index 000000000..4143861b2 --- /dev/null +++ b/internal/util-collection/src/main/scala/sbt/internal/util/HListFormats.scala @@ -0,0 +1,85 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt +package internal +package util + +import sjsonnew._ +import Types.:+: + +trait HListFormats { + implicit val lnilFormat1: JsonFormat[HNil] = forHNil(HNil: HNil) + implicit val lnilFormat2: JsonFormat[HNil.type] = forHNil(HNil) + + private def forHNil[A <: HNil](hnil: A): JsonFormat[A] = new JsonFormat[A] { + def write[J](x: A, builder: Builder[J]): Unit = { + builder.beginArray() + builder.endArray() + } + + def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): A = jsOpt match { + case None => hnil + case Some(js) => unbuilder.beginArray(js); unbuilder.endArray(); hnil + } + } + + implicit def hconsFormat[H, T <: HList]( + implicit hf: JsonFormat[H], + tf: HListJF[T] + ): JsonFormat[H :+: T] = + new JsonFormat[H :+: T] { + def write[J](hcons: H :+: T, builder: Builder[J]) = { + builder.beginArray() + hf.write(hcons.head, builder) + tf.write(hcons.tail, builder) + builder.endArray() + } + + def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]) = jsOpt match { + case None => HCons(hf.read(None, unbuilder), tf.read(None, unbuilder)) + case Some(js) => + unbuilder.beginArray(js) + val hcons = + HCons(hf.read(Some(unbuilder.nextElement), unbuilder), tf.read(Some(js), unbuilder)) + unbuilder.endArray() + hcons + } + } + + trait HListJF[A <: HList] { + def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): A + def write[J](obj: A, builder: Builder[J]): Unit + } + + implicit def hconsHListJF[H, T <: HList]( + implicit hf: JsonFormat[H], + tf: HListJF[T] + ): HListJF[H :+: T] = + new HListJF[H :+: T] { + def write[J](hcons: H :+: T, builder: Builder[J]) = { + hf.write(hcons.head, builder) + tf.write(hcons.tail, builder) + } + + def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]) = jsOpt match { + case None => HCons(hf.read(None, unbuilder), tf.read(None, unbuilder)) + case Some(js) => + HCons(hf.read(Some(unbuilder.nextElement), unbuilder), tf.read(Some(js), unbuilder)) + } + } + + implicit val lnilHListJF1: HListJF[HNil] = hnilHListJF(HNil: HNil) + implicit val lnilHListJF2: HListJF[HNil.type] = hnilHListJF(HNil) + + implicit def hnilHListJF[A <: HNil](hnil: A): HListJF[A] = new HListJF[A] { + def write[J](hcons: A, builder: Builder[J]) = () + def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]) = hnil + } +} + +object HListFormats extends HListFormats diff --git a/internal/util-collection/src/main/scala/sbt/internal/util/IDSet.scala b/internal/util-collection/src/main/scala/sbt/internal/util/IDSet.scala new file mode 100644 index 000000000..b3cacdfe1 --- /dev/null +++ b/internal/util-collection/src/main/scala/sbt/internal/util/IDSet.scala @@ -0,0 +1,60 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util + +import scala.collection.JavaConverters._ + +/** A mutable set interface that uses object identity to test for set membership.*/ +trait IDSet[T] { + def apply(t: T): Boolean + def contains(t: T): Boolean + def +=(t: T): Unit + def ++=(t: Iterable[T]): Unit + def -=(t: T): Boolean + def all: collection.Iterable[T] + def toList: List[T] + def isEmpty: Boolean + def foreach(f: T => Unit): Unit + def process[S](t: T)(ifSeen: S)(ifNew: => S): S +} + +object IDSet { + implicit def toTraversable[T]: IDSet[T] => Traversable[T] = _.all + def apply[T](values: T*): IDSet[T] = fromIterable(values) + + def apply[T](values: Iterable[T]): IDSet[T] = fromIterable(values) + + private def fromIterable[T](values: Iterable[T]): IDSet[T] = { + val s = create[T] + s ++= values + s + } + + def create[T]: IDSet[T] = new IDSet[T] { + private[this] val backing = new java.util.IdentityHashMap[T, AnyRef] + private[this] val Dummy: AnyRef = "" + + def apply(t: T) = contains(t) + def contains(t: T) = backing.containsKey(t) + def foreach(f: T => Unit) = all foreach f + def +=(t: T) = { backing.put(t, Dummy); () } + def ++=(t: Iterable[T]) = t foreach += + def -=(t: T) = if (backing.remove(t) eq null) false else true + def all = backing.keySet.asScala + def toList = all.toList + def isEmpty = backing.isEmpty + + def process[S](t: T)(ifSeen: S)(ifNew: => S) = + if (contains(t)) ifSeen + else { + this += t; ifNew + } + + override def toString = backing.toString + } +} diff --git a/internal/util-collection/src/main/scala/sbt/internal/util/INode.scala b/internal/util-collection/src/main/scala/sbt/internal/util/INode.scala new file mode 100644 index 000000000..3d7c86646 --- /dev/null +++ b/internal/util-collection/src/main/scala/sbt/internal/util/INode.scala @@ -0,0 +1,230 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util + +import java.lang.Runnable +import java.util.concurrent.{ atomic, Executor, LinkedBlockingQueue } +import atomic.{ AtomicBoolean, AtomicInteger } +import Types.{ ConstK, Id } + +object EvaluationState extends Enumeration { + val New, Blocked, Ready, Calling, Evaluated = Value +} + +abstract class EvaluateSettings[ScopeType] { + protected val init: Init[ScopeType] + import init._ + + protected def executor: Executor + protected def compiledSettings: Seq[Compiled[_]] + + import EvaluationState.{ Value => EvaluationState, _ } + + private[this] val complete = new LinkedBlockingQueue[Option[Throwable]] + private[this] val static = PMap.empty[ScopedKey, INode] + private[this] val allScopes: Set[ScopeType] = compiledSettings.map(_.key.scope).toSet + + private[this] def getStatic[T](key: ScopedKey[T]): INode[T] = + static get key getOrElse sys.error("Illegal reference to key " + key) + + private[this] val transform: Initialize ~> INode = λ[Initialize ~> INode] { + case k: Keyed[s, A1$] @unchecked => single(getStatic(k.scopedKey), k.transform) + case a: Apply[k, A1$] @unchecked => + new MixedNode[k, A1$]( + a.alist.transform[Initialize, INode](a.inputs, transform), + a.f, + a.alist + ) + case b: Bind[s, A1$] @unchecked => new BindNode[s, A1$](transform(b.in), x => transform(b.f(x))) + case v: Value[A1$] @unchecked => constant(v.value) + case v: ValidationCapture[A1$] @unchecked => strictConstant(v.key: A1$) + case t: TransformCapture => strictConstant(t.f: A1$) + case o: Optional[s, A1$] @unchecked => + o.a match { + case None => constant(() => o.f(None)) + case Some(i) => single[s, A1$](transform(i), x => o.f(Some(x))) + } + case x if x == StaticScopes => + strictConstant(allScopes.asInstanceOf[A1$]) // can't convince scalac that StaticScopes => T == Set[Scope] + } + + private[this] lazy val roots: Seq[INode[_]] = compiledSettings flatMap { cs => + (cs.settings map { s => + val t = transform(s.init) + static(s.key) = t + t + }): Seq[INode[_]] + } + + private[this] val running = new AtomicInteger + private[this] val cancel = new AtomicBoolean(false) + + def run(implicit delegates: ScopeType => Seq[ScopeType]): Settings[ScopeType] = { + assert(running.get() == 0, "Already running") + startWork() + roots.foreach(_.registerIfNew()) + workComplete() + complete.take() foreach { ex => + cancel.set(true) + throw ex + } + getResults(delegates) + } + + private[this] def getResults(implicit delegates: ScopeType => Seq[ScopeType]) = + static.toTypedSeq.foldLeft(empty) { + case (ss, static.TPair(key, node)) => + if (key.key.isLocal) ss else ss.set(key.scope, key.key, node.get) + } + + private[this] val getValue = λ[INode ~> Id](_.get) + + private[this] def submitEvaluate(node: INode[_]) = submit(node.evaluate()) + + private[this] def submitCallComplete[T](node: BindNode[_, T], value: T) = + submit(node.callComplete(value)) + + private[this] def submit(work: => Unit): Unit = { + startWork() + executor.execute(new Runnable { def run = if (!cancel.get()) run0(work) }) + } + + private[this] def run0(work: => Unit): Unit = { + try { + work + } catch { case e: Throwable => complete.put(Some(e)) } + workComplete() + } + + private[this] def startWork(): Unit = { running.incrementAndGet(); () } + + private[this] def workComplete(): Unit = + if (running.decrementAndGet() == 0) + complete.put(None) + + private[this] sealed abstract class INode[T] { + private[this] var state: EvaluationState = New + private[this] var value: T = _ + private[this] val blocking = new collection.mutable.ListBuffer[INode[_]] + private[this] var blockedOn: Int = 0 + private[this] val calledBy = new collection.mutable.ListBuffer[BindNode[_, T]] + + override def toString = + getClass.getName + " (state=" + state + ",blockedOn=" + blockedOn + ",calledBy=" + calledBy.size + ",blocking=" + blocking.size + "): " + + keyString + + private[this] def keyString = + (static.toSeq.flatMap { + case (key, value) => + if (value eq this) init.showFullKey.show(key) :: Nil else List.empty[String] + }).headOption getOrElse "non-static" + + final def get: T = synchronized { + assert(value != null, toString + " not evaluated") + value + } + + final def doneOrBlock(from: INode[_]): Boolean = synchronized { + val ready = state == Evaluated + if (!ready) { + blocking += from + () + } + registerIfNew() + ready + } + + final def isDone: Boolean = synchronized { state == Evaluated } + final def isNew: Boolean = synchronized { state == New } + final def isCalling: Boolean = synchronized { state == Calling } + final def registerIfNew(): Unit = synchronized { if (state == New) register() } + + private[this] def register(): Unit = { + assert(state == New, "Already registered and: " + toString) + val deps = dependsOn + blockedOn = deps.size - deps.count(_.doneOrBlock(this)) + if (blockedOn == 0) + schedule() + else + state = Blocked + } + + final def schedule(): Unit = synchronized { + assert(state == New || state == Blocked, "Invalid state for schedule() call: " + toString) + state = Ready + submitEvaluate(this) + } + + final def unblocked(): Unit = synchronized { + assert(state == Blocked, "Invalid state for unblocked() call: " + toString) + blockedOn -= 1 + assert(blockedOn >= 0, "Negative blockedOn: " + blockedOn + " for " + toString) + if (blockedOn == 0) schedule() + } + + final def evaluate(): Unit = synchronized { evaluate0() } + + protected final def makeCall(source: BindNode[_, T], target: INode[T]): Unit = { + assert(state == Ready, "Invalid state for call to makeCall: " + toString) + state = Calling + target.call(source) + } + + protected final def setValue(v: T): Unit = { + assert( + state != Evaluated, + "Already evaluated (trying to set value to " + v + "): " + toString + ) + if (v == null) sys.error("Setting value cannot be null: " + keyString) + value = v + state = Evaluated + blocking foreach { _.unblocked() } + blocking.clear() + calledBy foreach { node => + submitCallComplete(node, value) + } + calledBy.clear() + } + + final def call(by: BindNode[_, T]): Unit = synchronized { + registerIfNew() + state match { + case Evaluated => submitCallComplete(by, value) + case _ => + calledBy += by + () + } + } + + protected def dependsOn: Seq[INode[_]] + protected def evaluate0(): Unit + } + + private[this] def strictConstant[T](v: T): INode[T] = constant(() => v) + + private[this] def constant[T](f: () => T): INode[T] = + new MixedNode[ConstK[Unit]#l, T]((), _ => f(), AList.empty) + + private[this] def single[S, T](in: INode[S], f: S => T): INode[T] = + new MixedNode[λ[L[x] => L[S]], T](in, f, AList.single[S]) + + private[this] final class BindNode[S, T](in: INode[S], f: S => INode[T]) extends INode[T] { + protected def dependsOn = in :: Nil + protected def evaluate0(): Unit = makeCall(this, f(in.get)) + def callComplete(value: T): Unit = synchronized { + assert(isCalling, "Invalid state for callComplete(" + value + "): " + toString) + setValue(value) + } + } + + private[this] final class MixedNode[K[L[x]], T](in: K[INode], f: K[Id] => T, alist: AList[K]) + extends INode[T] { + protected def dependsOn = alist.toList(in) + protected def evaluate0(): Unit = setValue(f(alist.transform(in, getValue))) + } +} diff --git a/internal/util-collection/src/main/scala/sbt/internal/util/KList.scala b/internal/util-collection/src/main/scala/sbt/internal/util/KList.scala new file mode 100644 index 000000000..2761d8172 --- /dev/null +++ b/internal/util-collection/src/main/scala/sbt/internal/util/KList.scala @@ -0,0 +1,70 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util + +import Types._ +import Classes.Applicative + +/** A higher-kinded heterogeneous list of elements that share the same type constructor `M[_]`. */ +sealed trait KList[+M[_]] { + type Transform[N[_]] <: KList[N] + + /** Apply the natural transformation `f` to each element. */ + def transform[N[_]](f: M ~> N): Transform[N] + + /** Folds this list using a function that operates on the homogeneous type of the elements of this list. */ + def foldr[B](f: (M[_], B) => B, init: B): B + + /** Applies `f` to the elements of this list in the applicative functor defined by `ap`. */ + def apply[N[x] >: M[x], Z](f: Transform[Id] => Z)(implicit ap: Applicative[N]): N[Z] + + /** Equivalent to `transform(f) . apply(x => x)`, this is the essence of the iterator at the level of natural transformations.*/ + def traverse[N[_], P[_]](f: M ~> (N ∙ P)#l)(implicit np: Applicative[N]): N[Transform[P]] + + /** Discards the heterogeneous type information and constructs a plain List from this KList's elements. */ + def toList: List[M[_]] +} +object KList { + type Aux[+M[_], Transform0[N[_]]] = KList[M] { type Transform[N[_]] = Transform0[N] } +} + +final case class KCons[H, +T <: KList[M], +M[_]](head: M[H], tail: T) extends KList[M] { + final type Transform[N[_]] = KCons[H, tail.Transform[N], N] + + def transform[N[_]](f: M ~> N) = KCons(f(head), tail.transform(f)) + def toList: List[M[_]] = head :: tail.toList + + def apply[N[x] >: M[x], Z](f: Transform[Id] => Z)(implicit ap: Applicative[N]): N[Z] = { + val g = (t: tail.Transform[Id]) => (h: H) => f(KCons[H, tail.Transform[Id], Id](h, t)) + ap.apply(tail.apply[N, H => Z](g), head) + } + + def traverse[N[_], P[_]](f: M ~> (N ∙ P)#l)(implicit np: Applicative[N]): N[Transform[P]] = { + val tt: N[tail.Transform[P]] = tail.traverse[N, P](f) + val g = (t: tail.Transform[P]) => (h: P[H]) => KCons(h, t) + np.apply(np.map(g, tt), f(head)) + } + + def :^:[A, N[x] >: M[x]](h: N[A]) = KCons(h, this) + override def foldr[B](f: (M[_], B) => B, init: B): B = f(head, tail.foldr(f, init)) +} + +sealed abstract class KNil extends KList[NothingK] { + final type Transform[N[_]] = KNil + final def transform[N[_]](f: NothingK ~> N): Transform[N] = KNil + final def foldr[B](f: (NothingK[_], B) => B, init: B): B = init + final def toList = Nil + final def apply[N[x], Z](f: KNil => Z)(implicit ap: Applicative[N]): N[Z] = ap.pure(f(KNil)) + + final def traverse[N[_], P[_]](f: NothingK ~> (N ∙ P)#l)(implicit np: Applicative[N]): N[KNil] = + np.pure(KNil: KNil) +} + +case object KNil extends KNil { + def :^:[M[_], H](h: M[H]): KCons[H, KNil, M] = KCons(h, this) +} diff --git a/internal/util-collection/src/main/scala/sbt/internal/util/PMap.scala b/internal/util-collection/src/main/scala/sbt/internal/util/PMap.scala new file mode 100644 index 000000000..ed90560ee --- /dev/null +++ b/internal/util-collection/src/main/scala/sbt/internal/util/PMap.scala @@ -0,0 +1,129 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util + +import collection.mutable + +trait RMap[K[_], V[_]] { + def apply[T](k: K[T]): V[T] + def get[T](k: K[T]): Option[V[T]] + def contains[T](k: K[T]): Boolean + def toSeq: Seq[(K[_], V[_])] + + def toTypedSeq: Seq[TPair[_]] = toSeq.map { + case (k: K[t], v) => TPair[t](k, v.asInstanceOf[V[t]]) + } + + def keys: Iterable[K[_]] + def values: Iterable[V[_]] + def isEmpty: Boolean + + sealed case class TPair[T](key: K[T], value: V[T]) +} + +trait IMap[K[_], V[_]] extends (K ~> V) with RMap[K, V] { + def put[T](k: K[T], v: V[T]): IMap[K, V] + def remove[T](k: K[T]): IMap[K, V] + def mapValue[T](k: K[T], init: V[T], f: V[T] => V[T]): IMap[K, V] + def mapValues[V2[_]](f: V ~> V2): IMap[K, V2] + def mapSeparate[VL[_], VR[_]](f: V ~> λ[T => Either[VL[T], VR[T]]]): (IMap[K, VL], IMap[K, VR]) +} + +trait PMap[K[_], V[_]] extends (K ~> V) with RMap[K, V] { + def update[T](k: K[T], v: V[T]): Unit + def remove[T](k: K[T]): Option[V[T]] + def getOrUpdate[T](k: K[T], make: => V[T]): V[T] + def mapValue[T](k: K[T], init: V[T], f: V[T] => V[T]): V[T] +} + +object PMap { + implicit def toFunction[K[_], V[_]](map: PMap[K, V]): K[_] => V[_] = k => map(k) + def empty[K[_], V[_]]: PMap[K, V] = new DelegatingPMap[K, V](new mutable.HashMap) +} + +object IMap { + + /** + * Only suitable for K that is invariant in its type parameter. + * Option and List keys are not suitable, for example, + * because None <:< Option[String] and None <: Option[Int]. + */ + def empty[K[_], V[_]]: IMap[K, V] = new IMap0[K, V](Map.empty) + + private[sbt] def fromJMap[K[_], V[_]](map: java.util.Map[K[_], V[_]]): IMap[K, V] = + new IMap0[K, V](new WrappedMap(map)) + + private[sbt] class IMap0[K[_], V[_]](val backing: Map[K[_], V[_]]) + extends AbstractRMap[K, V] + with IMap[K, V] { + def get[T](k: K[T]): Option[V[T]] = (backing get k).asInstanceOf[Option[V[T]]] + def put[T](k: K[T], v: V[T]) = new IMap0[K, V](backing.updated(k, v)) + def remove[T](k: K[T]) = new IMap0[K, V](backing - k) + + def mapValue[T](k: K[T], init: V[T], f: V[T] => V[T]) = + put(k, f(this get k getOrElse init)) + + def mapValues[V2[_]](f: V ~> V2) = + new IMap0[K, V2](Map(backing.iterator.map { case (k, v) => k -> f(v) }.toArray: _*)) + + def mapSeparate[VL[_], VR[_]](f: V ~> λ[T => Either[VL[T], VR[T]]]) = { + val left = new java.util.concurrent.ConcurrentHashMap[K[_], VL[_]] + val right = new java.util.concurrent.ConcurrentHashMap[K[_], VR[_]] + Par(backing.toVector).foreach { + case (k, v) => + f(v) match { + case Left(l) => left.put(k, l) + case Right(r) => right.put(k, r) + } + } + (new IMap0[K, VL](new WrappedMap(left)), new IMap0[K, VR](new WrappedMap(right))) + } + + def toSeq = backing.toSeq + def keys = backing.keys + def values = backing.values + def isEmpty = backing.isEmpty + + override def toString = backing.toString + } +} + +abstract class AbstractRMap[K[_], V[_]] extends RMap[K, V] { + def apply[T](k: K[T]): V[T] = get(k).get + def contains[T](k: K[T]): Boolean = get(k).isDefined +} + +/** + * Only suitable for K that is invariant in its type parameter. + * Option and List keys are not suitable, for example, + * because None <:< Option[String] and None <: Option[Int]. + */ +class DelegatingPMap[K[_], V[_]](backing: mutable.Map[K[_], V[_]]) + extends AbstractRMap[K, V] + with PMap[K, V] { + def get[T](k: K[T]): Option[V[T]] = cast[T](backing.get(k)) + def update[T](k: K[T], v: V[T]): Unit = { backing(k) = v } + def remove[T](k: K[T]) = cast(backing.remove(k)) + def getOrUpdate[T](k: K[T], make: => V[T]) = cast[T](backing.getOrElseUpdate(k, make)) + + def mapValue[T](k: K[T], init: V[T], f: V[T] => V[T]): V[T] = { + val v = f(this get k getOrElse init) + update(k, v) + v + } + + def toSeq = backing.toSeq + def keys = backing.keys + def values = backing.values + def isEmpty = backing.isEmpty + + private[this] def cast[T](v: V[_]): V[T] = v.asInstanceOf[V[T]] + private[this] def cast[T](o: Option[V[_]]): Option[V[T]] = o map cast[T] + + override def toString = backing.toString +} diff --git a/internal/util-collection/src/main/scala/sbt/internal/util/Settings.scala b/internal/util-collection/src/main/scala/sbt/internal/util/Settings.scala new file mode 100644 index 000000000..f4110acc8 --- /dev/null +++ b/internal/util-collection/src/main/scala/sbt/internal/util/Settings.scala @@ -0,0 +1,966 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util + +import Types._ +import sbt.util.Show +import Util.{ nil, nilSeq } + +sealed trait Settings[ScopeType] { + def data: Map[ScopeType, AttributeMap] + def keys(scope: ScopeType): Set[AttributeKey[_]] + def scopes: Set[ScopeType] + def definingScope(scope: ScopeType, key: AttributeKey[_]): Option[ScopeType] + def allKeys[T](f: (ScopeType, AttributeKey[_]) => T): Seq[T] + def get[T](scope: ScopeType, key: AttributeKey[T]): Option[T] + def getDirect[T](scope: ScopeType, key: AttributeKey[T]): Option[T] + def set[T](scope: ScopeType, key: AttributeKey[T], value: T): Settings[ScopeType] +} + +private final class Settings0[ScopeType]( + val data: Map[ScopeType, AttributeMap], + val delegates: ScopeType => Seq[ScopeType] +) extends Settings[ScopeType] { + + def scopes: Set[ScopeType] = data.keySet + def keys(scope: ScopeType) = data(scope).keys.toSet + + def allKeys[T](f: (ScopeType, AttributeKey[_]) => T): Seq[T] = + data.flatMap { case (scope, map) => map.keys.map(k => f(scope, k)) }.toSeq + + def get[T](scope: ScopeType, key: AttributeKey[T]): Option[T] = + delegates(scope).flatMap(sc => getDirect(sc, key)).headOption + + def definingScope(scope: ScopeType, key: AttributeKey[_]): Option[ScopeType] = + delegates(scope).find(sc => getDirect(sc, key).isDefined) + + def getDirect[T](scope: ScopeType, key: AttributeKey[T]): Option[T] = + (data get scope).flatMap(_ get key) + + def set[T](scope: ScopeType, key: AttributeKey[T], value: T): Settings[ScopeType] = { + val map = data getOrElse (scope, AttributeMap.empty) + val newData = data.updated(scope, map.put(key, value)) + new Settings0(newData, delegates) + } +} + +// delegates should contain the input Scope as the first entry +// this trait is intended to be mixed into an object +trait Init[ScopeType] { + + /** The Show instance used when a detailed String needs to be generated. + * It is typically used when no context is available. + */ + def showFullKey: Show[ScopedKey[_]] + + sealed case class ScopedKey[T](scope: ScopeType, key: AttributeKey[T]) + extends KeyedInitialize[T] { + def scopedKey = this + } + + type SettingSeq[T] = Seq[Setting[T]] + type ScopedMap = IMap[ScopedKey, SettingSeq] + type CompiledMap = Map[ScopedKey[_], Compiled[_]] + type MapScoped = ScopedKey ~> ScopedKey + type ValidatedRef[T] = Either[Undefined, ScopedKey[T]] + type ValidatedInit[T] = Either[Seq[Undefined], Initialize[T]] + type ValidateRef = ScopedKey ~> ValidatedRef + type ScopeLocal = ScopedKey[_] => Seq[Setting[_]] + type MapConstant = ScopedKey ~> Option + + private[sbt] abstract class ValidateKeyRef { + def apply[T](key: ScopedKey[T], selfRefOk: Boolean): ValidatedRef[T] + } + + /** + * The result of this initialization is the composition of applied transformations. + * This can be useful when dealing with dynamic Initialize values. + */ + lazy val capturedTransformations: Initialize[Initialize ~> Initialize] = + new TransformCapture(idK[Initialize]) + + def setting[T]( + key: ScopedKey[T], + init: Initialize[T], + pos: SourcePosition = NoPosition + ): Setting[T] = new Setting[T](key, init, pos) + + def valueStrict[T](value: T): Initialize[T] = pure(() => value) + def value[T](value: => T): Initialize[T] = pure(value _) + def pure[T](value: () => T): Initialize[T] = new Value(value) + def optional[T, U](i: Initialize[T])(f: Option[T] => U): Initialize[U] = new Optional(Some(i), f) + + def update[T](key: ScopedKey[T])(f: T => T): Setting[T] = + setting[T](key, map(key)(f), NoPosition) + + def bind[S, T](in: Initialize[S])(f: S => Initialize[T]): Initialize[T] = new Bind(f, in) + + def map[S, T](in: Initialize[S])(f: S => T): Initialize[T] = + new Apply[λ[L[x] => L[S]], T](f, in, AList.single[S]) + + def app[K[L[x]], T](inputs: K[Initialize])(f: K[Id] => T)( + implicit alist: AList[K] + ): Initialize[T] = new Apply[K, T](f, inputs, alist) + + def uniform[S, T](inputs: Seq[Initialize[S]])(f: Seq[S] => T): Initialize[T] = + new Apply[λ[L[x] => List[L[S]]], T](f, inputs.toList, AList.seq[S]) + + /** + * The result of this initialization is the validated `key`. + * No dependency is introduced on `key`. If `selfRefOk` is true, validation will not fail if the key is referenced by a definition of `key`. + * That is, key := f(validated(key).value) is allowed only if `selfRefOk == true`. + */ + private[sbt] final def validated[T]( + key: ScopedKey[T], + selfRefOk: Boolean + ): ValidationCapture[T] = + new ValidationCapture(key, selfRefOk) + + /** + * Constructs a derived setting that will be automatically defined in every scope where one of its dependencies + * is explicitly defined and the where the scope matches `filter`. + * A setting initialized with dynamic dependencies is only allowed if `allowDynamic` is true. + * Only the static dependencies are tracked, however. Dependencies on previous values do not introduce a derived setting either. + */ + final def derive[T]( + s: Setting[T], + allowDynamic: Boolean = false, + filter: ScopeType => Boolean = const(true), + trigger: AttributeKey[_] => Boolean = const(true), + default: Boolean = false + ): Setting[T] = { + deriveAllowed(s, allowDynamic) foreach sys.error + val d = new DerivedSetting[T](s.key, s.init, s.pos, filter, trigger) + if (default) d.default() else d + } + + def deriveAllowed[T](s: Setting[T], allowDynamic: Boolean): Option[String] = s.init match { + case _: Bind[_, _] if !allowDynamic => Some("Cannot derive from dynamic dependencies.") + case _ => None + } + + // id is used for equality + private[sbt] final def defaultSetting[T](s: Setting[T]): Setting[T] = s.default() + + private[sbt] def defaultSettings(ss: Seq[Setting[_]]): Seq[Setting[_]] = + ss.map(s => defaultSetting(s)) + + private[this] final val nextID = new java.util.concurrent.atomic.AtomicLong + private[this] final def nextDefaultID(): Long = nextID.incrementAndGet() + + def empty(implicit delegates: ScopeType => Seq[ScopeType]): Settings[ScopeType] = + new Settings0(Map.empty, delegates) + + def asTransform(s: Settings[ScopeType]): ScopedKey ~> Id = λ[ScopedKey ~> Id](k => getValue(s, k)) + + def getValue[T](s: Settings[ScopeType], k: ScopedKey[T]) = + s.get(k.scope, k.key) getOrElse (throw new InvalidReference(k)) + + def asFunction[T](s: Settings[ScopeType]): ScopedKey[T] => T = k => getValue(s, k) + + def mapScope(f: ScopeType => ScopeType): MapScoped = new MapScoped { + def apply[T](k: ScopedKey[T]): ScopedKey[T] = k.copy(scope = f(k.scope)) + } + + private final class InvalidReference(val key: ScopedKey[_]) + extends RuntimeException( + "Internal settings error: invalid reference to " + showFullKey.show(key) + ) + + private[this] def applyDefaults(ss: Seq[Setting[_]]): Seq[Setting[_]] = { + val result = new java.util.LinkedHashSet[Setting[_]] + val others = new java.util.ArrayList[Setting[_]] + ss.foreach { + case u: DefaultSetting[_] => result.add(u) + case r => others.add(r) + } + result.addAll(others) + import scala.collection.JavaConverters._ + result.asScala.toVector + } + + def compiled(init: Seq[Setting[_]], actual: Boolean = true)( + implicit delegates: ScopeType => Seq[ScopeType], + scopeLocal: ScopeLocal, + display: Show[ScopedKey[_]] + ): CompiledMap = { + val initDefaults = applyDefaults(init) + // inject derived settings into scopes where their dependencies are directly defined + // and prepend per-scope settings + val derived = deriveAndLocal(initDefaults, mkDelegates(delegates)) + // group by Scope/Key, dropping dead initializations + val sMap: ScopedMap = grouped(derived) + // delegate references to undefined values according to 'delegates' + val dMap: ScopedMap = + if (actual) delegate(sMap)(delegates, display) else sMap + // merge Seq[Setting[_]] into Compiled + compile(dMap) + } + + @deprecated("Use makeWithCompiledMap", "1.4.0") + def make(init: Seq[Setting[_]])( + implicit delegates: ScopeType => Seq[ScopeType], + scopeLocal: ScopeLocal, + display: Show[ScopedKey[_]] + ): Settings[ScopeType] = makeWithCompiledMap(init)._2 + + def makeWithCompiledMap(init: Seq[Setting[_]])( + implicit delegates: ScopeType => Seq[ScopeType], + scopeLocal: ScopeLocal, + display: Show[ScopedKey[_]] + ): (CompiledMap, Settings[ScopeType]) = { + val cMap = compiled(init)(delegates, scopeLocal, display) + // order the initializations. cyclic references are detected here. + val ordered: Seq[Compiled[_]] = sort(cMap) + // evaluation: apply the initializations. + try { + (cMap, applyInits(ordered)) + } catch { + case rru: RuntimeUndefined => + throw Uninitialized(cMap.keys.toSeq, delegates, rru.undefined, true) + } + } + + def sort(cMap: CompiledMap): Seq[Compiled[_]] = + Dag.topologicalSort(cMap.values)(_.dependencies.map(cMap)) + + def compile(sMap: ScopedMap): CompiledMap = sMap match { + case m: IMap.IMap0[ScopedKey, SettingSeq] @unchecked => + Par(m.backing.toVector) + .map { + case (k, ss) => + val deps = ss.flatMap(_.dependencies).toSet + ( + k, + new Compiled(k.asInstanceOf[ScopedKey[Any]], deps, ss.asInstanceOf[SettingSeq[Any]]) + ) + } + .toVector + .toMap + case _ => + sMap.toTypedSeq.map { + case sMap.TPair(k, ss) => + val deps = ss.flatMap(_.dependencies) + (k, new Compiled(k, deps, ss)) + }.toMap + } + + def grouped(init: Seq[Setting[_]]): ScopedMap = { + val result = new java.util.HashMap[ScopedKey[_], Seq[Setting[_]]] + init.foreach { s => + result.putIfAbsent(s.key, Vector(s)) match { + case null => + case ss => result.put(s.key, if (s.definitive) Vector(s) else ss :+ s) + } + } + IMap.fromJMap[ScopedKey, SettingSeq]( + result.asInstanceOf[java.util.Map[ScopedKey[_], SettingSeq[_]]] + ) + } + + def add[T](m: ScopedMap, s: Setting[T]): ScopedMap = + m.mapValue[T](s.key, Vector.empty[Setting[T]], ss => append(ss, s)) + + def append[T](ss: Seq[Setting[T]], s: Setting[T]): Seq[Setting[T]] = + if (s.definitive) Vector(s) else ss :+ s + + def addLocal(init: Seq[Setting[_]])(implicit scopeLocal: ScopeLocal): Seq[Setting[_]] = + Par(init).map(_.dependencies flatMap scopeLocal).toVector.flatten ++ init + + def delegate(sMap: ScopedMap)( + implicit delegates: ScopeType => Seq[ScopeType], + display: Show[ScopedKey[_]] + ): ScopedMap = { + def refMap(ref: Setting[_], isFirst: Boolean) = new ValidateKeyRef { + def apply[T](k: ScopedKey[T], selfRefOk: Boolean) = + delegateForKey(sMap, k, delegates(k.scope), ref, selfRefOk || !isFirst) + } + + import scala.collection.JavaConverters._ + val undefined = new java.util.ArrayList[Undefined] + val result = new java.util.concurrent.ConcurrentHashMap[ScopedKey[_], Any] + val backing = sMap.toSeq + Par(backing).foreach { + case (key, settings) => + val valid = new java.util.ArrayList[Setting[_]] + val undefs = new java.util.ArrayList[Undefined] + def validate(s: Setting[_], first: Boolean): Unit = { + s.validateKeyReferenced(refMap(s, first)) match { + case Right(v) => valid.add(v); () + case Left(us) => us.foreach(u => undefs.add(u)) + } + } + settings.headOption match { + case Some(s) => + validate(s, true) + settings.tail.foreach(validate(_, false)) + case _ => + } + if (undefs.isEmpty) result.put(key, valid.asScala.toVector) + else undefined.addAll(undefs) + } + + if (undefined.isEmpty) + IMap.fromJMap[ScopedKey, SettingSeq]( + result.asInstanceOf[java.util.Map[ScopedKey[_], SettingSeq[_]]] + ) + else + throw Uninitialized(sMap.keys.toSeq, delegates, undefined.asScala.toList, false) + } + + private[this] def delegateForKey[T]( + sMap: ScopedMap, + k: ScopedKey[T], + scopes: Seq[ScopeType], + ref: Setting[_], + selfRefOk: Boolean + ): Either[Undefined, ScopedKey[T]] = { + val skeys = scopes.iterator.map(x => ScopedKey(x, k.key)) + val definedAt = skeys.find(sk => (selfRefOk || ref.key != sk) && (sMap contains sk)) + definedAt.toRight(Undefined(ref, k)) + } + + private[this] def applyInits(ordered: Seq[Compiled[_]])( + implicit delegates: ScopeType => Seq[ScopeType] + ): Settings[ScopeType] = { + val x = + java.util.concurrent.Executors.newFixedThreadPool(Runtime.getRuntime.availableProcessors) + try { + val eval: EvaluateSettings[ScopeType] = new EvaluateSettings[ScopeType] { + override val init: Init.this.type = Init.this + def compiledSettings = ordered + def executor = x + } + eval.run + } finally { + x.shutdown() + } + } + + def showUndefined( + u: Undefined, + validKeys: Seq[ScopedKey[_]], + delegates: ScopeType => Seq[ScopeType] + )( + implicit display: Show[ScopedKey[_]] + ): String = { + val guessed = guessIntendedScope(validKeys, delegates, u.referencedKey) + val derived = u.defining.isDerived + val refString = display.show(u.defining.key) + val sourceString = if (derived) "" else parenPosString(u.defining) + val guessedString = + if (derived) "" + else guessed.map(g => "\n Did you mean " + display.show(g) + " ?").toList.mkString + val derivedString = + if (derived) ", which is a derived setting that needs this key to be defined in this scope." + else "" + display.show(u.referencedKey) + " from " + refString + sourceString + derivedString + guessedString + } + + private[this] def parenPosString(s: Setting[_]): String = + s.positionString match { case None => ""; case Some(s) => " (" + s + ")" } + + def guessIntendedScope( + validKeys: Seq[ScopedKey[_]], + delegates: ScopeType => Seq[ScopeType], + key: ScopedKey[_] + ): Option[ScopedKey[_]] = { + val distances = validKeys.flatMap { validKey => + refinedDistance(delegates, validKey, key).map(dist => (dist, validKey)) + } + distances.sortBy(_._1).map(_._2).headOption + } + + def refinedDistance( + delegates: ScopeType => Seq[ScopeType], + a: ScopedKey[_], + b: ScopedKey[_] + ): Option[Int] = + if (a.key != b.key || a == b) None + else { + val dist = delegates(a.scope).indexOf(b.scope) + if (dist < 0) None else Some(dist) + } + + final class Uninitialized(val undefined: Seq[Undefined], override val toString: String) + extends Exception(toString) + + final class Undefined private[sbt] (val defining: Setting[_], val referencedKey: ScopedKey[_]) + + final class RuntimeUndefined(val undefined: Seq[Undefined]) + extends RuntimeException("References to undefined settings at runtime.") { + override def getMessage = + super.getMessage + undefined.map { u => + "\n" + u.referencedKey + " referenced from " + u.defining + }.mkString + } + + def Undefined(defining: Setting[_], referencedKey: ScopedKey[_]): Undefined = + new Undefined(defining, referencedKey) + + def Uninitialized( + validKeys: Seq[ScopedKey[_]], + delegates: ScopeType => Seq[ScopeType], + keys: Seq[Undefined], + runtime: Boolean + )(implicit display: Show[ScopedKey[_]]): Uninitialized = { + assert(keys.nonEmpty) + val suffix = if (keys.length > 1) "s" else "" + val prefix = if (runtime) "Runtime reference" else "Reference" + val keysString = + keys.map(u => showUndefined(u, validKeys, delegates)).mkString("\n\n ", "\n\n ", "") + new Uninitialized( + keys, + prefix + suffix + " to undefined setting" + suffix + ": " + keysString + "\n " + ) + } + + final class Compiled[T]( + val key: ScopedKey[T], + val dependencies: Iterable[ScopedKey[_]], + val settings: Seq[Setting[T]] + ) { + override def toString = showFullKey.show(key) + } + + final class Flattened(val key: ScopedKey[_], val dependencies: Iterable[ScopedKey[_]]) + + def flattenLocals(compiled: CompiledMap): Map[ScopedKey[_], Flattened] = { + val locals = compiled flatMap { + case (key, comp) => + if (key.key.isLocal) Seq(comp) + else nilSeq[Compiled[_]] + } + val ordered = Dag.topologicalSort(locals)( + _.dependencies.flatMap( + dep => + if (dep.key.isLocal) Seq[Compiled[_]](compiled(dep)) + else nilSeq[Compiled[_]] + ) + ) + def flatten( + cmap: Map[ScopedKey[_], Flattened], + key: ScopedKey[_], + deps: Iterable[ScopedKey[_]] + ): Flattened = + new Flattened( + key, + deps.flatMap( + dep => if (dep.key.isLocal) cmap(dep).dependencies else Seq[ScopedKey[_]](dep).toIterable + ) + ) + + val empty = Map.empty[ScopedKey[_], Flattened] + + val flattenedLocals = ordered.foldLeft(empty) { (cmap, c) => + cmap.updated(c.key, flatten(cmap, c.key, c.dependencies)) + } + + compiled flatMap { + case (key, comp) => + if (key.key.isLocal) nilSeq[(ScopedKey[_], Flattened)] + else + Seq[(ScopedKey[_], Flattened)]((key, flatten(flattenedLocals, key, comp.dependencies))) + } + } + + def definedAtString(settings: Seq[Setting[_]]): String = { + val posDefined = settings.flatMap(_.positionString.toList) + if (posDefined.nonEmpty) { + val header = + if (posDefined.size == settings.size) "defined at:" + else + "some of the defining occurrences:" + header + (posDefined.distinct mkString ("\n\t", "\n\t", "\n")) + } else "" + } + + /** + * The intersect method was calling Seq.contains which is very slow compared + * to converting the Seq to a Set and calling contains on the Set. This + * private trait abstracts out the two ways that Seq[ScopeType] was actually + * used, `contains` and `exists`. In mkDelegates, we can create and cache + * instances of Delegates so that we don't have to repeatedly convert the + * same Seq to Set. On a 2020 16" macbook pro, creating the compiled map + * for the sbt project is roughly 2 seconds faster after this change + * (about 3.5 seconds before compared to about 1.5 seconds after) + * + */ + private trait Delegates { + def contains(s: ScopeType): Boolean + def exists(f: ScopeType => Boolean): Boolean + } + private[this] def mkDelegates(delegates: ScopeType => Seq[ScopeType]): ScopeType => Delegates = { + val delegateMap = new java.util.concurrent.ConcurrentHashMap[ScopeType, Delegates] + s => + delegateMap.get(s) match { + case null => + val seq = delegates(s) + val set = seq.toSet + val d = new Delegates { + override def contains(s: ScopeType): Boolean = set.contains(s) + override def exists(f: ScopeType => Boolean): Boolean = seq.exists(f) + } + delegateMap.put(s, d) + d + case d => d + } + } + + /** + * Intersects two scopes, returning the more specific one if they intersect, or None otherwise. + */ + private[sbt] def intersect(s1: ScopeType, s2: ScopeType)( + implicit delegates: ScopeType => Seq[ScopeType] + ): Option[ScopeType] = intersectDelegates(s1, s2, mkDelegates(delegates)) + + /** + * Intersects two scopes, returning the more specific one if they intersect, or None otherwise. + */ + private def intersectDelegates( + s1: ScopeType, + s2: ScopeType, + delegates: ScopeType => Delegates + ): Option[ScopeType] = + if (delegates(s1).contains(s2)) Some(s1) // s1 is more specific + else if (delegates(s2).contains(s1)) Some(s2) // s2 is more specific + else None + + private[this] def deriveAndLocal(init: Seq[Setting[_]], delegates: ScopeType => Delegates)( + implicit scopeLocal: ScopeLocal + ): Seq[Setting[_]] = { + import collection.mutable + + final class Derived(val setting: DerivedSetting[_]) { + val dependencies = setting.dependencies.map(_.key) + def triggeredBy = dependencies.filter(setting.trigger) + val inScopes = new mutable.HashSet[ScopeType] + val outputs = new mutable.ListBuffer[Setting[_]] + } + + final class Deriveds(val key: AttributeKey[_], val settings: mutable.ListBuffer[Derived]) { + def dependencies = settings.flatMap(_.dependencies) + // This is mainly for use in the cyclic reference error message + override def toString = + s"Derived settings for ${key.label}, ${definedAtString(settings.map(_.setting).toSeq)}" + } + + // separate `derived` settings from normal settings (`defs`) + val (derived, rawDefs) = + Util.separate[Setting[_], Derived, Setting[_]](init) { + case d: DerivedSetting[_] => Left(new Derived(d)); case s => Right(s) + } + val defs = addLocal(rawDefs)(scopeLocal) + + // group derived settings by the key they define + val derivsByDef = new mutable.HashMap[AttributeKey[_], Deriveds] + for (s <- derived) { + val key = s.setting.key.key + derivsByDef.getOrElseUpdate(key, new Deriveds(key, new mutable.ListBuffer)).settings += s + } + + // index derived settings by triggering key. This maps a key to the list of settings potentially derived from it. + val derivedBy = new mutable.HashMap[AttributeKey[_], mutable.ListBuffer[Derived]] + for (s <- derived; d <- s.triggeredBy) + derivedBy.getOrElseUpdate(d, new mutable.ListBuffer) += s + + // Map a DerivedSetting[_] to the `Derived` struct wrapping it. Used to ultimately replace a DerivedSetting with + // the `Setting`s that were actually derived from it: `Derived.outputs` + val derivedToStruct: Map[DerivedSetting[_], Derived] = (derived map { s => + s.setting -> s + }).toMap + + // set of defined scoped keys, used to ensure a derived setting is only added if all dependencies are present + val defined = new mutable.HashSet[ScopedKey[_]] + def addDefs(ss: Seq[Setting[_]]): Unit = { for (s <- ss) defined += s.key } + addDefs(defs) + + // true iff the scoped key is in `defined`, taking delegation into account + def isDefined(key: AttributeKey[_], scope: ScopeType) = + delegates(scope).exists(s => defined.contains(ScopedKey(s, key))) + + // true iff all dependencies of derived setting `d` have a value (potentially via delegation) in `scope` + def allDepsDefined(d: Derived, scope: ScopeType, local: Set[AttributeKey[_]]): Boolean = + d.dependencies.forall(dep => local(dep) || isDefined(dep, scope)) + + // Returns the list of injectable derived settings and their local settings for `sk`. + // The settings are to be injected under `outputScope` = whichever scope is more specific of: + // * the dependency's (`sk`) scope + // * the DerivedSetting's scope in which it has been declared, `definingScope` + // provided that these two scopes intersect. + // A derived setting is injectable if: + // 1. it has not been previously injected into outputScope + // 2. it applies to outputScope (as determined by its `filter`) + // 3. all of its dependencies are defined for outputScope (allowing for delegation) + // This needs to handle local settings because a derived setting wouldn't be injected if it's local setting didn't exist yet. + val deriveFor = (sk: ScopedKey[_]) => { + val derivedForKey: List[Derived] = derivedBy.get(sk.key).toList.flatten + val scope = sk.scope + def localAndDerived(d: Derived): Seq[Setting[_]] = { + def definingScope = d.setting.key.scope + val outputScope = intersectDelegates(scope, definingScope, delegates) + outputScope collect { + case s if !d.inScopes.contains(s) && d.setting.filter(s) => + val local = d.dependencies.flatMap(dep => scopeLocal(ScopedKey(s, dep))) + if (allDepsDefined(d, s, local.map(_.key.key).toSet)) { + d.inScopes.add(s) + val out = local :+ d.setting.setScope(s) + d.outputs ++= out + out + } else + nilSeq + } getOrElse nilSeq + } + derivedForKey.flatMap(localAndDerived) + } + + val processed = new mutable.HashSet[ScopedKey[_]] + + // derives settings, transitively so that a derived setting can trigger another + def process(rem: List[Setting[_]]): Unit = rem match { + case s :: ss => + val sk = s.key + val ds = if (processed.add(sk)) deriveFor(sk) else nil + addDefs(ds) + process(ds ::: ss) + case Nil => + } + process(defs.toList) + + // Take all the original defs and DerivedSettings along with locals, replace each DerivedSetting with the actual + // settings that were derived. + val allDefs = addLocal(init)(scopeLocal) + allDefs.flatMap { + case d: DerivedSetting[_] => (derivedToStruct get d map (_.outputs)).toSeq.flatten + case s => s :: nil + } + } + + /** Abstractly defines a value of type `T`. + * + * Specifically it defines a node in a task graph, + * where the `dependencies` represents dependent nodes, + * and `evaluate` represents the calculation based on the existing body of knowledge. + * + * @tparam T the type of the value this defines. + */ + sealed trait Initialize[T] { + def dependencies: Seq[ScopedKey[_]] + def apply[S](g: T => S): Initialize[S] + + private[sbt] def mapReferenced(g: MapScoped): Initialize[T] + private[sbt] def mapConstant(g: MapConstant): Initialize[T] + + private[sbt] def validateReferenced(g: ValidateRef): ValidatedInit[T] = + validateKeyReferenced(new ValidateKeyRef { + def apply[B](key: ScopedKey[B], selfRefOk: Boolean) = g(key) + }) + + private[sbt] def validateKeyReferenced(g: ValidateKeyRef): ValidatedInit[T] + + def evaluate(map: Settings[ScopeType]): T + def zip[S](o: Initialize[S]): Initialize[(T, S)] = zipTupled(o)(idFun) + def zipWith[S, U](o: Initialize[S])(f: (T, S) => U): Initialize[U] = zipTupled(o)(f.tupled) + private[this] def zipTupled[S, U](o: Initialize[S])(f: ((T, S)) => U): Initialize[U] = + new Apply[λ[L[x] => (L[T], L[S])], U](f, (this, o), AList.tuple2[T, S]) + + /** A fold on the static attributes of this and nested Initializes. */ + private[sbt] def processAttributes[S](init: S)(f: (S, AttributeMap) => S): S + } + + object Initialize { + implicit def joinInitialize[T](s: Seq[Initialize[T]]): JoinInitSeq[T] = new JoinInitSeq(s) + + final class JoinInitSeq[T](s: Seq[Initialize[T]]) { + def joinWith[S](f: Seq[T] => S): Initialize[S] = uniform(s)(f) + def join: Initialize[Seq[T]] = uniform(s)(idFun) + } + + def join[T](inits: Seq[Initialize[T]]): Initialize[Seq[T]] = uniform(inits)(idFun) + + def joinAny[M[_]](inits: Seq[Initialize[M[T]] forSome { type T }]): Initialize[Seq[M[_]]] = + join(inits.asInstanceOf[Seq[Initialize[M[_]]]]) + } + + object SettingsDefinition { + implicit def unwrapSettingsDefinition(d: SettingsDefinition): Seq[Setting[_]] = d.settings + implicit def wrapSettingsDefinition(ss: Seq[Setting[_]]): SettingsDefinition = + new SettingList(ss) + } + + sealed trait SettingsDefinition { + def settings: Seq[Setting[_]] + } + + final class SettingList(val settings: Seq[Setting[_]]) extends SettingsDefinition + + sealed class Setting[T] private[Init] ( + val key: ScopedKey[T], + val init: Initialize[T], + val pos: SourcePosition + ) extends SettingsDefinition { + def settings = this :: Nil + def definitive: Boolean = !init.dependencies.contains(key) + def dependencies: Seq[ScopedKey[_]] = + remove(init.dependencies.asInstanceOf[Seq[ScopedKey[T]]], key) + def mapReferenced(g: MapScoped): Setting[T] = make(key, init mapReferenced g, pos) + + def validateReferenced(g: ValidateRef): Either[Seq[Undefined], Setting[T]] = + (init validateReferenced g).right.map(newI => make(key, newI, pos)) + + private[sbt] def validateKeyReferenced(g: ValidateKeyRef): Either[Seq[Undefined], Setting[T]] = + (init validateKeyReferenced g).right.map(newI => make(key, newI, pos)) + + def mapKey(g: MapScoped): Setting[T] = make(g(key), init, pos) + def mapInit(f: (ScopedKey[T], T) => T): Setting[T] = make(key, init(t => f(key, t)), pos) + def mapConstant(g: MapConstant): Setting[T] = make(key, init mapConstant g, pos) + def withPos(pos: SourcePosition) = make(key, init, pos) + + def positionString: Option[String] = pos match { + case pos: FilePosition => Some(pos.path + ":" + pos.startLine) + case NoPosition => None + } + + private[sbt] def mapInitialize(f: Initialize[T] => Initialize[T]): Setting[T] = + make(key, f(init), pos) + + override def toString = "setting(" + key + ") at " + pos + + protected[this] def make[B]( + key: ScopedKey[B], + init: Initialize[B], + pos: SourcePosition + ): Setting[B] = new Setting[B](key, init, pos) + + protected[sbt] def isDerived: Boolean = false + private[sbt] def setScope(s: ScopeType): Setting[T] = + make(key.copy(scope = s), init.mapReferenced(mapScope(const(s))), pos) + + /** Turn this setting into a `DefaultSetting` if it's not already, otherwise returns `this` */ + private[sbt] def default(id: => Long = nextDefaultID()): DefaultSetting[T] = + DefaultSetting(key, init, pos, id) + } + + private[Init] sealed class DerivedSetting[T]( + sk: ScopedKey[T], + i: Initialize[T], + p: SourcePosition, + val filter: ScopeType => Boolean, + val trigger: AttributeKey[_] => Boolean + ) extends Setting[T](sk, i, p) { + + override def make[B](key: ScopedKey[B], init: Initialize[B], pos: SourcePosition): Setting[B] = + new DerivedSetting[B](key, init, pos, filter, trigger) + + protected[sbt] override def isDerived: Boolean = true + + override def default(_id: => Long): DefaultSetting[T] = + new DerivedSetting[T](sk, i, p, filter, trigger) with DefaultSetting[T] { val id = _id } + + override def toString = "derived " + super.toString + } + + // Only keep the first occurrence of this setting and move it to the front so that it has lower precedence than non-defaults. + // This is intended for internal sbt use only, where alternatives like Plugin.globalSettings are not available. + private[Init] sealed trait DefaultSetting[T] extends Setting[T] { + val id: Long + + override def make[B](key: ScopedKey[B], init: Initialize[B], pos: SourcePosition): Setting[B] = + super.make(key, init, pos) default id + + override final def hashCode = id.hashCode + + override final def equals(o: Any): Boolean = o match { + case d: DefaultSetting[_] => d.id == id; case _ => false + } + + override def toString = s"default($id) " + super.toString + override def default(id: => Long) = this + } + + object DefaultSetting { + def apply[T](sk: ScopedKey[T], i: Initialize[T], p: SourcePosition, _id: Long) = + new Setting[T](sk, i, p) with DefaultSetting[T] { val id = _id } + } + + private[this] def handleUndefined[T](vr: ValidatedInit[T]): Initialize[T] = vr match { + case Left(undefs) => throw new RuntimeUndefined(undefs) + case Right(x) => x + } + + private[this] lazy val getValidated = λ[ValidatedInit ~> Initialize](handleUndefined(_)) + + // mainly for reducing generated class count + private[this] def validateKeyReferencedT(g: ValidateKeyRef) = + λ[Initialize ~> ValidatedInit](_ validateKeyReferenced g) + + private[this] def mapReferencedT(g: MapScoped) = λ[Initialize ~> Initialize](_ mapReferenced g) + private[this] def mapConstantT(g: MapConstant) = λ[Initialize ~> Initialize](_ mapConstant g) + private[this] def evaluateT(g: Settings[ScopeType]) = λ[Initialize ~> Id](_ evaluate g) + + private[this] def deps(ls: Seq[Initialize[_]]): Seq[ScopedKey[_]] = ls.flatMap(_.dependencies) + + /** An `Initialize[T]` associated with a `ScopedKey[S]`. + * @tparam S the type of the associated `ScopedKey` + * @tparam T the type of the value this `Initialize` defines. + */ + sealed trait Keyed[S, T] extends Initialize[T] { + def scopedKey: ScopedKey[S] + def transform: S => T + + final def dependencies = scopedKey :: Nil + final def apply[Z](g: T => Z): Initialize[Z] = new GetValue(scopedKey, g compose transform) + final def evaluate(ss: Settings[ScopeType]): T = transform(getValue(ss, scopedKey)) + final def mapReferenced(g: MapScoped): Initialize[T] = new GetValue(g(scopedKey), transform) + + private[sbt] final def validateKeyReferenced(g: ValidateKeyRef): ValidatedInit[T] = + g(scopedKey, false) match { + case Left(un) => Left(un :: Nil) + case Right(nk) => Right(new GetValue(nk, transform)) + } + + final def mapConstant(g: MapConstant): Initialize[T] = g(scopedKey) match { + case None => this + case Some(const) => new Value(() => transform(const)) + } + + private[sbt] def processAttributes[B](init: B)(f: (B, AttributeMap) => B): B = init + } + + private[this] final class GetValue[S, T](val scopedKey: ScopedKey[S], val transform: S => T) + extends Keyed[S, T] + + /** A `Keyed` where the type of the value and the associated `ScopedKey` are the same. + * @tparam T the type of both the value this `Initialize` defines and the type of the associated `ScopedKey`. + */ + trait KeyedInitialize[T] extends Keyed[T, T] { + final val transform = idFun[T] + } + + private[sbt] final class TransformCapture(val f: Initialize ~> Initialize) + extends Initialize[Initialize ~> Initialize] { + def dependencies = Nil + def apply[Z](g2: (Initialize ~> Initialize) => Z): Initialize[Z] = map(this)(g2) + def evaluate(ss: Settings[ScopeType]): Initialize ~> Initialize = f + def mapReferenced(g: MapScoped) = new TransformCapture(mapReferencedT(g) ∙ f) + def mapConstant(g: MapConstant) = new TransformCapture(mapConstantT(g) ∙ f) + + def validateKeyReferenced(g: ValidateKeyRef) = + Right(new TransformCapture(getValidated ∙ validateKeyReferencedT(g) ∙ f)) + + private[sbt] def processAttributes[S](init: S)(f: (S, AttributeMap) => S): S = init + } + + private[sbt] final class ValidationCapture[T](val key: ScopedKey[T], val selfRefOk: Boolean) + extends Initialize[ScopedKey[T]] { + def dependencies = Nil + def apply[Z](g2: ScopedKey[T] => Z): Initialize[Z] = map(this)(g2) + def evaluate(ss: Settings[ScopeType]) = key + def mapReferenced(g: MapScoped) = new ValidationCapture(g(key), selfRefOk) + def mapConstant(g: MapConstant) = this + + def validateKeyReferenced(g: ValidateKeyRef) = g(key, selfRefOk) match { + case Left(un) => Left(un :: Nil) + case Right(k) => Right(new ValidationCapture(k, selfRefOk)) + } + + private[sbt] def processAttributes[S](init: S)(f: (S, AttributeMap) => S): S = init + } + + private[sbt] final class Bind[S, T](val f: S => Initialize[T], val in: Initialize[S]) + extends Initialize[T] { + def dependencies = in.dependencies + def apply[Z](g: T => Z): Initialize[Z] = new Bind[S, Z](s => f(s)(g), in) + def evaluate(ss: Settings[ScopeType]): T = f(in evaluate ss) evaluate ss + def mapReferenced(g: MapScoped) = new Bind[S, T](s => f(s) mapReferenced g, in mapReferenced g) + + def validateKeyReferenced(g: ValidateKeyRef) = (in validateKeyReferenced g).right.map { + validIn => + new Bind[S, T](s => handleUndefined(f(s) validateKeyReferenced g), validIn) + } + + def mapConstant(g: MapConstant) = new Bind[S, T](s => f(s) mapConstant g, in mapConstant g) + + private[sbt] def processAttributes[B](init: B)(f: (B, AttributeMap) => B): B = + in.processAttributes(init)(f) + } + + private[sbt] final class Optional[S, T](val a: Option[Initialize[S]], val f: Option[S] => T) + extends Initialize[T] { + def dependencies = deps(a.toList) + def apply[Z](g: T => Z): Initialize[Z] = new Optional[S, Z](a, g compose f) + def mapReferenced(g: MapScoped) = new Optional(a map mapReferencedT(g).fn, f) + + def validateKeyReferenced(g: ValidateKeyRef) = a match { + case None => Right(this) + case Some(i) => Right(new Optional(i.validateKeyReferenced(g).right.toOption, f)) + } + + def mapConstant(g: MapConstant): Initialize[T] = new Optional(a map mapConstantT(g).fn, f) + def evaluate(ss: Settings[ScopeType]): T = f(a.flatMap(i => trapBadRef(evaluateT(ss)(i)))) + + // proper solution is for evaluate to be deprecated or for external use only and a new internal method returning Either be used + private[this] def trapBadRef[A](run: => A): Option[A] = + try Some(run) + catch { case _: InvalidReference => None } + + private[sbt] def processAttributes[B](init: B)(f: (B, AttributeMap) => B): B = a match { + case None => init + case Some(i) => i.processAttributes(init)(f) + } + } + + private[sbt] final class Value[T](val value: () => T) extends Initialize[T] { + def dependencies = Nil + def mapReferenced(g: MapScoped) = this + def validateKeyReferenced(g: ValidateKeyRef) = Right(this) + def apply[S](g: T => S) = new Value[S](() => g(value())) + def mapConstant(g: MapConstant) = this + def evaluate(map: Settings[ScopeType]): T = value() + private[sbt] def processAttributes[S](init: S)(f: (S, AttributeMap) => S): S = init + } + + private[sbt] final object StaticScopes extends Initialize[Set[ScopeType]] { + def dependencies = Nil + def mapReferenced(g: MapScoped) = this + def validateKeyReferenced(g: ValidateKeyRef) = Right(this) + def apply[S](g: Set[ScopeType] => S) = map(this)(g) + def mapConstant(g: MapConstant) = this + def evaluate(map: Settings[ScopeType]) = map.scopes + private[sbt] def processAttributes[S](init: S)(f: (S, AttributeMap) => S): S = init + } + + private[sbt] final class Apply[K[L[x]], T]( + val f: K[Id] => T, + val inputs: K[Initialize], + val alist: AList[K] + ) extends Initialize[T] { + def dependencies = deps(alist.toList(inputs)) + def mapReferenced(g: MapScoped) = mapInputs(mapReferencedT(g)) + def apply[S](g: T => S) = new Apply(g compose f, inputs, alist) + def mapConstant(g: MapConstant) = mapInputs(mapConstantT(g)) + + def mapInputs(g: Initialize ~> Initialize): Initialize[T] = + new Apply(f, alist.transform(inputs, g), alist) + + def evaluate(ss: Settings[ScopeType]) = f(alist.transform(inputs, evaluateT(ss))) + + def validateKeyReferenced(g: ValidateKeyRef) = { + val tx = alist.transform(inputs, validateKeyReferencedT(g)) + val undefs = alist.toList(tx).flatMap(_.left.toSeq.flatten) + val get = λ[ValidatedInit ~> Initialize](_.right.get) + if (undefs.isEmpty) Right(new Apply(f, alist.transform(tx, get), alist)) else Left(undefs) + } + + private[sbt] def processAttributes[S](init: S)(f: (S, AttributeMap) => S): S = + alist.toList(inputs).foldLeft(init) { (v, i) => + i.processAttributes(v)(f) + } + } + private def remove[T](s: Seq[T], v: T) = s filterNot (_ == v) +} diff --git a/internal/util-collection/src/main/scala/sbt/internal/util/Signal.scala b/internal/util-collection/src/main/scala/sbt/internal/util/Signal.scala new file mode 100644 index 000000000..b0e862410 --- /dev/null +++ b/internal/util-collection/src/main/scala/sbt/internal/util/Signal.scala @@ -0,0 +1,97 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util + +import sun.misc.{ Signal, SignalHandler } + +object Signals { + val CONT = "CONT" + val INT = "INT" + + def withHandler[T](handler: () => Unit, signal: String = INT)(action: () => T): T = { + val result = + try { + val signals = new Signals0 + signals.withHandler(signal, handler, action) + } catch { case _: LinkageError => Right(action()): Either[Throwable, T] } + + result match { + case Left(e) => throw e + case Right(v) => v + } + } + + /** Helper interface so we can expose internals of signal-isms to others. */ + sealed trait Registration { + def remove(): Unit + } + + /** + * Register a signal handler that can be removed later. + * NOTE: Does not stack with other signal handlers!!!! + */ + def register(handler: () => Unit, signal: String = INT): Registration = + // TODO - Maybe we can just ignore things if not is-supported. + if (supported(signal)) { + val intSignal = new Signal(signal) + val newHandler = new SignalHandler { + def handle(sig: Signal): Unit = { handler() } + } + val oldHandler = Signal.handle(intSignal, newHandler) + new UnregisterNewHandler(intSignal, oldHandler) + } else { + // TODO - Maybe we should just throw an exception if we don't support signals... + NullUnregisterNewHandler + } + + def supported(signal: String): Boolean = + try { + val signals = new Signals0 + signals.supported(signal) + } catch { case _: LinkageError => false } +} + +private class UnregisterNewHandler(intSignal: Signal, oldHandler: SignalHandler) + extends Signals.Registration { + override def remove(): Unit = { + Signal.handle(intSignal, oldHandler) + () + } +} +private object NullUnregisterNewHandler extends Signals.Registration { + override def remove(): Unit = () +} + +// Must only be referenced using a +// try { } catch { case _: LinkageError => ... } +// block to +private final class Signals0 { + def supported(signal: String): Boolean = { + import sun.misc.Signal + try { + new Signal(signal); true + } catch { case _: IllegalArgumentException => false } + } + + // returns a LinkageError in `action` as Left(t) in order to avoid it being + // incorrectly swallowed as missing Signal/SignalHandler + def withHandler[T](signal: String, handler: () => Unit, action: () => T): Either[Throwable, T] = { + import sun.misc.{ Signal, SignalHandler } + val intSignal = new Signal(signal) + val newHandler = new SignalHandler { + def handle(sig: Signal): Unit = { handler() } + } + + val oldHandler = Signal.handle(intSignal, newHandler) + + try Right(action()) + catch { case e: LinkageError => Left(e) } finally { + Signal.handle(intSignal, oldHandler); () + } + } +} diff --git a/internal/util-collection/src/main/scala/sbt/internal/util/TypeFunctions.scala b/internal/util-collection/src/main/scala/sbt/internal/util/TypeFunctions.scala new file mode 100644 index 000000000..5d11c57fb --- /dev/null +++ b/internal/util-collection/src/main/scala/sbt/internal/util/TypeFunctions.scala @@ -0,0 +1,70 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util + +trait TypeFunctions { + import TypeFunctions._ + type Id[X] = X + type NothingK[X] = Nothing + sealed trait Const[A] { type Apply[B] = A } + sealed trait ConstK[A] { type l[L[x]] = A } + sealed trait Compose[A[_], B[_]] { type Apply[T] = A[B[T]] } + sealed trait ∙[A[_], B[_]] { type l[T] = A[B[T]] } + private type AnyLeft[T] = Left[T, Nothing] + private type AnyRight[T] = Right[Nothing, T] + + final val left: Id ~> Left[*, Nothing] = + λ[Id ~> AnyLeft](Left(_)).setToString("TypeFunctions.left") + final val right: Id ~> Right[Nothing, *] = + λ[Id ~> AnyRight](Right(_)).setToString("TypeFunctions.right") + final val some: Id ~> Some[*] = λ[Id ~> Some](Some(_)).setToString("TypeFunctions.some") + final def idFun[T]: T => T = ((t: T) => t).setToString("TypeFunctions.id") + final def const[A, B](b: B): A => B = ((_: A) => b).setToString(s"TypeFunctions.const($b)") + final def idK[M[_]]: M ~> M = λ[M ~> M](m => m).setToString("TypeFunctions.idK") + + def nestCon[M[_], N[_], G[_]](f: M ~> N): (M ∙ G)#l ~> (N ∙ G)#l = + f.asInstanceOf[(M ∙ G)#l ~> (N ∙ G)#l] // implemented with a cast to avoid extra object+method call. + // castless version: + // λ[(M ∙ G)#l ~> (N ∙ G)#l](f(_)) + + type Endo[T] = T => T + type ~>|[A[_], B[_]] = A ~> Compose[Option, B]#Apply +} + +object TypeFunctions extends TypeFunctions { + private implicit class Ops[T[_], R[_]](val underlying: T ~> R) extends AnyVal { + def setToString(string: String): T ~> R = new (T ~> R) { + override def apply[U](a: T[U]): R[U] = underlying(a) + override def toString: String = string + override def equals(o: Any): Boolean = underlying.equals(o) + override def hashCode: Int = underlying.hashCode + } + } + private implicit class FunctionOps[A, B](val f: A => B) extends AnyVal { + def setToString(string: String): A => B = new (A => B) { + override def apply(a: A): B = f(a) + override def toString: String = string + override def equals(o: Any): Boolean = f.equals(o) + override def hashCode: Int = f.hashCode + } + } +} + +trait ~>[-A[_], +B[_]] { outer => + def apply[T](a: A[T]): B[T] + // directly on ~> because of type inference limitations + final def ∙[C[_]](g: C ~> A): C ~> B = λ[C ~> B](c => outer.apply(g(c))) + final def ∙[C, D](g: C => D)(implicit ev: D <:< A[D]): C => B[D] = i => apply(ev(g(i))) + final def fn[T]: A[T] => B[T] = (t: A[T]) => apply[T](t) +} + +object ~> { + import TypeFunctions._ + val Id: Id ~> Id = idK[Id] + implicit def tcIdEquals: Id ~> Id = Id +} diff --git a/internal/util-collection/src/main/scala/sbt/internal/util/Types.scala b/internal/util-collection/src/main/scala/sbt/internal/util/Types.scala new file mode 100644 index 000000000..e07b6bd9f --- /dev/null +++ b/internal/util-collection/src/main/scala/sbt/internal/util/Types.scala @@ -0,0 +1,16 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util + +object Types extends Types + +trait Types extends TypeFunctions { + val :^: = KCons + type :+:[H, T <: HList] = HCons[H, T] + val :+: = HCons +} diff --git a/internal/util-collection/src/main/scala/sbt/internal/util/Util.scala b/internal/util-collection/src/main/scala/sbt/internal/util/Util.scala new file mode 100644 index 000000000..0155306c8 --- /dev/null +++ b/internal/util-collection/src/main/scala/sbt/internal/util/Util.scala @@ -0,0 +1,79 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util + +import java.util.Locale + +import scala.reflect.macros.blackbox +import scala.language.experimental.macros + +object Util { + def makeList[T](size: Int, value: T): List[T] = List.fill(size)(value) + + def separateE[A, B](ps: Seq[Either[A, B]]): (Seq[A], Seq[B]) = + separate(ps)(Types.idFun) + + def separate[T, A, B](ps: Seq[T])(f: T => Either[A, B]): (Seq[A], Seq[B]) = { + val (a, b) = ps.foldLeft((Nil: Seq[A], Nil: Seq[B]))((xs, y) => prependEither(xs, f(y))) + (a.reverse, b.reverse) + } + + def prependEither[A, B](acc: (Seq[A], Seq[B]), next: Either[A, B]): (Seq[A], Seq[B]) = + next match { + case Left(l) => (l +: acc._1, acc._2) + case Right(r) => (acc._1, r +: acc._2) + } + + def pairID[A, B] = (a: A, b: B) => (a, b) + + private[this] lazy val Hyphen = """-(\p{javaLowerCase})""".r + + def hasHyphen(s: String): Boolean = s.indexOf('-') >= 0 + + def hyphenToCamel(s: String): String = + if (hasHyphen(s)) Hyphen.replaceAllIn(s, _.group(1).toUpperCase(Locale.ENGLISH)) else s + + private[this] lazy val Camel = """(\p{javaLowerCase})(\p{javaUpperCase})""".r + + def camelToHyphen(s: String): String = + Camel.replaceAllIn(s, m => m.group(1) + "-" + m.group(2).toLowerCase(Locale.ENGLISH)) + + def quoteIfKeyword(s: String): String = if (ScalaKeywords.values(s)) '`' + s + '`' else s + + def ignoreResult[T](f: => T): Unit = macro Macro.ignore + + lazy val isMac: Boolean = + System.getProperty("os.name").toLowerCase(Locale.ENGLISH).contains("mac") + + lazy val isWindows: Boolean = + System.getProperty("os.name").toLowerCase(Locale.ENGLISH).contains("windows") + + lazy val isCygwin: Boolean = { + val os = Option(System.getenv("OSTYPE")) + os match { + case Some(x) => x.toLowerCase(Locale.ENGLISH).contains("cygwin") + case _ => false + } + } + + lazy val isNonCygwinWindows: Boolean = isWindows && !isCygwin + lazy val isCygwinWindows: Boolean = isWindows && isCygwin + + lazy val isEmacs: Boolean = Option(System.getenv("INSIDE_EMACS")).isDefined + + def nil[A]: List[A] = List.empty[A] + def nilSeq[A]: Seq[A] = Seq.empty[A] + def none[A]: Option[A] = (None: Option[A]) + + implicit class AnyOps[A](private val value: A) extends AnyVal { + def some: Option[A] = (Some(value): Option[A]) + } + class Macro(val c: blackbox.Context) { + def ignore(f: c.Tree): c.Expr[Unit] = c.universe.reify({ c.Expr[Any](f).splice; () }) + } +} diff --git a/internal/util-collection/src/main/scala/sbt/util/OptJsonWriter.scala b/internal/util-collection/src/main/scala/sbt/util/OptJsonWriter.scala new file mode 100644 index 000000000..13883bbff --- /dev/null +++ b/internal/util-collection/src/main/scala/sbt/util/OptJsonWriter.scala @@ -0,0 +1,29 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.util + +import sjsonnew.JsonWriter + +sealed trait OptJsonWriter[A] +final case class NoJsonWriter[A]() extends OptJsonWriter[A] +final case class SomeJsonWriter[A](value: JsonWriter[A]) extends OptJsonWriter[A] + +trait OptJsonWriter0 { + implicit def fallback[A]: NoJsonWriter[A] = NoJsonWriter() +} +object OptJsonWriter extends OptJsonWriter0 { + implicit def lift[A](implicit z: JsonWriter[A]): SomeJsonWriter[A] = SomeJsonWriter(z) + + trait StrictMode0 { + implicit def conflictingFallback1[A]: NoJsonWriter[A] = NoJsonWriter() + implicit def conflictingFallback2[A]: NoJsonWriter[A] = NoJsonWriter() + } + object StrictMode extends StrictMode0 { + implicit def lift[A](implicit z: JsonWriter[A]): SomeJsonWriter[A] = SomeJsonWriter(z) + } +} diff --git a/internal/util-collection/src/main/scala/sbt/util/Show.scala b/internal/util-collection/src/main/scala/sbt/util/Show.scala new file mode 100644 index 000000000..fa2c27008 --- /dev/null +++ b/internal/util-collection/src/main/scala/sbt/util/Show.scala @@ -0,0 +1,17 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.util + +trait Show[A] { + def show(a: A): String +} +object Show { + def apply[A](f: A => String): Show[A] = a => f(a) + + def fromToString[A]: Show[A] = _.toString +} diff --git a/internal/util-collection/src/test/scala/DagSpecification.scala b/internal/util-collection/src/test/scala/DagSpecification.scala new file mode 100644 index 000000000..f5da77dee --- /dev/null +++ b/internal/util-collection/src/test/scala/DagSpecification.scala @@ -0,0 +1,59 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util + +import org.scalacheck._ +import Prop._ + +import scala.collection.mutable.HashSet + +object DagSpecification extends Properties("Dag") { + property("No repeated nodes") = forAll { (dag: TestDag) => + isSet(dag.topologicalSort) + } + property("Sort contains node") = forAll { (dag: TestDag) => + dag.topologicalSort.contains(dag) + } + property("Dependencies precede node") = forAll { (dag: TestDag) => + dependenciesPrecedeNodes(dag.topologicalSort) + } + + implicit lazy val arbTestDag: Arbitrary[TestDag] = Arbitrary(Gen.sized(dagGen)) + private def dagGen(nodeCount: Int): Gen[TestDag] = { + val nodes = new HashSet[TestDag] + def nonterminalGen(p: Gen.Parameters): Gen[TestDag] = { + val seed = rng.Seed.random() + for { + i <- 0 until nodeCount + nextDeps <- Gen.someOf(nodes).apply(p, seed) + } nodes += new TestDag(i, nextDeps) + for (nextDeps <- Gen.someOf(nodes)) yield new TestDag(nodeCount, nextDeps) + } + Gen.parameterized(nonterminalGen) + } + + private def isSet[T](c: Seq[T]) = Set(c: _*).size == c.size + private def dependenciesPrecedeNodes(sort: List[TestDag]) = { + val seen = new HashSet[TestDag] + def iterate(remaining: List[TestDag]): Boolean = { + remaining match { + case Nil => true + case node :: tail => + if (node.dependencies.forall(seen.contains) && !seen.contains(node)) { + seen += node + iterate(tail) + } else + false + } + } + iterate(sort) + } +} +class TestDag(id: Int, val dependencies: Iterable[TestDag]) extends Dag[TestDag] { + override def toString = id + "->" + dependencies.mkString("[", ",", "]") +} diff --git a/internal/util-collection/src/test/scala/HListFormatSpec.scala b/internal/util-collection/src/test/scala/HListFormatSpec.scala new file mode 100644 index 000000000..dbdd5ce88 --- /dev/null +++ b/internal/util-collection/src/test/scala/HListFormatSpec.scala @@ -0,0 +1,35 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt +package internal +package util + +import sjsonnew.shaded.scalajson.ast.unsafe._ +import sjsonnew._, BasicJsonProtocol._, support.scalajson.unsafe._ +import HListFormats._ + +class HListFormatSpec extends UnitSpec { + val quux = 23 :+: "quux" :+: true :+: HNil + + it should "round trip quux" in assertRoundTrip(quux) + it should "round trip hnil" in assertRoundTrip(HNil) + + it should "have a flat structure for quux" in assertJsonString(quux, """[23,"quux",true]""") + it should "have a flat structure for hnil" in assertJsonString(HNil, "[]") + + def assertRoundTrip[A: JsonWriter: JsonReader](x: A) = { + val jsonString: String = toJsonString(x) + val jValue: JValue = Parser.parseUnsafe(jsonString) + val y: A = Converter.fromJson[A](jValue).get + assert(x === y) + } + + def assertJsonString[A: JsonWriter](x: A, s: String) = assert(toJsonString(x) === s) + + def toJsonString[A: JsonWriter](x: A): String = CompactPrinter(Converter.toJson(x).get) +} diff --git a/internal/util-collection/src/test/scala/KeyTest.scala b/internal/util-collection/src/test/scala/KeyTest.scala new file mode 100644 index 000000000..b2ebb4d11 --- /dev/null +++ b/internal/util-collection/src/test/scala/KeyTest.scala @@ -0,0 +1,39 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util + +import org.scalacheck._ +import Prop._ + +object KeyTest extends Properties("AttributeKey") { + property("equality") = { + compare(AttributeKey[Int]("test"), AttributeKey[Int]("test"), true) && + compare(AttributeKey[Int]("test"), AttributeKey[Int]("test", "description"), true) && + compare(AttributeKey[Int]("test", "a"), AttributeKey[Int]("test", "b"), true) && + compare(AttributeKey[Int]("test"), AttributeKey[Int]("tests"), false) && + compare(AttributeKey[Int]("test"), AttributeKey[Double]("test"), false) && + compare(AttributeKey[java.lang.Integer]("test"), AttributeKey[Int]("test"), false) && + compare(AttributeKey[Map[Int, String]]("test"), AttributeKey[Map[Int, String]]("test"), true) && + compare(AttributeKey[Map[Int, String]]("test"), AttributeKey[Map[Int, _]]("test"), false) + } + + def compare(a: AttributeKey[_], b: AttributeKey[_], same: Boolean) = + ("a.label: " + a.label) |: + ("a.manifest: " + a.manifest) |: + ("b.label: " + b.label) |: + ("b.manifest: " + b.manifest) |: + ("expected equal? " + same) |: + compare0(a, b, same) + + def compare0(a: AttributeKey[_], b: AttributeKey[_], same: Boolean) = + if (same) { + ("equality" |: (a == b)) && + ("hash" |: (a.hashCode == b.hashCode)) + } else + ("equality" |: (a != b)) +} diff --git a/internal/util-collection/src/test/scala/PMapTest.scala b/internal/util-collection/src/test/scala/PMapTest.scala new file mode 100644 index 000000000..c21cfd4b0 --- /dev/null +++ b/internal/util-collection/src/test/scala/PMapTest.scala @@ -0,0 +1,22 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util + +import Types._ + +// compilation test +object PMapTest { + val mp = new DelegatingPMap[Some, Id](new collection.mutable.HashMap) + mp(Some("asdf")) = "a" + mp(Some(3)) = 9 + val x = Some(3) :^: Some("asdf") :^: KNil + val y = x.transform[Id](mp) + assert(y.head == 9) + assert(y.tail.head == "a") + assert(y.tail.tail == KNil) +} diff --git a/internal/util-collection/src/test/scala/SettingsExample.scala b/internal/util-collection/src/test/scala/SettingsExample.scala new file mode 100644 index 000000000..5399f79c1 --- /dev/null +++ b/internal/util-collection/src/test/scala/SettingsExample.scala @@ -0,0 +1,97 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util + +import sbt.util.Show + +/** Define our settings system */ +// A basic scope indexed by an integer. +final case class Scope(nestIndex: Int, idAtIndex: Int = 0) + +// Extend the Init trait. +// (It is done this way because the Scope type parameter is used everywhere in Init. +// Lots of type constructors would become binary, which as you may know requires lots of type lambdas +// when you want a type function with only one parameter. +// That would be a general pain.) +case class SettingsExample() extends Init[Scope] { + // Provides a way of showing a Scope+AttributeKey[_] + val showFullKey: Show[ScopedKey[_]] = Show[ScopedKey[_]]((key: ScopedKey[_]) => { + s"${key.scope.nestIndex}(${key.scope.idAtIndex})/${key.key.label}" + }) + + // A sample delegation function that delegates to a Scope with a lower index. + val delegates: Scope => Seq[Scope] = { + case s @ Scope(index, proj) => + s +: (if (index <= 0) Nil + else { + (if (proj > 0) List(Scope(index)) else Nil) ++: delegates(Scope(index - 1)) + }) + } + + // Not using this feature in this example. + val scopeLocal: ScopeLocal = _ => Nil + + // These three functions + a scope (here, Scope) are sufficient for defining our settings system. +} + +/** Usage Example **/ +case class SettingsUsage(val settingsExample: SettingsExample) { + import settingsExample._ + + // Define some keys + val a = AttributeKey[Int]("a") + val b = AttributeKey[Int]("b") + + // Scope these keys + val a3 = ScopedKey(Scope(3), a) + val a4 = ScopedKey(Scope(4), a) + val a5 = ScopedKey(Scope(5), a) + + val b4 = ScopedKey(Scope(4), b) + + // Define some settings + val mySettings: Seq[Setting[_]] = Seq( + setting(a3, value(3)), + setting(b4, map(a4)(_ * 3)), + update(a5)(_ + 1) + ) + + // "compiles" and applies the settings. + // This can be split into multiple steps to access intermediate results if desired. + // The 'inspect' command operates on the output of 'compile', for example. + val applied: Settings[Scope] = + makeWithCompiledMap(mySettings)(delegates, scopeLocal, showFullKey)._2 + + // Show results. + /* for(i <- 0 to 5; k <- Seq(a, b)) { + println( k.label + i + " = " + applied.get( Scope(i), k) ) + }*/ + + /** + * Output: + * For the None results, we never defined the value and there was no value to delegate to. + * For a3, we explicitly defined it to be 3. + * a4 wasn't defined, so it delegates to a3 according to our delegates function. + * b4 gets the value for a4 (which delegates to a3, so it is 3) and multiplies by 3 + * a5 is defined as the previous value of a5 + 1 and + * since no previous value of a5 was defined, it delegates to a4, resulting in 3+1=4. + * b5 isn't defined explicitly, so it delegates to b4 and is therefore equal to 9 as well + * a0 = None + * b0 = None + * a1 = None + * b1 = None + * a2 = None + * b2 = None + * a3 = Some(3) + * b3 = None + * a4 = Some(3) + * b4 = Some(9) + * a5 = Some(4) + * b5 = Some(9) + */ +} diff --git a/internal/util-collection/src/test/scala/SettingsTest.scala b/internal/util-collection/src/test/scala/SettingsTest.scala new file mode 100644 index 000000000..bbfa2a848 --- /dev/null +++ b/internal/util-collection/src/test/scala/SettingsTest.scala @@ -0,0 +1,219 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util + +import org.scalacheck._, Prop._ + +object SettingsTest extends Properties("settings") { + val settingsExample: SettingsExample = SettingsExample() + import settingsExample._ + val settingsUsage = SettingsUsage(settingsExample) + import settingsUsage._ + + import scala.reflect.Manifest + + final val ChainMax = 5000 + lazy val chainLengthGen = Gen.choose(1, ChainMax) + + property("Basic settings test") = secure(all(tests: _*)) + + property("Basic chain") = forAll(chainLengthGen) { (i: Int) => + val abs = math.abs(i) + singleIntTest(chain(abs, value(0)), abs) + } + property("Basic bind chain") = forAll(chainLengthGen) { (i: Int) => + val abs = math.abs(i) + singleIntTest(chainBind(value(abs)), 0) + } + + property("Allows references to completed settings") = forAllNoShrink(30) { allowedReference } + final def allowedReference(intermediate: Int): Prop = { + val top = value(intermediate) + def iterate(init: Initialize[Int]): Initialize[Int] = + bind(init) { t => + if (t <= 0) + top + else + iterate(value(t - 1)) + } + evaluate(setting(chk, iterate(top)) :: Nil); true + } + + property("Derived setting chain depending on (prev derived, normal setting)") = + forAllNoShrink(Gen.choose(1, 100).label("numSettings")) { derivedSettings } + final def derivedSettings(nr: Int): Prop = { + val genScopedKeys = { + // We wan + // t to generate lists of keys that DO NOT inclue the "ch" key we use to check things. + val attrKeys = mkAttrKeys[Int](nr).filter(_.forall(_.label != "ch")) + attrKeys map (_ map (ak => ScopedKey(Scope(0), ak))) + }.label("scopedKeys").filter(_.nonEmpty) + forAll(genScopedKeys) { scopedKeys => + try { + // Note; It's evil to grab last IF you haven't verified the set can't be empty. + val last = scopedKeys.last + val derivedSettings: Seq[Setting[Int]] = ( + for { + List(scoped0, scoped1) <- chk :: scopedKeys sliding 2 + nextInit = if (scoped0 == chk) chk + else + (scoped0 zipWith chk) { (p, _) => + p + 1 + } + } yield derive(setting(scoped1, nextInit)) + ).toSeq + + { + // Note: This causes a cycle refernec error, quite frequently. + checkKey(last, Some(nr - 1), evaluate(setting(chk, value(0)) +: derivedSettings)) :| "Not derived?" + } && { + checkKey(last, None, evaluate(derivedSettings)) :| "Should not be derived" + } + } catch { + case t: Throwable => + // TODO - For debugging only. + t.printStackTrace(System.err) + throw t + } + } + } + + private def mkAttrKeys[T](nr: Int)(implicit mf: Manifest[T]): Gen[List[AttributeKey[T]]] = { + import Gen._ + val nonEmptyAlphaStr = + nonEmptyListOf(alphaChar) + .map({ xs: List[Char] => + val s = xs.mkString + s.take(1).toLowerCase + s.drop(1) + }) + .suchThat(_.forall(_.isLetter)) + + (for { + list <- Gen.listOfN(nr, nonEmptyAlphaStr) suchThat (l => l.size == l.distinct.size) + item <- list + } yield AttributeKey[T](item)).label(s"mkAttrKeys($nr)") + } + + property("Derived setting(s) replace DerivedSetting in the Seq[Setting[_]]") = + derivedKeepsPosition + final def derivedKeepsPosition: Prop = { + val a: ScopedKey[Int] = ScopedKey(Scope(0), AttributeKey[Int]("a")) + val b: ScopedKey[Int] = ScopedKey(Scope(0), AttributeKey[Int]("b")) + val prop1 = { + val settings: Seq[Setting[_]] = Seq( + setting(a, value(3)), + setting(b, value(6)), + derive(setting(b, a)), + setting(a, value(5)), + setting(b, value(8)) + ) + val ev = evaluate(settings) + checkKey(a, Some(5), ev) && checkKey(b, Some(8), ev) + } + val prop2 = { + val settings: Seq[Setting[Int]] = Seq( + setting(a, value(3)), + setting(b, value(6)), + derive(setting(b, a)), + setting(a, value(5)) + ) + val ev = evaluate(settings) + checkKey(a, Some(5), ev) && checkKey(b, Some(5), ev) + } + prop1 && prop2 + } + + property( + "DerivedSetting in ThisBuild scopes derived settings under projects thus allowing safe +=" + ) = forAllNoShrink(Gen.choose(1, 100)) { derivedSettingsScope } + final def derivedSettingsScope(nrProjects: Int): Prop = { + forAll(mkAttrKeys[Int](2)) { + case List(key, derivedKey) => + val projectKeys = for { proj <- 1 to nrProjects } yield ScopedKey(Scope(1, proj), key) + val projectDerivedKeys = for { proj <- 1 to nrProjects } yield ScopedKey( + Scope(1, proj), + derivedKey + ) + val globalKey = ScopedKey(Scope(0), key) + val globalDerivedKey = ScopedKey(Scope(0), derivedKey) + // Each project defines an initial value, but the update is defined in globalKey. + // However, the derived Settings that come from this should be scoped in each project. + val settings: Seq[Setting[_]] = + derive(setting(globalDerivedKey, settingsExample.map(globalKey)(_ + 1))) +: projectKeys + .map(pk => setting(pk, value(0))) + val ev = evaluate(settings) + // Also check that the key has no value at the "global" scope + val props = for { pk <- projectDerivedKeys } yield checkKey(pk, Some(1), ev) + checkKey(globalDerivedKey, None, ev) && Prop.all(props: _*) + } + } + + // Circular (dynamic) references currently loop infinitely. + // This is the expected behavior (detecting dynamic cycles is expensive), + // but it may be necessary to provide an option to detect them (with a performance hit) + // This would test that cycle detection. + // property("Catches circular references") = forAll(chainLengthGen) { checkCircularReferences _ } + final def checkCircularReferences(intermediate: Int): Prop = { + val ccr = new CCR(intermediate) + try { + evaluate(setting(chk, ccr.top) :: Nil); false + } catch { + case _: java.lang.Exception => true + } + } + + def tests = + for (i <- 0 to 5; k <- Seq(a, b)) yield { + val expected = expectedValues(2 * i + (if (k == a) 0 else 1)) + checkKey[Int](ScopedKey(Scope(i), k), expected, applied) + } + + lazy val expectedValues = None :: None :: None :: None :: None :: None :: Some(3) :: None :: + Some(3) :: Some(9) :: Some(4) :: Some(9) :: Nil + + lazy val ch = AttributeKey[Int]("ch") + lazy val chk = ScopedKey(Scope(0), ch) + def chain(i: Int, prev: Initialize[Int]): Initialize[Int] = + if (i <= 0) prev else chain(i - 1, prev(_ + 1)) + + def chainBind(prev: Initialize[Int]): Initialize[Int] = + bind(prev) { v => + if (v <= 0) prev else chainBind(value(v - 1)) + } + def singleIntTest(i: Initialize[Int], expected: Int) = { + val eval = evaluate(setting(chk, i) :: Nil) + checkKey(chk, Some(expected), eval) + } + + def checkKey[T](key: ScopedKey[T], expected: Option[T], settings: Settings[Scope]) = { + val value = settings.get(key.scope, key.key) + ("Key: " + key) |: + ("Value: " + value) |: + ("Expected: " + expected) |: + (value == expected) + } + + def evaluate(settings: Seq[Setting[_]]): Settings[Scope] = + try { + makeWithCompiledMap(settings)(delegates, scopeLocal, showFullKey)._2 + } catch { + case e: Throwable => e.printStackTrace(); throw e + } +} +// This setup is a workaround for module synchronization issues +final class CCR(intermediate: Int) { + import SettingsTest.settingsExample._ + lazy val top = iterate(value(intermediate)) + def iterate(init: Initialize[Int]): Initialize[Int] = + bind(init) { t => + if (t <= 0) + top + else + iterate(value(t - 1)) + } +} diff --git a/internal/util-collection/src/test/scala/UnitSpec.scala b/internal/util-collection/src/test/scala/UnitSpec.scala new file mode 100644 index 000000000..ed582c3bb --- /dev/null +++ b/internal/util-collection/src/test/scala/UnitSpec.scala @@ -0,0 +1,12 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util + +import org.scalatest._ + +abstract class UnitSpec extends FlatSpec with Matchers diff --git a/internal/util-complete/NOTICE b/internal/util-complete/NOTICE new file mode 100644 index 000000000..d5827b9c2 --- /dev/null +++ b/internal/util-complete/NOTICE @@ -0,0 +1,4 @@ +sbt: Completion Component +Copyright 2011 - 2017, Lightbend, Inc. +Copyright 2008 - 2010, Mark Harrah +Licensed under BSD-3-Clause license (see LICENSE) diff --git a/internal/util-complete/src/main/scala/sbt/internal/util/LineReader.scala b/internal/util-complete/src/main/scala/sbt/internal/util/LineReader.scala new file mode 100644 index 000000000..d52903d87 --- /dev/null +++ b/internal/util-complete/src/main/scala/sbt/internal/util/LineReader.scala @@ -0,0 +1,370 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util + +import java.io._ +import java.util.{ List => JList } + +import jline.console.ConsoleReader +import jline.console.history.{ FileHistory, MemoryHistory } +import org.jline.reader.{ + Candidate, + Completer, + EndOfFileException, + LineReader => JLineReader, + LineReaderBuilder, + ParsedLine, + UserInterruptException, +} +import org.jline.utils.ClosedException +import sbt.internal.util.complete.Parser +import sbt.io.syntax._ + +import scala.annotation.tailrec +import scala.concurrent.duration._ +import scala.util.control.NonFatal +import java.nio.channels.ClosedByInterruptException +import java.net.MalformedURLException + +import org.jline.builtins.InputRC + +trait LineReader extends AutoCloseable { + def readLine(prompt: String, mask: Option[Char] = None): Option[String] + override def close(): Unit = {} +} + +object LineReader { + val HandleCONT = + !java.lang.Boolean.getBoolean("sbt.disable.cont") && Signals.supported(Signals.CONT) + val MaxHistorySize = 500 + + private def completer(parser: Parser[_]): Completer = new Completer { + def complete(lr: JLineReader, pl: ParsedLine, candidates: JList[Candidate]): Unit = { + Parser.completions(parser, pl.line(), 10).get.foreach { c => + /* + * For commands like `~` that delegate parsing to another parser, the `~` may be + * excluded from the completion result. For example, + * ~testOnly + * might return results like + * 'testOnly ;' + * 'testOnly com.foo.FooSpec' + * ... + * If we use the raw display, JLine will reject the completions because they are + * missing the leading `~`. To workaround this, we append to the result to the + * line provided the line does not end with " ". This fixes the missing `~` in + * the prefix problem. We also need to split the line on space and take the + * last token and append to that otherwise the completion will double print + * the prefix, so that `testOnly com` might expand to something like: + * `testOnly testOnly\ com.foo.FooSpec` instead of `testOnly com.foo.FooSpec`. + */ + if (c.append.nonEmpty) { + val cand = pl.line() match { + case line if line.endsWith(" ") => c.append + case line => line.split(" ").last + c.append + } + // https://github.com/jline/jline3/blob/9a4971868e4bdd29a36e454de01f54d3cd6071e0/reader/src/main/java/org/jline/reader/Candidate.java#L123-L131 + // "If the candidate is complete and is selected, a space separator will be added." + val complete = false + candidates.add(new Candidate(cand, cand, null, null, null, null, complete)) + } + } + } + } + private[this] def inputrcFileUrl(): Option[URL] = { + // keep jline2 compatibility + // https://github.com/jline/jline2/blob/12b98d94589e3bd6a6/src/main/java/jline/console/ConsoleReader.java#L291-L306 + sys.props + .get("jline.inputrc") + .flatMap { path => + try { + Some(url(path)) + } catch { + case _: MalformedURLException => + Some(file(path).toURI.toURL) + } + } + .orElse { + sys.props.get("user.home").map { home => + val f = file(home) / ".inputrc" + (if (f.isFile) f else file("/etc/inputrc")).toURI.toURL + } + } + } + // cache on memory. + private[this] lazy val inputrcFileContents: Option[Array[Byte]] = + inputrcFileUrl().map(in => sbt.io.IO.readBytes(in.openStream())) + def createReader( + historyPath: Option[File], + parser: Parser[_], + terminal: Terminal, + ): LineReader = { + // We may want to consider insourcing LineReader.java from jline. We don't otherwise + // directly need jline3 for sbt. + new LineReader { + override def readLine(prompt: String, mask: Option[Char]): Option[String] = { + val term = JLine3(terminal) + val reader = LineReaderBuilder.builder().terminal(term).completer(completer(parser)).build() + try { + inputrcFileContents.foreach { bytes => + InputRC.configure( + reader, + new ByteArrayInputStream(bytes) + ) + } + } catch { + case NonFatal(_) => + // ignore + } + historyPath.foreach(f => reader.setVariable(JLineReader.HISTORY_FILE, f)) + val signalRegistration = terminal match { + case _: Terminal.ConsoleTerminal => Some(Signals.register(() => terminal.write(-1))) + case _ => None + } + try terminal.withRawInput { + Option(mask.map(reader.readLine(prompt, _)).getOrElse(reader.readLine(prompt))) + } catch { + case e: EndOfFileException => + if (terminal == Terminal.console && System.console == null) None + else Some("exit") + case _: IOError | _: ClosedException => Some("exit") + case _: UserInterruptException | _: ClosedByInterruptException | + _: UncheckedIOException => + throw new InterruptedException + } finally { + signalRegistration.foreach(_.remove()) + terminal.prompt.reset() + term.close() + } + } + } + } + + def createJLine2Reader( + historyPath: Option[File], + terminal: Terminal, + prompt: Prompt = Prompt.Running, + ): ConsoleReader = { + val cr = Terminal.createReader(terminal, prompt) + cr.setExpandEvents(false) // https://issues.scala-lang.org/browse/SI-7650 + cr.setBellEnabled(false) + val h = historyPath match { + case None => new MemoryHistory + case Some(file) => new FileHistory(file): MemoryHistory + } + h.setMaxSize(MaxHistorySize) + cr.setHistory(h) + cr.setHistoryEnabled(true) + cr + } + def simple(terminal: Terminal): LineReader = new SimpleReader(None, HandleCONT, terminal) + def simple( + historyPath: Option[File], + handleCONT: Boolean = HandleCONT, + injectThreadSleep: Boolean = false + ): LineReader = new SimpleReader(historyPath, handleCONT, injectThreadSleep) +} + +abstract class JLine extends LineReader { + protected[this] def handleCONT: Boolean + protected[this] def reader: ConsoleReader + @deprecated("For binary compatibility only", "1.4.0") + protected[this] def injectThreadSleep: Boolean = false + @deprecated("For binary compatibility only", "1.4.0") + protected[this] lazy val in: InputStream = Terminal.wrappedSystemIn + + override def readLine(prompt: String, mask: Option[Char] = None): Option[String] = + try { + unsynchronizedReadLine(prompt, mask) + } catch { + case _: InterruptedException => + // println("readLine: InterruptedException") + Option("") + } + + private[this] def unsynchronizedReadLine(prompt: String, mask: Option[Char]): Option[String] = + readLineWithHistory(prompt, mask) map { x => + x.trim + } + + private[this] def readLineWithHistory(prompt: String, mask: Option[Char]): Option[String] = + reader.getHistory match { + case fh: FileHistory => + try readLineDirect(prompt, mask) + finally fh.flush() + case _ => readLineDirect(prompt, mask) + } + + private[this] def readLineDirect(prompt: String, mask: Option[Char]): Option[String] = + if (handleCONT) + Signals.withHandler(() => resume(), signal = Signals.CONT)( + () => readLineDirectRaw(prompt, mask) + ) + else + readLineDirectRaw(prompt, mask) + + private[this] def readLineDirectRaw(prompt: String, mask: Option[Char]): Option[String] = { + val newprompt = handleMultilinePrompt(prompt) + mask match { + case Some(m) => Option(reader.readLine(newprompt, m)) + case None => Option(reader.readLine(newprompt)) + } + } + + private[this] def handleMultilinePrompt(prompt: String): String = { + val lines0 = """\r?\n""".r.split(prompt) + lines0.length match { + case 0 | 1 => handleProgress(prompt) + case _ => + val lines = lines0.toList map handleProgress + // Workaround for regression jline/jline2#205 + reader.getOutput.write(lines.init.mkString("\n") + "\n") + lines.last + } + } + + private[this] def handleProgress(prompt: String): String = { + import ConsoleAppender._ + if (showProgress) s"$DeleteLine" + prompt + else prompt + } + + private[this] def resume(): Unit = { + Terminal.reset() + reader.drawLine() + reader.flush() + } +} + +@deprecated("Use LineReader apis", "1.4.0") +private[sbt] object JLine { + @deprecated("For binary compatibility only", "1.4.0") + protected[this] val originalIn = new FileInputStream(FileDescriptor.in) + + @deprecated("Handled by Terminal.fixTerminalProperty", "1.4.0") + private[sbt] def fixTerminalProperty(): Unit = () + + @deprecated("For binary compatibility only", "1.4.0") + private[sbt] def makeInputStream(injectThreadSleep: Boolean): InputStream = + if (injectThreadSleep) new InputStreamWrapper(originalIn, 2.milliseconds) + else originalIn + + // When calling this, ensure that enableEcho has been or will be called. + // TerminalFactory.get will initialize the terminal to disable echo. + @deprecated("Don't use jline.Terminal directly", "1.4.0") + private[sbt] def terminal: jline.Terminal = Terminal.deprecatedTeminal + + /** + * For accessing the JLine Terminal object. + * This ensures synchronized access as well as re-enabling echo after getting the Terminal. + */ + @deprecated( + "Don't use jline.Terminal directly. Use Terminal.get.withCanonicalIn instead.", + "1.4.0" + ) + def usingTerminal[T](f: jline.Terminal => T): T = f(Terminal.get.toJLine) + + @deprecated("unused", "1.4.0") + def createReader(): ConsoleReader = createReader(None, Terminal.wrappedSystemIn) + + @deprecated("Use LineReader.createReader", "1.4.0") + def createReader(historyPath: Option[File], in: InputStream): ConsoleReader = { + val cr = Terminal.createReader(Terminal.console, Prompt.Running) + cr.setExpandEvents(false) // https://issues.scala-lang.org/browse/SI-7650 + cr.setBellEnabled(false) + val h = historyPath match { + case None => new MemoryHistory + case Some(file) => new FileHistory(file): MemoryHistory + } + h.setMaxSize(MaxHistorySize) + cr.setHistory(h) + cr + } + + @deprecated("Avoid referencing JLine directly.", "1.4.0") + def withJLine[T](action: => T): T = Terminal.get.withRawInput(action) + + @deprecated("Use LineReader.simple instead", "1.4.0") + def simple( + historyPath: Option[File], + handleCONT: Boolean = LineReader.HandleCONT, + injectThreadSleep: Boolean = false + ): SimpleReader = new SimpleReader(historyPath, handleCONT, injectThreadSleep) + + @deprecated("Use LineReader.MaxHistorySize", "1.4.0") + val MaxHistorySize = LineReader.MaxHistorySize + + @deprecated("Use LineReader.HandleCONT", "1.4.0") + val HandleCONT = LineReader.HandleCONT +} + +@deprecated("For binary compatibility only", "1.4.0") +private[sbt] class InputStreamWrapper(is: InputStream, val poll: Duration) + extends FilterInputStream(is) { + @tailrec final override def read(): Int = + if (is.available() != 0) is.read() + else { + Thread.sleep(poll.toMillis) + read() + } + + @tailrec final override def read(b: Array[Byte]): Int = + if (is.available() != 0) is.read(b) + else { + Thread.sleep(poll.toMillis) + read(b) + } + + @tailrec final override def read(b: Array[Byte], off: Int, len: Int): Int = + if (is.available() != 0) is.read(b, off, len) + else { + Thread.sleep(poll.toMillis) + read(b, off, len) + } +} + +final class FullReader( + historyPath: Option[File], + complete: Parser[_], + val handleCONT: Boolean, + terminal: Terminal +) extends JLine { + @deprecated("Use the constructor with no injectThreadSleep parameter", "1.4.0") + def this( + historyPath: Option[File], + complete: Parser[_], + handleCONT: Boolean = LineReader.HandleCONT, + injectThreadSleep: Boolean = false + ) = + this( + historyPath, + complete, + handleCONT, + Terminal.console + ) + protected[this] val reader: ConsoleReader = { + val cr = LineReader.createJLine2Reader(historyPath, terminal) + sbt.internal.util.complete.JLineCompletion.installCustomCompletor(cr, complete) + cr + } +} + +class SimpleReader private[sbt] ( + historyPath: Option[File], + val handleCONT: Boolean, + terminal: Terminal +) extends JLine { + def this(historyPath: Option[File], handleCONT: Boolean, injectThreadSleep: Boolean) = + this(historyPath, handleCONT, Terminal.console) + protected[this] lazy val reader: ConsoleReader = + LineReader.createJLine2Reader(historyPath, terminal) +} + +object SimpleReader extends SimpleReader(None, LineReader.HandleCONT, false) { + def apply(terminal: Terminal): SimpleReader = + new SimpleReader(None, LineReader.HandleCONT, terminal) +} diff --git a/internal/util-complete/src/main/scala/sbt/internal/util/complete/Completions.scala b/internal/util-complete/src/main/scala/sbt/internal/util/complete/Completions.scala new file mode 100644 index 000000000..2d938d89a --- /dev/null +++ b/internal/util-complete/src/main/scala/sbt/internal/util/complete/Completions.scala @@ -0,0 +1,173 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util +package complete + +/** + * Represents a set of completions. + * It exists instead of implicitly defined operations on top of Set[Completion] + * for laziness. + */ +sealed trait Completions { + def get: Set[Completion] + + final def x(o: Completions): Completions = flatMap(_ x o) + final def ++(o: Completions): Completions = Completions(get ++ o.get) + final def +:(o: Completion): Completions = Completions(get + o) + final def filter(f: Completion => Boolean): Completions = Completions(get filter f) + final def filterS(f: String => Boolean): Completions = filter(c => f(c.append)) + + override def toString = get.mkString("Completions(", ",", ")") + + final def flatMap(f: Completion => Completions): Completions = + Completions(get.flatMap(c => f(c).get)) + + final def map(f: Completion => Completion): Completions = Completions(get map f) + + override final def hashCode = get.hashCode + override final def equals(o: Any) = o match { + case c: Completions => get == c.get; case _ => false + } +} + +object Completions { + + /** Returns a lazy Completions instance using the provided Completion Set. */ + def apply(cs: => Set[Completion]): Completions = new Completions { + lazy val get = cs + } + + /** Returns a strict Completions instance using the provided Completion Set. */ + def strict(cs: Set[Completion]): Completions = apply(cs) + + /** + * No suggested completions, not even the empty Completion. + * This typically represents invalid input. + */ + val nil: Completions = strict(Set.empty) + + /** + * Only includes an empty Suggestion. + * This typically represents valid input that either has no completions or accepts no further input. + */ + val empty: Completions = strict(Set.empty + Completion.empty) + + /** Returns a strict Completions instance containing only the provided Completion.*/ + def single(c: Completion): Completions = strict(Set.empty + c) + +} + +/** + * Represents a completion. + * The abstract members `display` and `append` are best explained with an example. + * + * Assuming space-delimited tokens, processing this: + * am is are w + * could produce these Completions: + * Completion { display = "was"; append = "as" } + * Completion { display = "were"; append = "ere" } + * to suggest the tokens "was" and "were". + * + * In this way, two pieces of information are preserved: + * 1) what needs to be appended to the current input if a completion is selected + * 2) the full token being completed, which is useful for presenting a user with choices to select + */ +sealed trait Completion { + + /** The proposed suffix to append to the existing input to complete the last token in the input.*/ + def append: String + + /** The string to present to the user to represent the full token being suggested.*/ + def display: String + + /** True if this Completion is suggesting the empty string.*/ + def isEmpty: Boolean + + /** Appends the completions in `o` with the completions in this Completion.*/ + def ++(o: Completion): Completion = Completion.concat(this, o) + + final def x(o: Completions): Completions = + if (Completion evaluatesRight this) o.map(this ++ _) else Completions.strict(Set.empty + this) + + override final lazy val hashCode = Completion.hashCode(this) + override final def equals(o: Any) = o match { + case c: Completion => Completion.equal(this, c); case _ => false + } +} + +final class DisplayOnly(val display: String) extends Completion { + def isEmpty = display.isEmpty + def append = "" + override def toString = "{" + display + "}" +} + +final class Token(val display: String, val append: String) extends Completion { + def isEmpty = display.isEmpty && append.isEmpty + override final def toString = "[" + display + "]++" + append +} + +final class Suggestion(val append: String) extends Completion { + def isEmpty = append.isEmpty + def display = append + override def toString = append +} + +object Completion { + def concat(a: Completion, b: Completion): Completion = + (a, b) match { + case (as: Suggestion, bs: Suggestion) => suggestion(as.append + bs.append) + case (at: Token, _) if at.append.isEmpty => b + case _ if a.isEmpty => b + case _ => a + } + + def evaluatesRight(a: Completion): Boolean = + a match { + case _: Suggestion => true + case at: Token if at.append.isEmpty => true + case _ => a.isEmpty + } + + def equal(a: Completion, b: Completion): Boolean = + (a, b) match { + case (as: Suggestion, bs: Suggestion) => as.append == bs.append + case (ad: DisplayOnly, bd: DisplayOnly) => ad.display == bd.display + case (at: Token, bt: Token) => at.display == bt.display && at.append == bt.append + case _ => false + } + + def hashCode(a: Completion): Int = + a match { + case as: Suggestion => (0, as.append).hashCode + case ad: DisplayOnly => (1, ad.display).hashCode + case at: Token => (2, at.display, at.append).hashCode + } + + val empty: Completion = suggestion("") + def single(c: Char): Completion = suggestion(c.toString) + + def displayOnly(value: String): Completion = new DisplayOnly(value) + + def token(prepend: String, append: String): Completion = + new Token(prepend + append, append) + + /** @since 0.12.1 */ + def tokenDisplay(append: String, display: String): Completion = new Token(display, append) + + def suggestion(value: String): Completion = new Suggestion(value) + + @deprecated("No longer used. for binary compatibility", "1.1.0") + private[complete] def displayOnly(value: => String): Completion = new DisplayOnly(value) + + @deprecated("No longer used. for binary compatibility", "1.1.0") + private[complete] def token(prepend: => String, append: => String): Completion = + new Token(prepend + append, append) + + @deprecated("No longer used. for binary compatibility", "1.1.0") + private[complete] def suggestion(value: => String): Completion = new Suggestion(value) +} diff --git a/internal/util-complete/src/main/scala/sbt/internal/util/complete/EditDistance.scala b/internal/util-complete/src/main/scala/sbt/internal/util/complete/EditDistance.scala new file mode 100644 index 000000000..e7c710c87 --- /dev/null +++ b/internal/util-complete/src/main/scala/sbt/internal/util/complete/EditDistance.scala @@ -0,0 +1,60 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util +package complete + +import java.lang.Character.{ toLowerCase => lower } + +/** @author Paul Phillips */ +object EditDistance { + + /** + * Translated from the java version at + * http://www.merriampark.com/ld.htm + * which is declared to be public domain. + */ + def levenshtein( + s: String, + t: String, + insertCost: Int = 1, + deleteCost: Int = 1, + subCost: Int = 1, + transposeCost: Int = 1, + matchCost: Int = 0, + caseCost: Int = 1, + transpositions: Boolean = false + ): Int = { + val _ = transposeCost + val n = s.length + val m = t.length + if (n == 0) return m + if (m == 0) return n + + val d = Array.ofDim[Int](n + 1, m + 1) + 0 to n foreach (x => d(x)(0) = x) + 0 to m foreach (x => d(0)(x) = x) + + for (i <- 1 to n; s_i = s(i - 1); j <- 1 to m) { + val t_j = t(j - 1) + val cost = if (s_i == t_j) matchCost else if (lower(s_i) == lower(t_j)) caseCost else subCost + + val c1 = d(i - 1)(j) + deleteCost + val c2 = d(i)(j - 1) + insertCost + val c3 = d(i - 1)(j - 1) + cost + + d(i)(j) = c1 min c2 min c3 + + if (transpositions) { + if (i > 1 && j > 1 && s(i - 1) == t(j - 2) && s(i - 2) == t(j - 1)) + d(i)(j) = d(i)(j) min (d(i - 2)(j - 2) + cost) + } + } + + d(n)(m) + } +} diff --git a/internal/util-complete/src/main/scala/sbt/internal/util/complete/ExampleSource.scala b/internal/util-complete/src/main/scala/sbt/internal/util/complete/ExampleSource.scala new file mode 100644 index 000000000..e15217524 --- /dev/null +++ b/internal/util-complete/src/main/scala/sbt/internal/util/complete/ExampleSource.scala @@ -0,0 +1,75 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util +package complete + +import java.io.File +import sbt.io.IO + +/** + * These sources of examples are used in parsers for user input completion. An example of such a source is the + * [[sbt.internal.util.complete.FileExamples]] class, which provides a list of suggested files to the user as they press the + * TAB key in the console. + */ +trait ExampleSource { + + /** + * @return a (possibly lazy) list of completion example strings. These strings are continuations of user's input. The + * user's input is incremented with calls to [[withAddedPrefix]]. + */ + def apply(): Iterable[String] + + /** + * @param addedPrefix a string that just typed in by the user. + * @return a new source of only those examples that start with the string typed by the user so far (with addition of + * the just added prefix). + */ + def withAddedPrefix(addedPrefix: String): ExampleSource + +} + +/** + * A convenience example source that wraps any collection of strings into a source of examples. + * @param examples the examples that will be displayed to the user when they press the TAB key. + */ +sealed case class FixedSetExamples(examples: Iterable[String]) extends ExampleSource { + override def withAddedPrefix(addedPrefix: String): ExampleSource = + FixedSetExamples(examplesWithRemovedPrefix(addedPrefix)) + + override def apply(): Iterable[String] = examples + + private def examplesWithRemovedPrefix(prefix: String) = examples.collect { + case example if example startsWith prefix => example substring prefix.length + } +} + +/** + * Provides path completion examples based on files in the base directory. + * @param base the directory within which this class will search for completion examples. + * @param prefix the part of the path already written by the user. + */ +class FileExamples(base: File, prefix: String = "") extends ExampleSource { + override def apply(): Stream[String] = files(base).map(_ substring prefix.length) + + override def withAddedPrefix(addedPrefix: String): FileExamples = + new FileExamples(base, prefix + addedPrefix) + + protected def files(directory: File): Stream[String] = { + val childPaths = IO.listFiles(directory).toStream + val prefixedDirectChildPaths = childPaths map { IO.relativize(base, _).get } filter { + _ startsWith prefix + } + val dirsToRecurseInto = childPaths filter { _.isDirectory } map { IO.relativize(base, _).get } filter { + dirStartsWithPrefix + } + prefixedDirectChildPaths append dirsToRecurseInto.flatMap(dir => files(new File(base, dir))) + } + + private def dirStartsWithPrefix(relativizedPath: String): Boolean = + (relativizedPath startsWith prefix) || (prefix startsWith relativizedPath) +} diff --git a/internal/util-complete/src/main/scala/sbt/internal/util/complete/History.scala b/internal/util-complete/src/main/scala/sbt/internal/util/complete/History.scala new file mode 100644 index 000000000..b65dba0dc --- /dev/null +++ b/internal/util-complete/src/main/scala/sbt/internal/util/complete/History.scala @@ -0,0 +1,63 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util +package complete + +import History.number +import java.io.File + +final class History private (val lines: IndexedSeq[String], val path: Option[File]) { + private def reversed = lines.reverse + + def all: Seq[String] = lines + def size = lines.length + def !! : Option[String] = !-(1) + + def apply(i: Int): Option[String] = + if (0 <= i && i < size) Some(lines(i)) + else { + sys.error("Invalid history index: " + i) + } + + def !(i: Int): Option[String] = apply(i) + + def !(s: String): Option[String] = + number(s) match { + case Some(n) => if (n < 0) !-(-n) else apply(n) + case None => nonEmpty(s) { reversed.find(_.startsWith(s)) } + } + + def !-(n: Int): Option[String] = apply(size - n - 1) + + def !?(s: String): Option[String] = nonEmpty(s) { reversed.drop(1).find(_.contains(s)) } + + private def nonEmpty[T](s: String)(act: => Option[T]): Option[T] = + if (s.isEmpty) + sys.error("No action specified to history command") + else + act + + def list(historySize: Int, show: Int): Seq[String] = + lines.toList + .drop(scala.math.max(0, lines.size - historySize)) + .zipWithIndex + .map { case (line, number) => " " + number + " " + line } + .takeRight(show max 1) +} + +object History { + def apply(lines: Seq[String], path: Option[File], error: String => Unit): History = + new History(lines.toIndexedSeq, path) + def apply(lines: Seq[String], path: Option[File]): History = + new History(lines.toIndexedSeq, path) + + def number(s: String): Option[Int] = + try { + Some(s.toInt) + } catch { case _: NumberFormatException => None } +} diff --git a/internal/util-complete/src/main/scala/sbt/internal/util/complete/HistoryCommands.scala b/internal/util-complete/src/main/scala/sbt/internal/util/complete/HistoryCommands.scala new file mode 100644 index 000000000..6c45f54e9 --- /dev/null +++ b/internal/util-complete/src/main/scala/sbt/internal/util/complete/HistoryCommands.scala @@ -0,0 +1,88 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util +package complete + +import sbt.io.IO +import Util.{ AnyOps, nil } + +object HistoryCommands { + val Start = "!" + // second characters + val Contains = "?" + val Last = "!" + val ListCommands = ":" + + def ContainsFull = h(Contains) + def LastFull = h(Last) + def ListFull = h(ListCommands) + + def ListN = ListFull + "n" + def ContainsString = ContainsFull + "string" + def StartsWithString = Start + "string" + def Previous = Start + "-n" + def Nth = Start + "n" + + private def h(s: String) = Start + s + def plainCommands = Seq(ListFull, Start, LastFull, ContainsFull) + + def descriptions = Seq( + LastFull -> "Execute the last command again", + ListFull -> "Show all previous commands", + ListN -> "Show the last n commands", + Nth -> ("Execute the command with index n, as shown by the " + ListFull + " command"), + Previous -> "Execute the nth command before this one", + StartsWithString -> "Execute the most recent command starting with 'string'", + ContainsString -> "Execute the most recent command containing 'string'" + ) + + def helpString = + "History commands:\n " + (descriptions + .map { case (c, d) => c + " " + d }) + .mkString("\n ") + + def printHelp(): Unit = println(helpString) + + def printHistory(history: complete.History, historySize: Int, show: Int): Unit = + history.list(historySize, show).foreach(println) + + import DefaultParsers._ + + val MaxLines = 500 + lazy val num = token(NatBasic, "") + lazy val last = Last ^^^ { execute(_.!!) } + + lazy val list = ListCommands ~> (num ?? Int.MaxValue) map { show => (h: History) => + { printHistory(h, MaxLines, show); nil[String].some } + } + + lazy val execStr = flag('?') ~ token(any.+.string, "") map { + case (contains, str) => + execute(h => if (contains) h !? str else h ! str) + } + + lazy val execInt = flag('-') ~ num map { + case (neg, value) => + execute(h => if (neg) h !- value else h ! value) + } + + lazy val help = success((h: History) => { printHelp(); nil[String].some }) + + def execute(f: History => Option[String]): History => Option[List[String]] = (h: History) => { + val command = f(h).filterNot(_.startsWith(Start)) + val lines = h.lines.toArray + command.foreach(lines(lines.length - 1) = _) + h.path foreach { h => + IO.writeLines(h, lines) + } + command.toList.some + } + + val actionParser: Parser[complete.History => Option[List[String]]] = + Start ~> (help | last | execInt | list | execStr) // execStr must come last +} diff --git a/internal/util-complete/src/main/scala/sbt/internal/util/complete/JLineCompletion.scala b/internal/util-complete/src/main/scala/sbt/internal/util/complete/JLineCompletion.scala new file mode 100644 index 000000000..1e9f4cfcb --- /dev/null +++ b/internal/util-complete/src/main/scala/sbt/internal/util/complete/JLineCompletion.scala @@ -0,0 +1,184 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util +package complete + +import jline.console.ConsoleReader +import jline.console.completer.{ Completer, CompletionHandler } +import scala.annotation.tailrec +import scala.collection.JavaConverters._ + +object JLineCompletion { + def installCustomCompletor(reader: ConsoleReader, parser: Parser[_]): Unit = + installCustomCompletor(reader)(parserAsCompletor(parser)) + + def installCustomCompletor(reader: ConsoleReader)( + complete: (String, Int) => (Seq[String], Seq[String]) + ): Unit = + installCustomCompletor(customCompletor(complete), reader) + + def installCustomCompletor( + complete: (ConsoleReader, Int) => Boolean, + reader: ConsoleReader + ): Unit = { + reader.removeCompleter(DummyCompletor) + reader.addCompleter(DummyCompletor) + reader.setCompletionHandler(new CustomHandler(complete)) + } + + private[this] final class CustomHandler(completeImpl: (ConsoleReader, Int) => Boolean) + extends CompletionHandler { + private[this] var previous: Option[(String, Int)] = None + private[this] var level: Int = 1 + + override def complete( + reader: ConsoleReader, + candidates: java.util.List[CharSequence], + position: Int + ) = { + val current = Some(bufferSnapshot(reader)) + level = if (current == previous) level + 1 else 1 + previous = current + try completeImpl(reader, level) + catch { + case e: Exception => + reader.print("\nException occurred while determining completions.") + e.printStackTrace() + false + } + } + } + + // always provides dummy completions so that the custom completion handler gets called + // (ConsoleReader doesn't call the handler if there aren't any completions) + // the custom handler will then throw away the candidates and call the custom function + private[this] final object DummyCompletor extends Completer { + override def complete( + buffer: String, + cursor: Int, + candidates: java.util.List[CharSequence] + ): Int = { + candidates.asInstanceOf[java.util.List[String]] add "dummy" + 0 + } + } + + def parserAsCompletor(p: Parser[_]): (String, Int) => (Seq[String], Seq[String]) = + (str, level) => convertCompletions(Parser.completions(p, str, level)) + + def convertCompletions(c: Completions): (Seq[String], Seq[String]) = { + val cs = c.get + if (cs.isEmpty) + (Nil, "{invalid input}" :: Nil) + else + convertCompletions(cs) + } + + def convertCompletions(cs: Set[Completion]): (Seq[String], Seq[String]) = { + val (insert, display) = + cs.foldLeft((Set.empty[String], Set.empty[String])) { + case (t @ (insert, display), comp) => + if (comp.isEmpty) t + else (appendNonEmpty(insert, comp.append), appendNonEmpty(display, comp.display)) + } + (insert.toSeq, display.toSeq.sorted) + } + + def appendNonEmpty(set: Set[String], add: String) = if (add.trim.isEmpty) set else set + add + + def customCompletor( + f: (String, Int) => (Seq[String], Seq[String]) + ): (ConsoleReader, Int) => Boolean = + (reader, level) => { + val success = complete(beforeCursor(reader), string => f(string, level), reader) + reader.flush() + success + } + + def bufferSnapshot(reader: ConsoleReader): (String, Int) = { + val b = reader.getCursorBuffer + (b.buffer.toString, b.cursor) + } + + def beforeCursor(reader: ConsoleReader): String = { + val b = reader.getCursorBuffer + b.buffer.substring(0, b.cursor) + } + + // returns false if there was nothing to insert and nothing to display + def complete( + beforeCursor: String, + completions: String => (Seq[String], Seq[String]), + reader: ConsoleReader + ): Boolean = { + val (insert, display) = completions(beforeCursor) + val common = commonPrefix(insert) + if (common.isEmpty) + if (display.isEmpty) + () + else + showCompletions(display, reader) + else + appendCompletion(common, reader) + + !(common.isEmpty && display.isEmpty) + } + + def appendCompletion(common: String, reader: ConsoleReader): Unit = { + reader.getCursorBuffer.write(common) + reader.redrawLine() + } + + /** + * `display` is assumed to be the exact strings requested to be displayed. + * In particular, duplicates should have been removed already. + */ + def showCompletions(display: Seq[String], reader: ConsoleReader): Unit = { + printCompletions(display, reader) + reader.drawLine() + } + + def printCompletions(cs: Seq[String], reader: ConsoleReader): Unit = { + val print = shouldPrint(cs, reader) + reader.println() + if (print) printLinesAndColumns(cs, reader) + } + + def printLinesAndColumns(cs: Seq[String], reader: ConsoleReader): Unit = { + val (lines, columns) = cs partition hasNewline + for (line <- lines) { + reader.print(line) + if (line.charAt(line.length - 1) != '\n') + reader.println() + } + reader.printColumns(columns.map(_.trim).asJava) + } + + def hasNewline(s: String): Boolean = s.indexOf('\n') >= 0 + + def shouldPrint(cs: Seq[String], reader: ConsoleReader): Boolean = { + val size = cs.size + (size <= reader.getAutoprintThreshold) || + confirm("Display all %d possibilities? (y or n) ".format(size), 'y', 'n', reader) + } + + def confirm(prompt: String, trueC: Char, falseC: Char, reader: ConsoleReader): Boolean = { + reader.println() + reader.print(prompt) + reader.flush() + reader.readCharacter(trueC, falseC) == trueC + } + + def commonPrefix(s: Seq[String]): String = if (s.isEmpty) "" else s reduceLeft commonPrefix + + def commonPrefix(a: String, b: String): String = { + val len = scala.math.min(a.length, b.length) + @tailrec def loop(i: Int): Int = if (i >= len) len else if (a(i) != b(i)) i else loop(i + 1) + a.substring(0, loop(0)) + } +} diff --git a/internal/util-complete/src/main/scala/sbt/internal/util/complete/Parser.scala b/internal/util-complete/src/main/scala/sbt/internal/util/complete/Parser.scala new file mode 100644 index 000000000..464ab1916 --- /dev/null +++ b/internal/util-complete/src/main/scala/sbt/internal/util/complete/Parser.scala @@ -0,0 +1,988 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util +package complete + +import Parser._ +import sbt.internal.util.Types.{ left, right, some } +import sbt.internal.util.Util.{ makeList, separate } + +/** + * A String parser that provides semi-automatic tab completion. + * A successful parse results in a value of type `T`. + * The methods in this trait are what must be implemented to define a new Parser implementation, but are not typically useful for common usage. + * Instead, most useful methods for combining smaller parsers into larger parsers are implicitly added by the [[RichParser]] type. + */ +trait Parser[+T] { + def derive(i: Char): Parser[T] + def resultEmpty: Result[T] + def result: Option[T] + def completions(level: Int): Completions + def failure: Option[Failure] + def isTokenStart = false + def ifValid[S](p: => Parser[S]): Parser[S] + def valid: Boolean +} + +sealed trait RichParser[A] { + + /** Apply the original Parser and then apply `next` (in order). The result of both is provides as a pair. */ + def ~[B](next: Parser[B]): Parser[(A, B)] + + /** Apply the original Parser one or more times and provide the non-empty sequence of results.*/ + def + : Parser[Seq[A]] + + /** Apply the original Parser zero or more times and provide the (potentially empty) sequence of results.*/ + def * : Parser[Seq[A]] + + /** Apply the original Parser zero or one times, returning None if it was applied zero times or the result wrapped in Some if it was applied once.*/ + def ? : Parser[Option[A]] + + /** Apply either the original Parser or `b`.*/ + def |[B >: A](b: Parser[B]): Parser[B] + + /** Apply either the original Parser or `b`.*/ + def ||[B](b: Parser[B]): Parser[Either[A, B]] + + /** Apply the original Parser to the input and then apply `f` to the result.*/ + def map[B](f: A => B): Parser[B] + + /** + * Returns the original parser. This is useful for converting literals to Parsers. + * For example, `'c'.id` or `"asdf".id` + */ + def id: Parser[A] + + /** Apply the original Parser, but provide `value` as the result if it succeeds. */ + def ^^^[B](value: B): Parser[B] + + /** Apply the original Parser, but provide `alt` as the result if it fails.*/ + def ??[B >: A](alt: B): Parser[B] + + /** + * Produces a Parser that applies the original Parser and then applies `next` (in order), discarding the result of `next`. + * (The arrow point in the direction of the retained result.) + */ + def <~[B](b: Parser[B]): Parser[A] + + /** + * Produces a Parser that applies the original Parser and then applies `next` (in order), discarding the result of the original parser. + * (The arrow point in the direction of the retained result.) + */ + def ~>[B](b: Parser[B]): Parser[B] + + /** Uses the specified message if the original Parser fails.*/ + def !!!(msg: String): Parser[A] + + /** + * If an exception is thrown by the original Parser, + * capture it and fail locally instead of allowing the exception to propagate up and terminate parsing. + */ + def failOnException: Parser[A] + + /** + * Apply the original parser, but only succeed if `o` also succeeds. + * Note that `o` does not need to consume the same amount of input to satisfy this condition. + */ + def &(o: Parser[_]): Parser[A] + + /** Explicitly defines the completions for the original Parser.*/ + def examples(s: String*): Parser[A] + + /** Explicitly defines the completions for the original Parser.*/ + def examples(s: Set[String], check: Boolean = false): Parser[A] + + /** + * @param exampleSource the source of examples when displaying completions to the user. + * @param maxNumberOfExamples limits the number of examples that the source of examples should return. This can + * prevent lengthy pauses and avoids bad interactive user experience. + * @param removeInvalidExamples indicates whether completion examples should be checked for validity (against the + * given parser). Invalid examples will be filtered out and only valid suggestions will + * be displayed. + * @return a new parser with a new source of completions. + */ + def examples( + exampleSource: ExampleSource, + maxNumberOfExamples: Int, + removeInvalidExamples: Boolean + ): Parser[A] + + /** + * @param exampleSource the source of examples when displaying completions to the user. + * @return a new parser with a new source of completions. It displays at most 25 completion examples and does not + * remove invalid examples. + */ + def examples(exampleSource: ExampleSource): Parser[A] = + examples(exampleSource, maxNumberOfExamples = 25, removeInvalidExamples = false) + + /** Converts a Parser returning a Char sequence to a Parser returning a String.*/ + def string(implicit ev: A <:< Seq[Char]): Parser[String] + + /** + * Produces a Parser that filters the original parser. + * If 'f' is not true when applied to the output of the original parser, the Parser returned by this method fails. + * The failure message is constructed by applying `msg` to the String that was successfully parsed by the original parser. + */ + def filter(f: A => Boolean, msg: String => String): Parser[A] + + /** Applies the original parser, applies `f` to the result to get the next parser, and applies that parser and uses its result for the overall result. */ + def flatMap[B](f: A => Parser[B]): Parser[B] +} + +/** Contains Parser implementation helper methods not typically needed for using parsers. */ +object Parser extends ParserMain { + sealed abstract class Result[+T] { + def isFailure: Boolean + def isValid: Boolean + def errors: Seq[String] + def or[B >: T](b: => Result[B]): Result[B] + def either[B](b: => Result[B]): Result[Either[T, B]] + def map[B](f: T => B): Result[B] + def flatMap[B](f: T => Result[B]): Result[B] + def &&(b: => Result[_]): Result[T] + def filter(f: T => Boolean, msg: => String): Result[T] + def seq[B](b: => Result[B]): Result[(T, B)] = app(b)((m, n) => (m, n)) + def app[B, C](b: => Result[B])(f: (T, B) => C): Result[C] + def toEither: Either[() => Seq[String], T] + } + + final case class Value[+T](value: T) extends Result[T] { + def isFailure = false + def isValid: Boolean = true + def errors = Nil + + def app[B, C](b: => Result[B])(f: (T, B) => C): Result[C] = b match { + case fail: Failure => fail + case Value(bv) => Value(f(value, bv)) + } + + def &&(b: => Result[_]): Result[T] = b match { case f: Failure => f; case _ => this } + def or[B >: T](b: => Result[B]): Result[B] = this + def either[B](b: => Result[B]): Result[Either[T, B]] = Value(Left(value)) + def map[B](f: T => B): Result[B] = Value(f(value)) + def flatMap[B](f: T => Result[B]): Result[B] = f(value) + def filter(f: T => Boolean, msg: => String): Result[T] = if (f(value)) this else mkFailure(msg) + def toEither = Right(value) + } + + final class Failure private[sbt] (mkErrors: => Seq[String], val definitive: Boolean) + extends Result[Nothing] { + lazy val errors: Seq[String] = mkErrors + def isFailure = true + def isValid = false + def map[B](f: Nothing => B) = this + def flatMap[B](f: Nothing => Result[B]) = this + + def or[B](b: => Result[B]): Result[B] = b match { + case v: Value[B] => v + case f: Failure => if (definitive) this else this ++ f + } + + def either[B](b: => Result[B]): Result[Either[Nothing, B]] = b match { + case Value(v) => Value(Right(v)) + case f: Failure => if (definitive) this else this ++ f + } + + def filter(f: Nothing => Boolean, msg: => String) = this + def app[B, C](b: => Result[B])(f: (Nothing, B) => C): Result[C] = this + def &&(b: => Result[_]) = this + def toEither = Left(() => errors) + + private[sbt] def ++(f: Failure) = mkFailures(errors ++ f.errors) + } + + def mkFailures(errors: => Seq[String], definitive: Boolean = false): Failure = + new Failure(errors.distinct, definitive) + + def mkFailure(error: => String, definitive: Boolean = false): Failure = + new Failure(error :: Nil, definitive) + + def tuple[A, B](a: Option[A], b: Option[B]): Option[(A, B)] = + (a, b) match { case (Some(av), Some(bv)) => Some((av, bv)); case _ => None } + + def mapParser[A, B](a: Parser[A], f: A => B): Parser[B] = + a.ifValid { + a.result match { + case Some(av) => success(f(av)) + case None => + a match { + case m: MapParser[_, A] => m.map(f) + case _ => new MapParser(a, f) + } + } + } + + def bindParser[A, B](a: Parser[A], f: A => Parser[B]): Parser[B] = + a.ifValid { + a.result match { + case Some(av) => f(av) + case None => new BindParser(a, f) + } + } + + def filterParser[T]( + a: Parser[T], + f: T => Boolean, + seen: String, + msg: String => String + ): Parser[T] = + a.ifValid { + a.result match { + case Some(av) if f(av) => success(av) + case _ => new Filter(a, f, seen, msg) + } + } + + def seqParser[A, B](a: Parser[A], b: Parser[B]): Parser[(A, B)] = + a.ifValid { + b.ifValid { + (a.result, b.result) match { + case (Some(av), Some(bv)) => success((av, bv)) + case (Some(av), None) => b map (bv => (av, bv)) + case (None, Some(bv)) => a map (av => (av, bv)) + case (None, None) => new SeqParser(a, b) + } + } + } + + def choiceParser[A, B](a: Parser[A], b: Parser[B]): Parser[Either[A, B]] = + if (a.valid) + if (b.valid) new HetParser(a, b) else a.map(left.fn) + else + b.map(right.fn) + + def opt[T](a: Parser[T]): Parser[Option[T]] = + if (a.valid) new Optional(a) else success(None) + + def onFailure[T](delegate: Parser[T], msg: String): Parser[T] = + if (delegate.valid) new OnFailure(delegate, msg) else failure(msg) + + def trapAndFail[T](delegate: Parser[T]): Parser[T] = + delegate.ifValid(new TrapAndFail(delegate)) + + def zeroOrMore[T](p: Parser[T]): Parser[Seq[T]] = repeat(p, 0, Infinite) + def oneOrMore[T](p: Parser[T]): Parser[Seq[T]] = repeat(p, 1, Infinite) + + def repeat[T](p: Parser[T], min: Int = 0, max: UpperBound = Infinite): Parser[Seq[T]] = + repeat(None, p, min, max, Nil) + + private[complete] def repeat[T]( + partial: Option[Parser[T]], + repeated: Parser[T], + min: Int, + max: UpperBound, + revAcc: List[T] + ): Parser[Seq[T]] = { + assume(min >= 0, "Minimum must be greater than or equal to zero (was " + min + ")") + assume( + max >= min, + "Minimum must be less than or equal to maximum (min: " + min + ", max: " + max + ")" + ) + + def checkRepeated(invalidButOptional: => Parser[Seq[T]]): Parser[Seq[T]] = + repeated match { + case _: Invalid if min == 0 => invalidButOptional + case i: Invalid => i + case _ => + repeated.result match { + case Some(value) => + success(revAcc reverse_::: value :: Nil) // revAcc should be Nil here + case None => + if (max.isZero) success(revAcc.reverse) + else new Repeat(partial, repeated, min, max, revAcc) + } + } + + partial match { + case Some(part) => + part.ifValid { + part.result match { + case Some(value) => repeat(None, repeated, min, max, value :: revAcc) + case None => checkRepeated(part.map(lv => (lv :: revAcc).reverse)) + } + } + case None => checkRepeated(success(Nil)) + } + } + + def and[T](a: Parser[T], b: Parser[_]): Parser[T] = a.ifValid(b.ifValid(new And(a, b))) +} + +trait ParserMain { + + /** Provides combinators for Parsers.*/ + implicit def richParser[A](a: Parser[A]): RichParser[A] = new RichParser[A] { + def ~[B](b: Parser[B]) = seqParser(a, b) + def ||[B](b: Parser[B]) = choiceParser(a, b) + def |[B >: A](b: Parser[B]) = homParser[B](a, b) + def ? = opt(a) + def * = zeroOrMore(a) + def + = oneOrMore(a) + def map[B](f: A => B) = mapParser(a, f) + def id = a + + def ^^^[B](value: B): Parser[B] = a map (_ => value) + def ??[B >: A](alt: B): Parser[B] = a.? map { x => + x.getOrElse[B](alt) + } + def <~[B](b: Parser[B]): Parser[A] = (a ~ b) map { case av ~ _ => av } + def ~>[B](b: Parser[B]): Parser[B] = (a ~ b) map { case _ ~ bv => bv } + def !!!(msg: String): Parser[A] = onFailure(a, msg) + def failOnException: Parser[A] = trapAndFail(a) + + def &(o: Parser[_]) = and(a, o) + def examples(s: String*): Parser[A] = examples(s.toSet) + + def examples(s: Set[String], check: Boolean = false): Parser[A] = + examples(new FixedSetExamples(s), s.size, check) + + def examples( + s: ExampleSource, + maxNumberOfExamples: Int, + removeInvalidExamples: Boolean + ): Parser[A] = + Parser.examples(a, s, maxNumberOfExamples, removeInvalidExamples) + + def filter(f: A => Boolean, msg: String => String): Parser[A] = filterParser(a, f, "", msg) + def string(implicit ev: A <:< Seq[Char]): Parser[String] = map(_.mkString) + def flatMap[B](f: A => Parser[B]) = bindParser(a, f) + } + + implicit def literalRichCharParser(c: Char): RichParser[Char] = richParser(c) + implicit def literalRichStringParser(s: String): RichParser[String] = richParser(s) + + /** + * Construct a parser that is valid, but has no valid result. This is used as a way + * to provide a definitive Failure when a parser doesn't match empty input. For example, + * in `softFailure(...) | p`, if `p` doesn't match the empty sequence, the failure will come + * from the Parser constructed by the `softFailure` method. + */ + private[sbt] def softFailure(msg: => String, definitive: Boolean = false): Parser[Nothing] = + SoftInvalid(mkFailures(msg :: Nil, definitive)) + + /** + * Defines a parser that always fails on any input with messages `msgs`. + * If `definitive` is `true`, any failures by later alternatives are discarded. + */ + def invalid(msgs: => Seq[String], definitive: Boolean = false): Parser[Nothing] = + Invalid(mkFailures(msgs, definitive)) + + /** + * Defines a parser that always fails on any input with message `msg`. + * If `definitive` is `true`, any failures by later alternatives are discarded. + */ + def failure(msg: => String, definitive: Boolean = false): Parser[Nothing] = + invalid(msg :: Nil, definitive) + + /** Defines a parser that always succeeds on empty input with the result `value`.*/ + def success[T](value: T): Parser[T] = new ValidParser[T] { + override def result = Some(value) + def resultEmpty = Value(value) + def derive(c: Char) = Parser.failure("Expected end of input.") + def completions(level: Int) = Completions.empty + override def toString = "success(" + value + ")" + } + + /** Presents a Char range as a Parser. A single Char is parsed only if it is in the given range.*/ + implicit def range(r: collection.immutable.NumericRange[Char]): Parser[Char] = { + val label = r.map(_.toString).toString + range(r, label) + } + + /** Presents a Char range as a Parser. A single Char is parsed only if it is in the given range.*/ + def range(r: collection.immutable.NumericRange[Char], label: String): Parser[Char] = + charClass(r contains _, label).examples(r.map(_.toString): _*) + + /** Defines a Parser that parses a single character only if it is contained in `legal`.*/ + def chars(legal: String): Parser[Char] = { + val set = legal.toSet + charClass(set, "character in '" + legal + "'") examples (set.map(_.toString)) + } + + /** + * Defines a Parser that parses a single character only if the predicate `f` returns true for that character. + * If this parser fails, `label` is used as the failure message. + */ + def charClass(f: Char => Boolean, label: String = ""): Parser[Char] = + new CharacterClass(f, label) + + /** Presents a single Char `ch` as a Parser that only parses that exact character. */ + implicit def literal(ch: Char): Parser[Char] = new ValidParser[Char] { + def result = None + def resultEmpty = mkFailure("Expected '" + ch + "'") + def derive(c: Char) = if (c == ch) success(ch) else new Invalid(resultEmpty) + def completions(level: Int) = Completions.single(Completion.suggestion(ch.toString)) + override def toString = "'" + ch + "'" + } + + /** Presents a literal String `s` as a Parser that only parses that exact text and provides it as the result.*/ + implicit def literal(s: String): Parser[String] = stringLiteral(s, 0) + + /** See [[unapply]]. */ + object ~ { + + /** Convenience for destructuring a tuple that mirrors the `~` combinator.*/ + def unapply[A, B](t: (A, B)): Some[(A, B)] = Some(t) + + } + + /** Parses input `str` using `parser`. If successful, the result is provided wrapped in `Right`. If unsuccessful, an error message is provided in `Left`.*/ + def parse[T](str: String, parser: Parser[T]): Either[String, T] = + Parser.result(parser, str).left.map { failures => + val (msgs, pos) = failures() + ProcessError(str, msgs, pos) + } + + /** + * Convenience method to use when developing a parser. + * `parser` is applied to the input `str`. + * If `completions` is true, the available completions for the input are displayed. + * Otherwise, the result of parsing is printed using the result's `toString` method. + * If parsing fails, the error message is displayed. + * + * See also [[sampleParse]] and [[sampleCompletions]]. + */ + def sample(str: String, parser: Parser[_], completions: Boolean = false): Unit = + if (completions) sampleCompletions(str, parser) else sampleParse(str, parser) + + /** + * Convenience method to use when developing a parser. + * `parser` is applied to the input `str` and the result of parsing is printed using the result's `toString` method. + * If parsing fails, the error message is displayed. + */ + def sampleParse(str: String, parser: Parser[_]): Unit = + parse(str, parser) match { + case Left(msg) => println(msg) + case Right(v) => println(v) + } + + /** + * Convenience method to use when developing a parser. + * `parser` is applied to the input `str` and the available completions are displayed on separate lines. + * If parsing fails, the error message is displayed. + */ + def sampleCompletions(str: String, parser: Parser[_], level: Int = 1): Unit = + Parser.completions(parser, str, level).get foreach println + + // intended to be temporary pending proper error feedback + def result[T](p: Parser[T], s: String): Either[() => (Seq[String], Int), T] = { + def loop(i: Int, a: Parser[T]): Either[() => (Seq[String], Int), T] = + a match { + case Invalid(f) => Left(() => (f.errors, i)) + case _ => + val ci = i + 1 + if (ci >= s.length) + a.resultEmpty.toEither.left.map { msgs0 => () => + val msgs = msgs0() + val nonEmpty = if (msgs.isEmpty) Seq("Unexpected end of input") else msgs + (nonEmpty, ci) + } else + loop(ci, a derive s(ci)) + } + loop(-1, p) + } + + /** Applies parser `p` to input `s`. */ + def apply[T](p: Parser[T])(s: String): Parser[T] = + s.foldLeft(p)(derive1) + + /** Applies parser `p` to a single character of input. */ + def derive1[T](p: Parser[T], c: Char): Parser[T] = + if (p.valid) p.derive(c) else p + + /** + * Applies parser `p` to input `s` and returns the completions at verbosity `level`. + * The interpretation of `level` is up to parser definitions, but 0 is the default by convention, + * with increasing positive numbers corresponding to increasing verbosity. Typically no more than + * a few levels are defined. + */ + def completions(p: Parser[_], s: String, level: Int): Completions = + // The x Completions.empty removes any trailing token completions where append.isEmpty + apply(p)(s).completions(level) x Completions.empty + + def examples[A](a: Parser[A], completions: Set[String], check: Boolean = false): Parser[A] = + examples(a, new FixedSetExamples(completions), completions.size, check) + + /** + * @param a the parser to decorate with a source of examples. All validation and parsing is delegated to this parser, + * only [[Parser.completions]] is modified. + * @param completions the source of examples when displaying completions to the user. + * @param maxNumberOfExamples limits the number of examples that the source of examples should return. This can + * prevent lengthy pauses and avoids bad interactive user experience. + * @param removeInvalidExamples indicates whether completion examples should be checked for validity (against the given parser). An + * exception is thrown if the example source contains no valid completion suggestions. + * @tparam A the type of values that are returned by the parser. + * @return + */ + def examples[A]( + a: Parser[A], + completions: ExampleSource, + maxNumberOfExamples: Int, + removeInvalidExamples: Boolean + ): Parser[A] = + if (a.valid) { + a.result match { + case Some(av) => success(av) + case None => + new ParserWithExamples(a, completions, maxNumberOfExamples, removeInvalidExamples) + } + } else a + + def matched( + t: Parser[_], + seen: Vector[Char] = Vector.empty, + partial: Boolean = false + ): Parser[String] = + t match { + case i: Invalid => if (partial && seen.nonEmpty) success(seen.mkString) else i + case _ => + if (t.result.isEmpty) + new MatchedString(t, seen, partial) + else + success(seen.mkString) + } + + /** + * Establishes delegate parser `t` as a single token of tab completion. + * When tab completion of part of this token is requested, the completions provided by the delegate `t` or a later derivative are appended to + * the prefix String already seen by this parser. + */ + def token[T](t: Parser[T]): Parser[T] = token(t, TokenCompletions.default) + + /** + * Establishes delegate parser `t` as a single token of tab completion. + * When tab completion of part of this token is requested, no completions are returned if `hide` returns true for the current tab completion level. + * Otherwise, the completions provided by the delegate `t` or a later derivative are appended to the prefix String already seen by this parser. + */ + def token[T](t: Parser[T], hide: Int => Boolean): Parser[T] = + token(t, TokenCompletions.default.hideWhen(hide)) + + /** + * Establishes delegate parser `t` as a single token of tab completion. + * When tab completion of part of this token is requested, `description` is displayed for suggestions and no completions are ever performed. + */ + def token[T](t: Parser[T], description: String): Parser[T] = + token(t, TokenCompletions.displayOnly(description)) + + /** + * Establishes delegate parser `t` as a single token of tab completion. + * When tab completion of part of this token is requested, `display` is used as the printed suggestion, but the completions from the delegate + * parser `t` are used to complete if unambiguous. + */ + def tokenDisplay[T](t: Parser[T], display: String): Parser[T] = + token(t, TokenCompletions.overrideDisplay(display)) + + def token[T](t: Parser[T], complete: TokenCompletions): Parser[T] = + mkToken(t, "", complete) + + private[sbt] def mkToken[T](t: Parser[T], seen: String, complete: TokenCompletions): Parser[T] = + if (t.valid && !t.isTokenStart) + if (t.result.isEmpty) new TokenStart(t, seen, complete) else t + else + t + + def homParser[A](a: Parser[A], b: Parser[A]): Parser[A] = (a, b) match { + case (Invalid(af), Invalid(bf)) => Invalid(af ++ bf) + case (Invalid(_), bv) => bv + case (av, Invalid(_)) => av + case (_, _) => new HomParser(a, b) + } + + def not(p: Parser[_], failMessage: String): Parser[Unit] = p.result match { + case None => new Not(p, failMessage) + case Some(_) => failure(failMessage) + } + + def oneOf[T](p: Seq[Parser[T]]): Parser[T] = p.reduceLeft(_ | _) + def seq[T](p: Seq[Parser[T]]): Parser[Seq[T]] = seq0(p, Nil) + + def seq0[T](p: Seq[Parser[T]], errors: => Seq[String]): Parser[Seq[T]] = { + val (newErrors, valid) = separate(p) { + case Invalid(f) => Left(f.errors _): Either[() => Seq[String], Parser[T]] + case ok => Right(ok): Either[() => Seq[String], Parser[T]] + } + def combinedErrors = errors ++ newErrors.flatMap(_()) + if (valid.isEmpty) invalid(combinedErrors) else new ParserSeq(valid, combinedErrors) + } + + def stringLiteral(s: String, start: Int): Parser[String] = { + val len = s.length + if (len == 0) sys.error("String literal cannot be empty") + else if (start >= len) success(s) + else new StringLiteral(s, start) + } +} + +sealed trait ValidParser[T] extends Parser[T] { + final def valid = true + final def failure = None + final def ifValid[S](p: => Parser[S]): Parser[S] = p +} + +private final case class Invalid(fail: Failure) extends Parser[Nothing] { + def failure = Some(fail) + def result = None + def resultEmpty = fail + def derive(c: Char) = sys.error("Invalid.") + def completions(level: Int) = Completions.nil + override def toString = fail.errors.mkString("; ") + def valid = false + def ifValid[S](p: => Parser[S]): Parser[S] = this +} + +private final case class SoftInvalid(fail: Failure) extends ValidParser[Nothing] { + def result = None + def resultEmpty = fail + def derive(c: Char) = Invalid(fail) + def completions(level: Int) = Completions.nil + override def toString = fail.errors.mkString("; ") +} + +private final class TrapAndFail[A](a: Parser[A]) extends ValidParser[A] { + def result = + try { + a.result + } catch { case _: Exception => None } + def resultEmpty = + try { + a.resultEmpty + } catch { case e: Exception => fail(e) } + + def derive(c: Char) = + try { + trapAndFail(a derive c) + } catch { + case e: Exception => Invalid(fail(e)) + } + + def completions(level: Int) = + try { + a.completions(level) + } catch { + case _: Exception => Completions.nil + } + + override def toString = "trap(" + a + ")" + override def isTokenStart = a.isTokenStart + private[this] def fail(e: Exception): Failure = mkFailure(e.toString) +} + +private final class OnFailure[A](a: Parser[A], message: String) extends ValidParser[A] { + def result = a.result + + def resultEmpty = a.resultEmpty match { + case _: Failure => mkFailure(message); case v: Value[A] => v + } + + def derive(c: Char) = onFailure(a derive c, message) + def completions(level: Int) = a.completions(level) + override def toString = "(" + a + " !!! \"" + message + "\" )" + override def isTokenStart = a.isTokenStart +} + +private final class SeqParser[A, B](a: Parser[A], b: Parser[B]) extends ValidParser[(A, B)] { + lazy val result = tuple(a.result, b.result) + lazy val resultEmpty = a.resultEmpty seq b.resultEmpty + + def derive(c: Char) = { + val common = a.derive(c) ~ b + a.resultEmpty match { + case Value(av) => common | b.derive(c).map(br => (av, br)) + case _: Failure => common + } + } + + def completions(level: Int) = a.completions(level) x b.completions(level) + override def toString = "(" + a + " ~ " + b + ")" +} + +private final class HomParser[A](a: Parser[A], b: Parser[A]) extends ValidParser[A] { + lazy val result = tuple(a.result, b.result) map (_._1) + def derive(c: Char) = (a derive c) | (b derive c) + lazy val resultEmpty = a.resultEmpty or b.resultEmpty + def completions(level: Int) = a.completions(level) ++ b.completions(level) + override def toString = "(" + a + " | " + b + ")" +} + +private final class HetParser[A, B](a: Parser[A], b: Parser[B]) extends ValidParser[Either[A, B]] { + lazy val result = tuple(a.result, b.result) map { case (a, _) => Left(a) } + def derive(c: Char) = (a derive c) || (b derive c) + lazy val resultEmpty = a.resultEmpty either b.resultEmpty + def completions(level: Int) = a.completions(level) ++ b.completions(level) + override def toString = "(" + a + " || " + b + ")" +} + +private final class ParserSeq[T](a: Seq[Parser[T]], errors: => Seq[String]) + extends ValidParser[Seq[T]] { + assert(a.nonEmpty) + + lazy val resultEmpty: Result[Seq[T]] = { + val res = a.map(_.resultEmpty) + val (failures, values) = separate(res)(_.toEither) + // if(failures.isEmpty) Value(values) else mkFailures(failures.flatMap(_()) ++ errors) + if (values.nonEmpty) Value(values) else mkFailures(failures.flatMap(_()) ++ errors) + } + + def result = { + val success = a.flatMap(_.result) + if (success.length == a.length) Some(success) else None + } + + def completions(level: Int) = a.map(_.completions(level)).reduceLeft(_ ++ _) + def derive(c: Char) = seq0(a.map(_ derive c), errors) + + override def toString = "seq(" + a + ")" +} + +private final class BindParser[A, B](a: Parser[A], f: A => Parser[B]) extends ValidParser[B] { + lazy val result = a.result flatMap (av => f(av).result) + lazy val resultEmpty = a.resultEmpty flatMap (av => f(av).resultEmpty) + + def completions(level: Int) = + a.completions(level) flatMap { c => + apply(a)(c.append).resultEmpty match { + case _: Failure => Completions.strict(Set.empty + c) + case Value(av) => c x f(av).completions(level) + } + } + + def derive(c: Char) = { + val common = a derive c flatMap f + a.resultEmpty match { + case Value(av) => common | derive1(f(av), c) + case _: Failure => common + } + } + + override def isTokenStart = a.isTokenStart + + override def toString = "bind(" + a + ")" +} + +private final class MapParser[A, B](a: Parser[A], f: A => B) extends ValidParser[B] { + lazy val result = a.result map f + lazy val resultEmpty = a.resultEmpty map f + def derive(c: Char) = (a derive c) map f + def completions(level: Int) = a.completions(level) + override def isTokenStart = a.isTokenStart + override def toString = "map(" + a + ")" + def map[C](g: B => C) = new MapParser[A, C](a, f.andThen(g)) +} + +private final class Filter[T](p: Parser[T], f: T => Boolean, seen: String, msg: String => String) + extends ValidParser[T] { + def filterResult(r: Result[T]) = r.filter(f, msg(seen)) + lazy val result = p.result filter f + lazy val resultEmpty = filterResult(p.resultEmpty) + def derive(c: Char) = filterParser(p derive c, f, seen + c, msg) + + def completions(level: Int) = p.completions(level) filterS { s => + filterResult(apply(p)(s).resultEmpty).isValid + } + + override def toString = "filter(" + p + ")" + override def isTokenStart = p.isTokenStart +} + +private final class MatchedString(delegate: Parser[_], seenV: Vector[Char], partial: Boolean) + extends ValidParser[String] { + lazy val seen = seenV.mkString + def derive(c: Char) = matched(delegate derive c, seenV :+ c, partial) + def completions(level: Int) = delegate.completions(level) + def result = if (delegate.result.isDefined) Some(seen) else None + + def resultEmpty = delegate.resultEmpty match { + case f: Failure if !partial => f; case _ => Value(seen) + } + + override def isTokenStart = delegate.isTokenStart + override def toString = "matched(" + partial + ", " + seen + ", " + delegate + ")" +} + +private final class TokenStart[T](delegate: Parser[T], seen: String, complete: TokenCompletions) + extends ValidParser[T] { + def derive(c: Char) = mkToken(delegate derive c, seen + c, complete) + + def completions(level: Int) = complete match { + case dc: TokenCompletions.Delegating => + dc.completions(seen, level, delegate.completions(level)) + case fc: TokenCompletions.Fixed => fc.completions(seen, level) + } + + def result = delegate.result + def resultEmpty = delegate.resultEmpty + override def isTokenStart = true + override def toString = "token('" + complete + ", " + delegate + ")" +} + +private final class And[T](a: Parser[T], b: Parser[_]) extends ValidParser[T] { + lazy val result = tuple(a.result, b.result) map { _._1 } + def derive(c: Char) = (a derive c) & (b derive c) + def completions(level: Int) = a.completions(level).filterS(s => apply(b)(s).resultEmpty.isValid) + lazy val resultEmpty = a.resultEmpty && b.resultEmpty + override def toString = "(%s) && (%s)".format(a, b) +} + +private final class Not(delegate: Parser[_], failMessage: String) extends ValidParser[Unit] { + def derive(c: Char) = if (delegate.valid) not(delegate derive c, failMessage) else this + def completions(level: Int) = Completions.empty + def result = None + + lazy val resultEmpty = delegate.resultEmpty match { + case _: Failure => Value(()) + case _: Value[_] => mkFailure(failMessage) + } + + override def toString = " -(%s)".format(delegate) +} + +/** + * This class wraps an existing parser (the delegate), and replaces the delegate's completions with examples from + * the given example source. + * + * This class asks the example source for a limited amount of examples (to prevent lengthy and expensive + * computations and large amounts of allocated data). It then passes these examples on to the UI. + * + * @param delegate the parser to decorate with completion examples (i.e., completion of user input). + * @param exampleSource the source from which this class will take examples (potentially filter them with the delegate + * parser), and pass them to the UI. + * @param maxNumberOfExamples the maximum number of completions to read from the example source and pass to the UI. This + * limit prevents lengthy example generation and allocation of large amounts of memory. + * @param removeInvalidExamples indicates whether to remove examples that are deemed invalid by the delegate parser. + * @tparam T the type of value produced by the parser. + */ +private final class ParserWithExamples[T]( + delegate: Parser[T], + exampleSource: ExampleSource, + maxNumberOfExamples: Int, + removeInvalidExamples: Boolean +) extends ValidParser[T] { + + def derive(c: Char) = + examples( + delegate derive c, + exampleSource.withAddedPrefix(c.toString), + maxNumberOfExamples, + removeInvalidExamples + ) + + def result = delegate.result + + lazy val resultEmpty = delegate.resultEmpty + + def completions(level: Int) = { + if (exampleSource().isEmpty) + if (resultEmpty.isValid) Completions.nil else Completions.empty + else { + val examplesBasedOnTheResult = filteredExamples.take(maxNumberOfExamples).toSet + Completions(examplesBasedOnTheResult.map(ex => Completion.suggestion(ex))) + } + } + + override def toString = "examples(" + delegate + ", " + exampleSource().take(2).toList + ")" + + private def filteredExamples: Iterable[String] = { + if (removeInvalidExamples) + exampleSource().filter(isExampleValid) + else + exampleSource() + } + + private def isExampleValid(example: String): Boolean = { + apply(delegate)(example).resultEmpty.isValid + } +} + +private final class StringLiteral(str: String, start: Int) extends ValidParser[String] { + assert(0 <= start && start < str.length) + + def failMsg = "Expected '" + str + "'" + def resultEmpty = mkFailure(failMsg) + def result = None + + def derive(c: Char) = + if (str.charAt(start) == c) stringLiteral(str, start + 1) else new Invalid(resultEmpty) + + def completions(level: Int) = Completions.single(Completion.suggestion(str.substring(start))) + override def toString = '"' + str + '"' +} + +private final class CharacterClass(f: Char => Boolean, label: String) extends ValidParser[Char] { + def result = None + def resultEmpty = mkFailure("Expected " + label) + def derive(c: Char) = if (f(c)) success(c) else Invalid(resultEmpty) + def completions(level: Int) = Completions.empty + override def toString = "class(" + label + ")" +} + +private final class Optional[T](delegate: Parser[T]) extends ValidParser[Option[T]] { + def result = delegate.result map some.fn + def resultEmpty = Value(None) + def derive(c: Char) = (delegate derive c).map(some.fn) + def completions(level: Int) = Completion.empty +: delegate.completions(level) + override def toString = delegate.toString + "?" +} + +private final class Repeat[T]( + partial: Option[Parser[T]], + repeated: Parser[T], + min: Int, + max: UpperBound, + accumulatedReverse: List[T] +) extends ValidParser[Seq[T]] { + assume(0 <= min, "Minimum occurrences must be non-negative") + assume(max >= min, "Minimum occurrences must be less than the maximum occurrences") + + def derive(c: Char) = + partial match { + case Some(part) => + val partD = repeat(Some(part derive c), repeated, min, max, accumulatedReverse) + part.resultEmpty match { + case Value(pv) => partD | repeatDerive(c, pv :: accumulatedReverse) + case _: Failure => partD + } + case None => repeatDerive(c, accumulatedReverse) + } + + def repeatDerive(c: Char, accRev: List[T]): Parser[Seq[T]] = + repeat(Some(repeated derive c), repeated, scala.math.max(0, min - 1), max.decrement, accRev) + + def completions(level: Int) = { + def pow(comp: Completions, exp: Completions, n: Int): Completions = + if (n == 1) comp else pow(comp x exp, exp, n - 1) + + val repC = repeated.completions(level) + val fin = if (min == 0) Completion.empty +: repC else pow(repC, repC, min) + partial match { + case Some(p) => p.completions(level) x fin + case None => fin + } + } + + def result = None + + lazy val resultEmpty: Result[Seq[T]] = { + val partialAccumulatedOption = + partial match { + case None => (Value(accumulatedReverse): Result[List[T]]) + case Some(partialPattern) => + partialPattern.resultEmpty.map(_ :: accumulatedReverse) + } + (partialAccumulatedOption app repeatedParseEmpty)((x, y) => (x reverse_::: y): Seq[T]) + } + + private def repeatedParseEmpty: Result[List[T]] = { + if (min == 0) + Value(Nil) + else + // forced determinism + for (value <- repeated.resultEmpty) yield makeList(min, value) + } + + override def toString = "repeat(" + min + "," + max + "," + partial + "," + repeated + ")" +} diff --git a/internal/util-complete/src/main/scala/sbt/internal/util/complete/Parsers.scala b/internal/util-complete/src/main/scala/sbt/internal/util/complete/Parsers.scala new file mode 100644 index 000000000..651ddd8d0 --- /dev/null +++ b/internal/util-complete/src/main/scala/sbt/internal/util/complete/Parsers.scala @@ -0,0 +1,364 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util +package complete + +import Parser._ +import java.io.File +import java.net.URI +import java.lang.Character.{ + CURRENCY_SYMBOL, + DASH_PUNCTUATION, + MATH_SYMBOL, + MODIFIER_SYMBOL, + OTHER_PUNCTUATION, + OTHER_SYMBOL, + getType +} + +import scala.annotation.tailrec +import sbt.internal.util.Util.nilSeq + +/** Provides standard implementations of commonly useful [[Parser]]s. */ +trait Parsers { + + /** Matches the end of input, providing no useful result on success. */ + lazy val EOF = not(any, "Expected EOF") + + /** Parses any single character and provides that character as the result. */ + lazy val any: Parser[Char] = charClass(_ => true, "any character") + + /** Set that contains each digit in a String representation.*/ + lazy val DigitSet = Set("0", "1", "2", "3", "4", "5", "6", "7", "8", "9") + + /** Parses any single digit and provides that digit as a Char as the result.*/ + lazy val Digit = charClass(_.isDigit, "digit") examples DigitSet + + /** Set containing Chars for hexadecimal digits 0-9 and A-F (but not a-f). */ + lazy val HexDigitSet = + Set('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F') + + /** Parses a single hexadecimal digit (0-9, a-f, A-F). */ + lazy val HexDigit = charClass(c => HexDigitSet(c.toUpper), "hex digit") examples HexDigitSet.map( + _.toString + ) + + /** Parses a single letter, according to Char.isLetter, into a Char. */ + lazy val Letter = charClass(_.isLetter, "letter") + + /** Parses a single letter, according to Char.isUpper, into a Char. */ + lazy val Upper = charClass(_.isUpper, "upper") + + /** Parses a single letter, according to Char.isLower, into a Char. */ + lazy val Lower = charClass(_.isLower, "lower") + + /** Parses the first Char in an sbt identifier, which must be a [[Letter]].*/ + def IDStart = Letter + + /** Parses an identifier Char other than the first character. This includes letters, digits, dash `-`, and underscore `_`.*/ + lazy val IDChar = charClass(isIDChar, "ID character") + + /** Parses an identifier String, which must start with [[IDStart]] and contain zero or more [[IDChar]]s after that. */ + lazy val ID = identifier(IDStart, IDChar) + + /** Parses a single operator Char, as allowed by [[isOpChar]]. */ + lazy val OpChar = charClass(isOpChar, "symbol") + + /** Parses a non-empty operator String, which consists only of characters allowed by [[OpChar]]. */ + lazy val Op = OpChar.+.string + + /** Parses either an operator String defined by [[Op]] or a non-symbolic identifier defined by [[ID]]. */ + lazy val OpOrID = ID | Op + + /** Parses a single, non-symbolic Scala identifier Char. Valid characters are letters, digits, and the underscore character `_`. */ + lazy val ScalaIDChar = charClass(isScalaIDChar, "Scala identifier character") + + /** Parses a non-symbolic Scala-like identifier. The identifier must start with [[IDStart]] and contain zero or more [[ScalaIDChar]]s after that.*/ + lazy val ScalaID = identifier(IDStart, ScalaIDChar) + + /** Parses a non-symbolic Scala-like identifier. The identifier must start with [[Upper]] and contain zero or more [[ScalaIDChar]]s after that.*/ + lazy val CapitalizedID = identifier(Upper, ScalaIDChar) + + /** Parses a String that starts with `start` and is followed by zero or more characters parsed by `rep`.*/ + def identifier(start: Parser[Char], rep: Parser[Char]): Parser[String] = + start ~ rep.* map { case x ~ xs => (x +: xs).mkString } + + def opOrIDSpaced(s: String): Parser[Char] = + if (DefaultParsers.matches(ID, s)) + OpChar | SpaceClass + else if (DefaultParsers.matches(Op, s)) + IDChar | SpaceClass + else + any + + /** Returns true if `c` an operator character. */ + def isOpChar(c: Char) = !isDelimiter(c) && isOpType(getType(c)) + + def isOpType(cat: Int) = cat match { + case MATH_SYMBOL | OTHER_SYMBOL | DASH_PUNCTUATION | OTHER_PUNCTUATION | MODIFIER_SYMBOL | + CURRENCY_SYMBOL => + true; case _ => false + } + + /** Returns true if `c` is a dash `-`, a letter, digit, or an underscore `_`. */ + def isIDChar(c: Char) = isScalaIDChar(c) || c == '-' + + /** Returns true if `c` is a letter, digit, or an underscore `_`. */ + def isScalaIDChar(c: Char) = c.isLetterOrDigit || c == '_' + + def isDelimiter(c: Char) = c match { + case '`' | '\'' | '\"' | /*';' | */ ',' | '.' => true; case _ => false + } + + /** Matches a single character that is not a whitespace character. */ + lazy val NotSpaceClass = charClass(!_.isWhitespace, "non-whitespace character") + + /** Matches a single whitespace character, as determined by Char.isWhitespace.*/ + lazy val SpaceClass = charClass(_.isWhitespace, "whitespace character") + + /** Matches a non-empty String consisting of non-whitespace characters. */ + lazy val NotSpace = NotSpaceClass.+.string + + /** Matches a possibly empty String consisting of non-whitespace characters. */ + lazy val OptNotSpace = NotSpaceClass.*.string + + /** + * Matches a non-empty String consisting of whitespace characters. + * The suggested tab completion is a single, constant space character. + */ + lazy val Space: Parser[Seq[Char]] = SpaceClass.+.examples(" ") + + /** + * Matches a possibly empty String consisting of whitespace characters. + * The suggested tab completion is a single, constant space character. + */ + lazy val OptSpace = SpaceClass.*.examples(" ") + + /** Parses a non-empty String that contains only valid URI characters, as defined by [[URIChar]].*/ + lazy val URIClass = URIChar.+.string !!! "Invalid URI" + + /** Triple-quotes, as used for verbatim quoting.*/ + lazy val VerbatimDQuotes = "\"\"\"" + + /** Double quote character. */ + lazy val DQuoteChar = '\"' + + /** Backslash character. */ + lazy val BackslashChar = '\\' + + /** Matches a single double quote. */ + lazy val DQuoteClass = charClass(_ == DQuoteChar, "double-quote character") + + /** Matches any character except a double quote or whitespace. */ + lazy val NotDQuoteSpaceClass = + charClass({ c: Char => + (c != DQuoteChar) && !c.isWhitespace + }, "non-double-quote-space character") + + /** Matches any character except a double quote or backslash. */ + lazy val NotDQuoteBackslashClass = + charClass({ c: Char => + (c != DQuoteChar) && (c != BackslashChar) + }, "non-double-quote-backslash character") + + /** Matches a single character that is valid somewhere in a URI. */ + lazy val URIChar = charClass(alphanum, "alphanum") | chars("_-!.~'()*,;:$&+=?/[]@%#") + + /** Returns true if `c` is an ASCII letter or digit. */ + def alphanum(c: Char) = + ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || ('0' <= c && c <= '9') + + /** + * @param base the directory used for completion proposals (when the user presses the TAB key). Only paths under this + * directory will be proposed. + * @return the file that was parsed from the input string. The returned path may or may not exist. + */ + def fileParser(base: File): Parser[File] = + OptSpace ~> StringBasic + .examples(new FileExamples(base)) + .map(new File(_)) + + /** Parses a port number. Currently, this accepts any integer and presents a tab completion suggestion of ``. */ + lazy val Port = token(IntBasic, "") + + /** Parses a signed integer. */ + lazy val IntBasic = mapOrFail('-'.? ~ Digit.+)(Function.tupled(toInt)) + + /** Parses an unsigned integer. */ + lazy val NatBasic = mapOrFail(Digit.+)(_.mkString.toInt) + + private[this] def toInt(neg: Option[Char], digits: Seq[Char]): Int = + (neg.toSeq ++ digits).mkString.toInt + + /** Parses the lower-case values `true` and `false` into their corresponding Boolean values. */ + lazy val Bool = ("true" ^^^ true) | ("false" ^^^ false) + + /** + * Parses a potentially quoted String value. The value may be verbatim quoted ([[StringVerbatim]]), + * quoted with interpreted escapes ([[StringEscapable]]), or unquoted ([[NotQuoted]]). + */ + lazy val StringBasic = StringVerbatim | StringEscapable | NotQuoted + + /** + * Parses a verbatim quoted String value, discarding the quotes in the result. This kind of quoted text starts with triple quotes `"""` + * and ends at the next triple quotes and may contain any character in between. + */ + lazy val StringVerbatim: Parser[String] = VerbatimDQuotes ~> + any.+.string.filter(!_.contains(VerbatimDQuotes), _ => "Invalid verbatim string") <~ + VerbatimDQuotes + + /** + * Parses a string value, interpreting escapes and discarding the surrounding quotes in the result. + * See [[EscapeSequence]] for supported escapes. + */ + lazy val StringEscapable: Parser[String] = + (DQuoteChar ~> (NotDQuoteBackslashClass | EscapeSequence).+.string <~ DQuoteChar | + (DQuoteChar ~ DQuoteChar) ^^^ "") + + /** + * Parses a size unit string. For example, `128K` parsers to `128L * 1024`, and `1.25g` parses + * to `1024L * 1024 * 1024 * 5 / 4`. + */ + lazy val Size: Parser[Long] = SizeParser.value + + /** + * Parses a brace enclosed string and, if each opening brace is matched with a closing brace, + * it returns the entire string including the braces. + * + * @param open the opening character, e.g. '{' + * @param close the closing character, e.g. '}' + * @return a parser for the brace encloosed string. + */ + private[sbt] def braces(open: Char, close: Char): Parser[String] = { + val notDelim = charClass(c => c != open && c != close).*.string + def impl(): Parser[String] = { + (open ~ (notDelim ~ close).?).flatMap { + case (l, Some((content, r))) => Parser.success(l + content + r) + case (l, None) => + ((notDelim ~ impl()).map { + case (leftPrefix, nestedBraces) => leftPrefix + nestedBraces + }.+ ~ notDelim ~ close).map { + case ((nested, suffix), r) => l + nested.mkString + suffix + r + } + } + } + impl() + } + + /** + * Parses a single escape sequence into the represented Char. + * Escapes start with a backslash and are followed by `u` for a [[UnicodeEscape]] or by `b`, `t`, `n`, `f`, `r`, `"`, `'`, `\` for standard escapes. + */ + lazy val EscapeSequence: Parser[Char] = + BackslashChar ~> ('b' ^^^ '\b' | 't' ^^^ '\t' | 'n' ^^^ '\n' | 'f' ^^^ '\f' | 'r' ^^^ '\r' | + '\"' ^^^ '\"' | '\'' ^^^ '\'' | '\\' ^^^ '\\' | UnicodeEscape) + + /** + * Parses a single unicode escape sequence into the represented Char. + * A unicode escape begins with a backslash, followed by a `u` and 4 hexadecimal digits representing the unicode value. + */ + lazy val UnicodeEscape: Parser[Char] = + ("u" ~> repeat(HexDigit, 4, 4)) map { seq => + Integer.parseInt(seq.mkString, 16).toChar + } + + /** Parses an unquoted, non-empty String value that cannot start with a double quote and cannot contain whitespace.*/ + lazy val NotQuoted = (NotDQuoteSpaceClass ~ OptNotSpace) map { case (c, s) => c.toString + s } + + /** + * Applies `rep` zero or more times, separated by `sep`. + * The result is the (possibly empty) sequence of results from the multiple `rep` applications. The `sep` results are discarded. + */ + def repsep[T](rep: Parser[T], sep: Parser[_]): Parser[Seq[T]] = + rep1sep(rep, sep) ?? nilSeq[T] + + /** + * Applies `rep` one or more times, separated by `sep`. + * The result is the non-empty sequence of results from the multiple `rep` applications. The `sep` results are discarded. + */ + def rep1sep[T](rep: Parser[T], sep: Parser[_]): Parser[Seq[T]] = + (rep ~ (sep ~> rep).*).map { case (x ~ xs) => x +: xs } + + /** Wraps the result of `p` in `Some`.*/ + def some[T](p: Parser[T]): Parser[Option[T]] = p map { v => + Some(v) + } + + /** + * Applies `f` to the result of `p`, transforming any exception when evaluating + * `f` into a parse failure with the exception `toString` as the message. + */ + def mapOrFail[S, T](p: Parser[S])(f: S => T): Parser[T] = + p flatMap { s => + try { + success(f(s)) + } catch { case e: Exception => failure(e.toString) } + } + + /** + * Parses a space-delimited, possibly empty sequence of arguments. + * The arguments may use quotes and escapes according to [[StringBasic]]. + */ + def spaceDelimited(display: String): Parser[Seq[String]] = + (token(Space) ~> token(StringBasic, display)).* <~ SpaceClass.* + + /** Applies `p` and uses `true` as the result if it succeeds and turns failure into a result of `false`. */ + def flag[T](p: Parser[T]): Parser[Boolean] = (p ^^^ true) ?? false + + /** + * Defines a sequence parser where the parser used for each part depends on the previously parsed values. + * `p` is applied to the (possibly empty) sequence of already parsed values to obtain the next parser to use. + * The parsers obtained in this way are separated by `sep`, whose result is discarded and only the sequence + * of values from the parsers returned by `p` is used for the result. + */ + def repeatDep[A](p: Seq[A] => Parser[A], sep: Parser[Any]): Parser[Seq[A]] = { + def loop(acc: Seq[A]): Parser[Seq[A]] = { + val next = (sep ~> p(acc)) flatMap { result => + loop(acc :+ result) + } + next ?? acc + } + p(Vector()) flatMap { first => + loop(Seq(first)) + } + } + + /** Applies String.trim to the result of `p`. */ + def trimmed(p: Parser[String]) = p map { _.trim } + + /** Parses a URI that is valid according to the single argument java.net.URI constructor. */ + lazy val basicUri = mapOrFail(URIClass)(uri => new URI(uri)) + + /** Parses a URI that is valid according to the single argument java.net.URI constructor, using `ex` as tab completion examples. */ + def Uri(ex: Set[URI]) = basicUri examples (ex.map(_.toString)) +} + +/** Provides standard [[Parser]] implementations. */ +object Parsers extends Parsers + +/** Provides common [[Parser]] implementations and helper methods.*/ +object DefaultParsers extends Parsers with ParserMain { + + /** Applies parser `p` to input `s` and returns `true` if the parse was successful. */ + def matches(p: Parser[_], s: String): Boolean = + apply(p)(s).resultEmpty.isValid + + /** Returns `true` if `s` parses successfully according to [[ID]].*/ + def validID(s: String): Boolean = { + // Handwritten version of `matches(ID, s)` because validID turned up in profiling. + def isIdChar(c: Char): Boolean = Character.isLetterOrDigit(c) || (c == '-') || (c == '_') + @tailrec def isRestIdChar(cur: Int, s: String, length: Int): Boolean = + if (cur < length) + isIdChar(s.charAt(cur)) && isRestIdChar(cur + 1, s, length) + else + true + + !s.isEmpty && Character.isLetter(s.charAt(0)) && isRestIdChar(1, s, s.length) + } + +} diff --git a/internal/util-complete/src/main/scala/sbt/internal/util/complete/ProcessError.scala b/internal/util-complete/src/main/scala/sbt/internal/util/complete/ProcessError.scala new file mode 100644 index 000000000..1d3a540bf --- /dev/null +++ b/internal/util-complete/src/main/scala/sbt/internal/util/complete/ProcessError.scala @@ -0,0 +1,38 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util +package complete + +object ProcessError { + def apply(command: String, msgs: Seq[String], index: Int): String = { + val (line, modIndex) = extractLine(command, index) + val point = pointerSpace(command, modIndex) + msgs.mkString("\n") + "\n" + line + "\n" + point + "^" + } + + def extractLine(s: String, i: Int): (String, Int) = { + val notNewline = (c: Char) => c != '\n' && c != '\r' + val left = takeRightWhile(s.substring(0, i))(notNewline) + val right = s substring i takeWhile notNewline + (left + right, left.length) + } + + def takeRightWhile(s: String)(pred: Char => Boolean): String = { + def loop(i: Int): String = + if (i < 0) + s + else if (pred(s(i))) + loop(i - 1) + else + s.substring(i + 1) + loop(s.length - 1) + } + + def pointerSpace(s: String, i: Int): String = + (s take i) map { case '\t' => '\t'; case _ => ' ' } mkString "" +} diff --git a/internal/util-complete/src/main/scala/sbt/internal/util/complete/SizeParser.scala b/internal/util-complete/src/main/scala/sbt/internal/util/complete/SizeParser.scala new file mode 100644 index 000000000..1ca63efbe --- /dev/null +++ b/internal/util-complete/src/main/scala/sbt/internal/util/complete/SizeParser.scala @@ -0,0 +1,56 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util.complete + +import sbt.internal.util.complete.DefaultParsers._ + +private[sbt] object SizeParser { + def apply(s: String): Option[Long] = Parser.parse(s, value).toOption + private sealed trait SizeUnit + private case object Bytes extends SizeUnit + private case object KiloBytes extends SizeUnit + private case object MegaBytes extends SizeUnit + private case object GigaBytes extends SizeUnit + private def parseDouble(s: String): Parser[Either[Double, Long]] = + try Parser.success(Left(java.lang.Double.valueOf(s))) + catch { case _: NumberFormatException => Parser.failure(s"Couldn't parse $s as double.") } + private def parseLong(s: String): Parser[Either[Double, Long]] = + try Parser.success(Right(java.lang.Long.valueOf(s))) + catch { case _: NumberFormatException => Parser.failure(s"Couldn't parse $s as double.") } + private[this] val digit = charClass(_.isDigit, "digit") + private[this] val numberParser: Parser[Either[Double, Long]] = + (digit.+ ~ ('.'.examples() ~> digit.+).?).flatMap { + case (leading, Some(decimalPart)) => + parseDouble(s"${leading.mkString}.${decimalPart.mkString}") + case (leading, _) => parseLong(leading.mkString) + } + private[this] val unitParser: Parser[SizeUnit] = + token("b" | "B" | "g" | "G" | "k" | "K" | "m" | "M").map { + case "b" | "B" => Bytes + case "g" | "G" => GigaBytes + case "k" | "K" => KiloBytes + case "m" | "M" => MegaBytes + } + private[this] def multiply(left: Either[Double, Long], right: Long): Long = left match { + case Left(d) => (d * right).toLong + case Right(l) => l * right + } + private[sbt] val value: Parser[Long] = + ((numberParser <~ SpaceClass + .examples(" ", "b", "B", "g", "G", "k", "K", "m", "M") + .*) ~ unitParser.?) + .map { + case (number, unit) => + unit match { + case None | Some(Bytes) => multiply(number, right = 1L) + case Some(KiloBytes) => multiply(number, right = 1024L) + case Some(MegaBytes) => multiply(number, right = 1024L * 1024) + case Some(GigaBytes) => multiply(number, right = 1024L * 1024 * 1024) + } + } +} diff --git a/internal/util-complete/src/main/scala/sbt/internal/util/complete/TokenCompletions.scala b/internal/util-complete/src/main/scala/sbt/internal/util/complete/TokenCompletions.scala new file mode 100644 index 000000000..b956417da --- /dev/null +++ b/internal/util-complete/src/main/scala/sbt/internal/util/complete/TokenCompletions.scala @@ -0,0 +1,52 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util +package complete + +import Completion.{ token => ctoken, tokenDisplay } + +sealed trait TokenCompletions { + def hideWhen(f: Int => Boolean): TokenCompletions +} + +object TokenCompletions { + private[sbt] abstract class Delegating extends TokenCompletions { outer => + def completions(seen: String, level: Int, delegate: Completions): Completions + final def hideWhen(hide: Int => Boolean): TokenCompletions = new Delegating { + def completions(seen: String, level: Int, delegate: Completions): Completions = + if (hide(level)) Completions.nil else outer.completions(seen, level, delegate) + } + } + + private[sbt] abstract class Fixed extends TokenCompletions { outer => + def completions(seen: String, level: Int): Completions + final def hideWhen(hide: Int => Boolean): TokenCompletions = new Fixed { + def completions(seen: String, level: Int) = + if (hide(level)) Completions.nil else outer.completions(seen, level) + } + } + + val default: TokenCompletions = mapDelegateCompletions((seen, level, c) => ctoken(seen, c.append)) + + def displayOnly(msg: String): TokenCompletions = new Fixed { + def completions(seen: String, level: Int) = Completions.single(Completion.displayOnly(msg)) + } + + def overrideDisplay(msg: String): TokenCompletions = + mapDelegateCompletions((seen, level, c) => tokenDisplay(display = msg, append = c.append)) + + def fixed(f: (String, Int) => Completions): TokenCompletions = new Fixed { + def completions(seen: String, level: Int) = f(seen, level) + } + + def mapDelegateCompletions(f: (String, Int, Completion) => Completion): TokenCompletions = + new Delegating { + def completions(seen: String, level: Int, delegate: Completions) = + Completions(delegate.get.map(c => f(seen, level, c))) + } +} diff --git a/internal/util-complete/src/main/scala/sbt/internal/util/complete/TypeString.scala b/internal/util-complete/src/main/scala/sbt/internal/util/complete/TypeString.scala new file mode 100644 index 000000000..3a4e84ad2 --- /dev/null +++ b/internal/util-complete/src/main/scala/sbt/internal/util/complete/TypeString.scala @@ -0,0 +1,90 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util +package complete + +import DefaultParsers._ +import TypeString._ + +/** + * Basic representation of types parsed from Manifest.toString. + * This can only represent the structure of parameterized types. + * All other types are represented by a TypeString with an empty `args`. + */ +private[sbt] final class TypeString(val base: String, val args: List[TypeString]) { + override def toString = + if (base.startsWith(FunctionName)) + args.dropRight(1).mkString("(", ",", ")") + " => " + args.last + else if (base.startsWith(TupleName)) + args.mkString("(", ",", ")") + else + cleanupTypeName(base) + (if (args.isEmpty) "" else args.mkString("[", ",", "]")) +} + +private[sbt] object TypeString { + + /** Makes the string representation of a type as returned by Manifest.toString more readable.*/ + def cleanup(typeString: String): String = + parse(typeString, typeStringParser) match { + case Right(ts) => ts.toString + case Left(_) => typeString + } + + /** + * Makes a fully qualified type name provided by Manifest.toString more readable. + * The argument should be just a name (like scala.Tuple2) and not a full type (like scala.Tuple2[Int,Boolean]) + */ + def cleanupTypeName(base: String): String = + dropPrefix(base).replace('$', '.') + + /** + * Removes prefixes from a fully qualified type name that are unnecessary in the presence of standard imports for an sbt setting. + * This does not use the compiler and is therefore a conservative approximation. + */ + def dropPrefix(base: String): String = + if (base.startsWith(SbtPrefix)) + base.substring(SbtPrefix.length) + else if (base.startsWith(CollectionPrefix)) { + val simple = base.substring(CollectionPrefix.length) + if (ShortenCollection(simple)) simple else base + } else if (base.startsWith(ScalaPrefix)) + base.substring(ScalaPrefix.length) + else if (base.startsWith(JavaPrefix)) + base.substring(JavaPrefix.length) + else + TypeMap.getOrElse(base, base) + + final val CollectionPrefix = "scala.collection." + final val FunctionName = "scala.Function" + final val TupleName = "scala.Tuple" + final val SbtPrefix = "sbt." + final val ScalaPrefix = "scala." + final val JavaPrefix = "java.lang." + /* scala.collection.X -> X */ + val ShortenCollection = Set("Seq", "List", "Set", "Map", "Iterable") + + val TypeMap = Map( + "java.io.File" -> "File", + "java.net.URL" -> "URL", + "java.net.URI" -> "URI" + ) + + /** + * A Parser that extracts basic structure from the string representation of a type from Manifest.toString. + * This is rudimentary and essentially only decomposes the string into names and arguments for parameterized types. + */ + lazy val typeStringParser: Parser[TypeString] = { + def isFullScalaIDChar(c: Char) = isScalaIDChar(c) || c == '.' || c == '$' + lazy val fullScalaID = + identifier(IDStart, charClass(isFullScalaIDChar, "Scala identifier character")) + lazy val tpe: Parser[TypeString] = + for (id <- fullScalaID; args <- ('[' ~> rep1sep(tpe, ',') <~ ']').?) + yield new TypeString(id, args.toList.flatten) + tpe + } +} diff --git a/internal/util-complete/src/main/scala/sbt/internal/util/complete/UpperBound.scala b/internal/util-complete/src/main/scala/sbt/internal/util/complete/UpperBound.scala new file mode 100644 index 000000000..c502f8784 --- /dev/null +++ b/internal/util-complete/src/main/scala/sbt/internal/util/complete/UpperBound.scala @@ -0,0 +1,64 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util +package complete + +sealed trait UpperBound { + + /** True if and only if the given value meets this bound.*/ + def >=(min: Int): Boolean + + /** True if and only if this bound is one.*/ + def isOne: Boolean + + /** True if and only if this bound is zero.*/ + def isZero: Boolean + + /** + * If this bound is zero or Infinite, `decrement` returns this bound. + * Otherwise, this bound is finite and greater than zero and `decrement` returns the bound that is one less than this bound. + */ + def decrement: UpperBound + + /** True if and only if this is unbounded.*/ + def isInfinite: Boolean + +} + +/** Represents unbounded. */ +case object Infinite extends UpperBound { + + /** All finite numbers meet this bound. */ + def >=(min: Int) = true + + def isOne = false + def isZero = false + def decrement = this + def isInfinite = true + + override def toString = "Infinity" +} + +/** + * Represents a finite upper bound. The maximum allowed value is 'value', inclusive. + * It must positive. + */ +final case class Finite(value: Int) extends UpperBound { + assume(value >= 0, "Maximum occurrences must be nonnegative.") + + def >=(min: Int) = value >= min + def isOne = value == 1 + def isZero = value == 0 + def decrement = Finite(scala.math.max(0, value - 1)) + def isInfinite = false + override def toString = value.toString +} + +object UpperBound { + implicit def intToFinite(i: Int): Finite = Finite(i) +} diff --git a/internal/util-complete/src/test/scala/DefaultParsersSpec.scala b/internal/util-complete/src/test/scala/DefaultParsersSpec.scala new file mode 100644 index 000000000..b0f7bd47e --- /dev/null +++ b/internal/util-complete/src/test/scala/DefaultParsersSpec.scala @@ -0,0 +1,29 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util +package complete + +import org.scalacheck._, Gen._, Prop._ + +object DefaultParsersSpec extends Properties("DefaultParsers") { + import DefaultParsers.{ ID, isIDChar, matches, validID } + + property("∀ s ∈ String: validID(s) == matches(ID, s)") = forAll( + (s: String) => validID(s) == matches(ID, s) + ) + + property("∀ s ∈ genID: matches(ID, s)") = forAll(genID)(s => matches(ID, s)) + property("∀ s ∈ genID: validID(s)") = forAll(genID)(s => validID(s)) + + private val chars: Seq[Char] = Char.MinValue to Char.MaxValue + private val genID: Gen[String] = + for { + c <- oneOf(chars filter (_.isLetter)) + cs <- listOf(oneOf(chars filter isIDChar)) + } yield (c :: cs).mkString +} diff --git a/internal/util-complete/src/test/scala/ParserTest.scala b/internal/util-complete/src/test/scala/ParserTest.scala new file mode 100644 index 000000000..49f99056f --- /dev/null +++ b/internal/util-complete/src/test/scala/ParserTest.scala @@ -0,0 +1,162 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util +package complete + +object JLineTest { + import DefaultParsers._ + + val one = "blue" | "green" | "black" + val two = token("color" ~> Space) ~> token(one) + val three = token("color" ~> Space) ~> token(ID.examples("blue", "green", "black")) + val four = token("color" ~> Space) ~> token(ID, "") + + val num = token(NatBasic) + val five = (num ~ token("+" | "-") ~ num) <~ token('=') flatMap { + case a ~ "+" ~ b => token((a + b).toString) + case a ~ "-" ~ b => token((a - b).toString) + } + + val parsers = Map("1" -> one, "2" -> two, "3" -> three, "4" -> four, "5" -> five) + def main(args: Array[String]): Unit = { + import jline.TerminalFactory + import jline.console.ConsoleReader + val reader = new ConsoleReader() + TerminalFactory.get.init + + val parser = parsers(args(0)) + JLineCompletion.installCustomCompletor(reader, parser) + def loop(): Unit = { + val line = reader.readLine("> ") + if (line ne null) { + println("Result: " + apply(parser)(line).resultEmpty) + loop() + } + } + loop() + } +} + +import Parser._ +import org.scalacheck._ + +object ParserTest extends Properties("Completing Parser") { + import Parsers._ + import DefaultParsers.matches + + val nested = (token("a1") ~ token("b2")) ~ "c3" + val nestedDisplay = (token("a1", "") ~ token("b2", "")) ~ "c3" + + val spacePort = token(Space) ~> Port + + def p[T](f: T): T = { println(f); f } + + def checkSingle(in: String, expect: Completion)(expectDisplay: Completion = expect) = + (("token '" + in + "'") |: checkOne(in, nested, expect)) && + (("display '" + in + "'") |: checkOne(in, nestedDisplay, expectDisplay)) + + def checkOne(in: String, parser: Parser[_], expect: Completion): Prop = + completions(parser, in, 1) == Completions.single(expect) + + def checkAll(in: String, parser: Parser[_], expect: Completions): Prop = { + val cs = completions(parser, in, 1) + ("completions: " + cs) |: ("Expected: " + expect) |: (cs == expect: Prop) + } + + def checkInvalid(in: String) = + (("token '" + in + "'") |: checkInv(in, nested)) && + (("display '" + in + "'") |: checkInv(in, nestedDisplay)) + + def checkInv(in: String, parser: Parser[_]): Prop = { + val cs = completions(parser, in, 1) + ("completions: " + cs) |: (cs == Completions.nil: Prop) + } + + property("nested tokens a") = + checkSingle("", Completion.token("", "a1"))(Completion.displayOnly("")) + property("nested tokens a1") = + checkSingle("a", Completion.token("a", "1"))(Completion.displayOnly("")) + property("nested tokens a inv") = checkInvalid("b") + property("nested tokens b") = + checkSingle("a1", Completion.token("", "b2"))(Completion.displayOnly("")) + property("nested tokens b2") = + checkSingle("a1b", Completion.token("b", "2"))(Completion.displayOnly("")) + property("nested tokens b inv") = checkInvalid("a1a") + property("nested tokens c") = checkSingle("a1b2", Completion.suggestion("c3"))() + property("nested tokens c3") = checkSingle("a1b2c", Completion.suggestion("3"))() + property("nested tokens c inv") = checkInvalid("a1b2a") + + property("suggest space") = checkOne("", spacePort, Completion.token("", " ")) + property("suggest port") = checkOne(" ", spacePort, Completion.displayOnly("")) + property("no suggest at end") = checkOne("asdf", "asdf", Completion.suggestion("")) + property("no suggest at token end") = checkOne("asdf", token("asdf"), Completion.suggestion("")) + property("empty suggest for examples") = + checkOne("asdf", any.+.examples("asdf", "qwer"), Completion.suggestion("")) + property("empty suggest for examples token") = + checkOne("asdf", token(any.+.examples("asdf", "qwer")), Completion.suggestion("")) + + val colors = Set("blue", "green", "red") + val base = (seen: Seq[String]) => token(ID examples (colors -- seen)) + val sep = token(Space) + val repeat = repeatDep(base, sep) + def completionStrings(ss: Set[String]) = Completions(ss map (Completion.token("", _))) + + property("repeatDep no suggestions for bad input") = checkInv(".", repeat) + property("repeatDep suggest all") = checkAll("", repeat, completionStrings(colors)) + property("repeatDep suggest remaining two") = { + val first = colors.toSeq.head + checkAll(first + " ", repeat, completionStrings(colors - first)) + } + property("repeatDep suggest remaining one") = { + val take = colors.toSeq.take(2) + checkAll(take.mkString("", " ", " "), repeat, completionStrings(colors -- take)) + } + property("repeatDep requires at least one token") = !matches(repeat, "") + property("repeatDep accepts one token") = matches(repeat, colors.toSeq.head) + property("repeatDep accepts two tokens") = matches(repeat, colors.toSeq.take(2).mkString(" ")) +} +object ParserExample { + val ws = charClass(_.isWhitespace, "whitespace").+ + val notws = charClass(!_.isWhitespace, "not whitespace").+ + + val name = token("test") + val options = (ws ~> token("quick" | "failed" | "new")).* + val exampleSet = Set("am", "is", "are", "was", "were") + val include = (ws ~> token( + examples(notws.string, new FixedSetExamples(exampleSet), exampleSet.size, false) + )).* + + val t = name ~ options ~ include + + // Get completions for some different inputs + println(completions(t, "te", 1)) + println(completions(t, "test ", 1)) + println(completions(t, "test w", 1)) + + // Get the parsed result for different inputs + println(apply(t)("te").resultEmpty) + println(apply(t)("test").resultEmpty) + println(apply(t)("test w").resultEmpty) + println(apply(t)("test was were").resultEmpty) + + def run(n: Int): Unit = { + val a = 'a'.id + val aq = a.? + val aqn = repeat(aq, min = n, max = n) + val an = repeat(a, min = n, max = n) + val ann = aqn ~ an + + def r = apply(ann)("a" * (n * 2)).resultEmpty + println(r.isValid) + } + def run2(n: Int): Unit = { + val ab = "ab".?.* + val r = apply(ab)("a" * n).resultEmpty + println(r) + } +} diff --git a/internal/util-complete/src/test/scala/UnitSpec.scala b/internal/util-complete/src/test/scala/UnitSpec.scala new file mode 100644 index 000000000..ed582c3bb --- /dev/null +++ b/internal/util-complete/src/test/scala/UnitSpec.scala @@ -0,0 +1,12 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util + +import org.scalatest._ + +abstract class UnitSpec extends FlatSpec with Matchers diff --git a/internal/util-complete/src/test/scala/sbt/complete/FileExamplesTest.scala b/internal/util-complete/src/test/scala/sbt/complete/FileExamplesTest.scala new file mode 100644 index 000000000..da4662308 --- /dev/null +++ b/internal/util-complete/src/test/scala/sbt/complete/FileExamplesTest.scala @@ -0,0 +1,104 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util +package complete + +import java.io.File +import org.scalatest.Assertion +import sbt.io.IO + +class FileExamplesTest extends UnitSpec { + + "listing all files in an absolute base directory" should + "produce the entire base directory's contents" in { + withDirectoryStructure() { ds => + ds.fileExamples().toList should contain theSameElementsAs (ds.allRelativizedPaths) + } + } + + "listing files with a prefix that matches none" should "produce an empty list" in { + withDirectoryStructure(withCompletionPrefix = "z") { ds => + ds.fileExamples().toList shouldBe empty + } + } + + "listing single-character prefixed files" should "produce matching paths only" in { + withDirectoryStructure(withCompletionPrefix = "f") { ds => + ds.fileExamples().toList should contain theSameElementsAs (ds.prefixedPathsOnly) + } + } + + "listing directory-prefixed files" should "produce matching paths only" in { + withDirectoryStructure(withCompletionPrefix = "far") { ds => + ds.fileExamples().toList should contain theSameElementsAs (ds.prefixedPathsOnly) + } + } + + it should "produce sub-dir contents only when appending a file separator to the directory" in { + withDirectoryStructure(withCompletionPrefix = "far" + File.separator) { ds => + ds.fileExamples().toList should contain theSameElementsAs (ds.prefixedPathsOnly) + } + } + + "listing files with a sub-path prefix" should "produce matching paths only" in { + withDirectoryStructure(withCompletionPrefix = "far" + File.separator + "ba") { ds => + ds.fileExamples().toList should contain theSameElementsAs (ds.prefixedPathsOnly) + } + } + + "completing a full path" should "produce a list with an empty string" in { + withDirectoryStructure(withCompletionPrefix = "bazaar") { ds => + ds.fileExamples().toList shouldEqual List("") + } + } + + def withDirectoryStructure[A](withCompletionPrefix: String = "")( + thunk: DirectoryStructure => Assertion + ): Assertion = { + IO.withTemporaryDirectory { tempDir => + val ds = new DirectoryStructure(withCompletionPrefix) + ds.createSampleDirStructure(tempDir) + ds.fileExamples = new FileExamples(ds.baseDir, withCompletionPrefix) + thunk(ds) + } + } + + final class DirectoryStructure(withCompletionPrefix: String) { + var fileExamples: FileExamples = _ + var baseDir: File = _ + var childFiles: List[File] = _ + var childDirectories: List[File] = _ + var nestedFiles: List[File] = _ + var nestedDirectories: List[File] = _ + + def allRelativizedPaths: List[String] = + (childFiles ++ childDirectories ++ nestedFiles ++ nestedDirectories) + .map(IO.relativize(baseDir, _).get) + + def prefixedPathsOnly: List[String] = + allRelativizedPaths + .filter(_ startsWith withCompletionPrefix) + .map(_ substring withCompletionPrefix.length) + + def createSampleDirStructure(tempDir: File): Unit = { + childFiles = toChildFiles(tempDir, List("foo", "bar", "bazaar")) + childDirectories = toChildFiles(tempDir, List("moo", "far")) + nestedFiles = toChildFiles(childDirectories(1), List("farfile1", "barfile2")) + nestedDirectories = toChildFiles(childDirectories(1), List("fardir1", "bardir2")) + + (childDirectories ++ nestedDirectories).map(_.mkdirs()) + (childFiles ++ nestedFiles).map(_.createNewFile()) + + baseDir = tempDir + } + + private def toChildFiles(baseDir: File, files: List[String]): List[File] = + files.map(new File(baseDir, _)) + } + +} diff --git a/internal/util-complete/src/test/scala/sbt/complete/FixedSetExamplesTest.scala b/internal/util-complete/src/test/scala/sbt/complete/FixedSetExamplesTest.scala new file mode 100644 index 000000000..51c6a2f6f --- /dev/null +++ b/internal/util-complete/src/test/scala/sbt/complete/FixedSetExamplesTest.scala @@ -0,0 +1,32 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util +package complete + +class FixedSetExamplesTest extends UnitSpec { + + "adding a prefix" should "produce a smaller set of examples with the prefix removed" in { + val _ = new Examples { + fixedSetExamples.withAddedPrefix("f")() should contain theSameElementsAs + (List("oo", "ool", "u")) + fixedSetExamples.withAddedPrefix("fo")() should contain theSameElementsAs (List("o", "ol")) + fixedSetExamples.withAddedPrefix("b")() should contain theSameElementsAs (List("ar")) + } + } + + "without a prefix" should "produce the original set" in { + val _ = new Examples { + fixedSetExamples() shouldBe exampleSet + } + } + + trait Examples { + val exampleSet = List("foo", "bar", "fool", "fu") + val fixedSetExamples = FixedSetExamples(exampleSet) + } +} diff --git a/internal/util-complete/src/test/scala/sbt/complete/ParserWithExamplesTest.scala b/internal/util-complete/src/test/scala/sbt/complete/ParserWithExamplesTest.scala new file mode 100644 index 000000000..93e8e4c05 --- /dev/null +++ b/internal/util-complete/src/test/scala/sbt/complete/ParserWithExamplesTest.scala @@ -0,0 +1,117 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util +package complete + +import Completion._ + +class ParserWithExamplesTest extends UnitSpec { + + "listing a limited number of completions" should + "grab only the needed number of elements from the iterable source of examples" in { + val _ = new ParserWithLazyExamples { + parserWithExamples.completions(0) + examples.size shouldEqual maxNumberOfExamples + } + } + + "listing only valid completions" should + "use the delegate parser to remove invalid examples" in { + val _ = new ParserWithValidExamples { + val validCompletions = Completions( + Set( + suggestion("blue"), + suggestion("red") + ) + ) + parserWithExamples.completions(0) shouldEqual validCompletions + } + } + + "listing valid completions in a derived parser" should + "produce only valid examples that start with the character of the derivation" in { + val _ = new ParserWithValidExamples { + val derivedCompletions = Completions( + Set( + suggestion("lue") + ) + ) + parserWithExamples.derive('b').completions(0) shouldEqual derivedCompletions + } + } + + "listing valid and invalid completions" should + "produce the entire source of examples" in { + val _ = new parserWithAllExamples { + val completions = Completions(examples.map(suggestion(_)).toSet) + parserWithExamples.completions(0) shouldEqual completions + } + } + + "listing valid and invalid completions in a derived parser" should + "produce only examples that start with the character of the derivation" in { + val _ = new parserWithAllExamples { + val derivedCompletions = Completions( + Set( + suggestion("lue"), + suggestion("lock") + ) + ) + parserWithExamples.derive('b').completions(0) shouldEqual derivedCompletions + } + } + + class ParserWithLazyExamples + extends ParserExample( + GrowableSourceOfExamples(), + maxNumberOfExamples = 5, + removeInvalidExamples = false + ) + + class ParserWithValidExamples extends ParserExample(removeInvalidExamples = true) + + class parserWithAllExamples extends ParserExample(removeInvalidExamples = false) + + case class ParserExample( + examples: Iterable[String] = Set("blue", "yellow", "greeen", "block", "red"), + maxNumberOfExamples: Int = 25, + removeInvalidExamples: Boolean + ) { + + import DefaultParsers._ + + val colorParser = "blue" | "green" | "black" | "red" + val parserWithExamples: Parser[String] = new ParserWithExamples[String]( + colorParser, + FixedSetExamples(examples), + maxNumberOfExamples, + removeInvalidExamples + ) + } + + case class GrowableSourceOfExamples() extends Iterable[String] { + private var numberOfIteratedElements: Int = 0 + + override def iterator: Iterator[String] = { + new Iterator[String] { + var currentElement = 0 + + override def next(): String = { + currentElement += 1 + numberOfIteratedElements = Math.max(currentElement, numberOfIteratedElements) + numberOfIteratedElements.toString + } + + override def hasNext: Boolean = true + } + } + + override def size: Int = numberOfIteratedElements + } + +} diff --git a/internal/util-complete/src/test/scala/sbt/internal/util/complete/SizeParserSpec.scala b/internal/util-complete/src/test/scala/sbt/internal/util/complete/SizeParserSpec.scala new file mode 100644 index 000000000..521f2d10a --- /dev/null +++ b/internal/util-complete/src/test/scala/sbt/internal/util/complete/SizeParserSpec.scala @@ -0,0 +1,63 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util.complete + +import org.scalatest.FlatSpec + +class SizeParserSpec extends FlatSpec { + "SizeParser" should "handle raw bytes" in { + assert(Parser.parse(str = "123456", SizeParser.value) == Right(123456L)) + } + it should "handle bytes" in { + assert(Parser.parse(str = "123456b", SizeParser.value) == Right(123456L)) + assert(Parser.parse(str = "123456B", SizeParser.value) == Right(123456L)) + assert(Parser.parse(str = "123456 b", SizeParser.value) == Right(123456L)) + assert(Parser.parse(str = "123456 B", SizeParser.value) == Right(123456L)) + } + it should "handle kilobytes" in { + assert(Parser.parse(str = "123456k", SizeParser.value) == Right(123456L * 1024)) + assert(Parser.parse(str = "123456K", SizeParser.value) == Right(123456L * 1024)) + assert(Parser.parse(str = "123456 K", SizeParser.value) == Right(123456L * 1024)) + assert(Parser.parse(str = "123456 K", SizeParser.value) == Right(123456L * 1024)) + } + it should "handle megabytes" in { + assert(Parser.parse(str = "123456m", SizeParser.value) == Right(123456L * 1024 * 1024)) + assert(Parser.parse(str = "123456M", SizeParser.value) == Right(123456L * 1024 * 1024)) + assert(Parser.parse(str = "123456 M", SizeParser.value) == Right(123456L * 1024 * 1024)) + assert(Parser.parse(str = "123456 M", SizeParser.value) == Right(123456L * 1024 * 1024)) + } + it should "handle gigabytes" in { + assert(Parser.parse(str = "123456g", SizeParser.value) == Right(123456L * 1024 * 1024 * 1024)) + assert(Parser.parse(str = "123456G", SizeParser.value) == Right(123456L * 1024 * 1024 * 1024)) + assert(Parser.parse(str = "123456 G", SizeParser.value) == Right(123456L * 1024 * 1024 * 1024)) + assert(Parser.parse(str = "123456 G", SizeParser.value) == Right(123456L * 1024 * 1024 * 1024)) + } + it should "handle doubles" in { + assert(Parser.parse(str = "1.25g", SizeParser.value) == Right(5L * 1024 * 1024 * 1024 / 4)) + assert(Parser.parse(str = "1.25 g", SizeParser.value) == Right(5L * 1024 * 1024 * 1024 / 4)) + assert(Parser.parse(str = "1.25 g", SizeParser.value) == Right(5L * 1024 * 1024 * 1024 / 4)) + assert(Parser.parse(str = "1.25 G", SizeParser.value) == Right(5L * 1024 * 1024 * 1024 / 4)) + } + private val expectedCompletions = Set("", "b", "B", "g", "G", "k", "K", "m", "M", " ") + it should "have completions for long" in { + val completions = Parser.completions(SizeParser.value, "123", level = 0).get.map(_.display) + assert(completions == expectedCompletions) + } + it should "have completions for long with spaces" in { + val completions = Parser.completions(SizeParser.value, "123", level = 0).get.map(_.display) + assert(completions == expectedCompletions) + } + it should "have completions for double " in { + val completions = Parser.completions(SizeParser.value, "1.25", level = 0).get.map(_.display) + assert(completions == expectedCompletions) + } + it should "have completions for double with spaces" in { + val completions = Parser.completions(SizeParser.value, "1.25 ", level = 0).get.map(_.display) + assert(completions == expectedCompletions + "") + } +} diff --git a/internal/util-control/NOTICE b/internal/util-control/NOTICE new file mode 100644 index 000000000..76a30965a --- /dev/null +++ b/internal/util-control/NOTICE @@ -0,0 +1,3 @@ +Simple Build Tool: Control Component +Copyright 2009 Mark Harrah +Licensed under BSD-style license (see LICENSE) \ No newline at end of file diff --git a/internal/util-control/src/main/scala/sbt/internal/util/ErrorHandling.scala b/internal/util-control/src/main/scala/sbt/internal/util/ErrorHandling.scala new file mode 100644 index 000000000..7b2a86994 --- /dev/null +++ b/internal/util-control/src/main/scala/sbt/internal/util/ErrorHandling.scala @@ -0,0 +1,49 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util + +import java.io.IOException + +object ErrorHandling { + def translate[T](msg: => String)(f: => T) = + try { + f + } catch { + case e: IOException => throw new TranslatedIOException(msg + e.toString, e) + case e: Exception => throw new TranslatedException(msg + e.toString, e) + } + + def wideConvert[T](f: => T): Either[Throwable, T] = + try { + Right(f) + } catch { + case ex @ (_: Exception | _: StackOverflowError) => Left(ex) + case err @ (_: ThreadDeath | _: VirtualMachineError) => throw err + case x: Throwable => Left(x) + } + + def convert[T](f: => T): Either[Exception, T] = + try { + Right(f) + } catch { case e: Exception => Left(e) } + + def reducedToString(e: Throwable): String = + if (e.getClass == classOf[RuntimeException]) { + val msg = e.getMessage + if (msg == null || msg.isEmpty) e.toString else msg + } else + e.toString +} + +sealed class TranslatedException private[sbt] (msg: String, cause: Throwable) + extends RuntimeException(msg, cause) { + override def toString = msg +} + +final class TranslatedIOException private[sbt] (msg: String, cause: IOException) + extends TranslatedException(msg, cause) diff --git a/internal/util-control/src/main/scala/sbt/internal/util/ExitHook.scala b/internal/util-control/src/main/scala/sbt/internal/util/ExitHook.scala new file mode 100644 index 000000000..677b780fd --- /dev/null +++ b/internal/util-control/src/main/scala/sbt/internal/util/ExitHook.scala @@ -0,0 +1,28 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util + +/** Defines a function to call as sbt exits.*/ +trait ExitHook { + + /** Subclasses should implement this method, which is called when this hook is executed. */ + def runBeforeExiting(): Unit + +} + +object ExitHook { + def apply(f: => Unit): ExitHook = new ExitHook { def runBeforeExiting() = f } +} + +object ExitHooks { + + /** Calls each registered exit hook, trapping any exceptions so that each hook is given a chance to run. */ + def runExitHooks(exitHooks: Seq[ExitHook]): Seq[Throwable] = + exitHooks.flatMap(hook => ErrorHandling.wideConvert(hook.runBeforeExiting()).left.toOption) + +} diff --git a/internal/util-control/src/main/scala/sbt/internal/util/MessageOnlyException.scala b/internal/util-control/src/main/scala/sbt/internal/util/MessageOnlyException.scala new file mode 100644 index 000000000..094d4999c --- /dev/null +++ b/internal/util-control/src/main/scala/sbt/internal/util/MessageOnlyException.scala @@ -0,0 +1,28 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util + +final class MessageOnlyException(override val toString: String) extends RuntimeException(toString) + +/** + * A dummy exception for the top-level exception handler to know that an exception + * has been handled, but is being passed further up to indicate general failure. + */ +final class AlreadyHandledException(val underlying: Throwable) extends RuntimeException + +/** + * A marker trait for a top-level exception handler to know that this exception + * doesn't make sense to display. + */ +trait UnprintableException extends Throwable + +/** + * A marker trait that refines UnprintableException to indicate to a top-level exception handler + * that the code throwing this exception has already provided feedback to the user about the error condition. + */ +trait FeedbackProvidedException extends UnprintableException diff --git a/internal/util-control/src/main/scala/sbt/internal/util/RunningProcesses.scala b/internal/util-control/src/main/scala/sbt/internal/util/RunningProcesses.scala new file mode 100644 index 000000000..47da42d34 --- /dev/null +++ b/internal/util-control/src/main/scala/sbt/internal/util/RunningProcesses.scala @@ -0,0 +1,33 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util + +import java.util.concurrent.ConcurrentHashMap +import scala.sys.process.Process + +/** + * Manages forked processes created by sbt. Any process registered + * with RunningProcesses can be killed with the killAll method. In + * particular, this can be used in a signal handler to kill these + * processes when the user inputs ctrl+c. + */ +private[sbt] object RunningProcesses { + val active = ConcurrentHashMap.newKeySet[Process] + def add(process: Process): Unit = active.synchronized { + active.add(process) + () + } + def remove(process: Process): Unit = active.synchronized { + active.remove(process) + () + } + def killAll(): Unit = active.synchronized { + active.forEach(_.destroy()) + active.clear() + } +} diff --git a/internal/util-interface/src/main/java/xsbti/Logger.java b/internal/util-interface/src/main/java/xsbti/Logger.java new file mode 100644 index 000000000..54023b52e --- /dev/null +++ b/internal/util-interface/src/main/java/xsbti/Logger.java @@ -0,0 +1,22 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package xsbti; + +import java.util.function.Supplier; + +public interface Logger { + void error(Supplier msg); + + void warn(Supplier msg); + + void info(Supplier msg); + + void debug(Supplier msg); + + void trace(Supplier exception); +} diff --git a/internal/util-interface/src/main/java/xsbti/Position.java b/internal/util-interface/src/main/java/xsbti/Position.java new file mode 100644 index 000000000..da4f139d6 --- /dev/null +++ b/internal/util-interface/src/main/java/xsbti/Position.java @@ -0,0 +1,53 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package xsbti; + +import java.io.File; +import java.util.Optional; + +public interface Position { + Optional line(); + + String lineContent(); + + Optional offset(); + + // pointer to the column position of the error/warning + Optional pointer(); + + Optional pointerSpace(); + + Optional sourcePath(); + + Optional sourceFile(); + + // Default values to avoid breaking binary compatibility + default Optional startOffset() { + return Optional.empty(); + } + + default Optional endOffset() { + return Optional.empty(); + } + + default Optional startLine() { + return Optional.empty(); + } + + default Optional startColumn() { + return Optional.empty(); + } + + default Optional endLine() { + return Optional.empty(); + } + + default Optional endColumn() { + return Optional.empty(); + } +} diff --git a/internal/util-interface/src/main/java/xsbti/Problem.java b/internal/util-interface/src/main/java/xsbti/Problem.java new file mode 100644 index 000000000..78c9145a3 --- /dev/null +++ b/internal/util-interface/src/main/java/xsbti/Problem.java @@ -0,0 +1,29 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package xsbti; + +import java.util.Optional; + +public interface Problem { + String category(); + + Severity severity(); + + String message(); + + Position position(); + + // Default value to avoid breaking binary compatibility + /** + * If present, the string shown to the user when displaying this Problem. Otherwise, the Problem + * will be shown in an implementation-defined way based on the values of its other fields. + */ + default Optional rendered() { + return Optional.empty(); + } +} diff --git a/internal/util-interface/src/main/java/xsbti/Severity.java b/internal/util-interface/src/main/java/xsbti/Severity.java new file mode 100644 index 000000000..833d91093 --- /dev/null +++ b/internal/util-interface/src/main/java/xsbti/Severity.java @@ -0,0 +1,14 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package xsbti; + +public enum Severity { + Info, + Warn, + Error +} diff --git a/internal/util-interface/src/main/java/xsbti/T2.java b/internal/util-interface/src/main/java/xsbti/T2.java new file mode 100644 index 000000000..2f51726d7 --- /dev/null +++ b/internal/util-interface/src/main/java/xsbti/T2.java @@ -0,0 +1,15 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package xsbti; + +/** Used to pass a pair of values. */ +public interface T2 { + public A1 get1(); + + public A2 get2(); +} diff --git a/internal/util-logging/NOTICE b/internal/util-logging/NOTICE new file mode 100644 index 000000000..2455dad65 --- /dev/null +++ b/internal/util-logging/NOTICE @@ -0,0 +1,3 @@ +Simple Build Tool: Logging Component +Copyright 2008, 2009, 2010 Mark Harrah, Tony Sloane +Licensed under BSD-style license (see LICENSE) \ No newline at end of file diff --git a/internal/util-logging/src/main/contraband-scala/sbt/internal/util/AbstractEntry.scala b/internal/util-logging/src/main/contraband-scala/sbt/internal/util/AbstractEntry.scala new file mode 100644 index 000000000..023b1009f --- /dev/null +++ b/internal/util-logging/src/main/contraband-scala/sbt/internal/util/AbstractEntry.scala @@ -0,0 +1,27 @@ +/** + * This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.internal.util +abstract class AbstractEntry( + val channelName: Option[String], + val execId: Option[String]) extends Serializable { + + + + + override def equals(o: Any): Boolean = this.eq(o.asInstanceOf[AnyRef]) || (o match { + case x: AbstractEntry => (this.channelName == x.channelName) && (this.execId == x.execId) + case _ => false + }) + override def hashCode: Int = { + 37 * (37 * (37 * (17 + "sbt.internal.util.AbstractEntry".##) + channelName.##) + execId.##) + } + override def toString: String = { + "AbstractEntry(" + channelName + ", " + execId + ")" + } +} +object AbstractEntry { + +} diff --git a/internal/util-logging/src/main/contraband-scala/sbt/internal/util/LogOption.scala b/internal/util-logging/src/main/contraband-scala/sbt/internal/util/LogOption.scala new file mode 100644 index 000000000..3e0c50d64 --- /dev/null +++ b/internal/util-logging/src/main/contraband-scala/sbt/internal/util/LogOption.scala @@ -0,0 +1,15 @@ +/** + * This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.internal.util +/** value for logging options like color */ +sealed abstract class LogOption extends Serializable +object LogOption { + + + case object Always extends LogOption + case object Never extends LogOption + case object Auto extends LogOption +} diff --git a/internal/util-logging/src/main/contraband-scala/sbt/internal/util/ProgressEvent.scala b/internal/util-logging/src/main/contraband-scala/sbt/internal/util/ProgressEvent.scala new file mode 100644 index 000000000..ada2a80c8 --- /dev/null +++ b/internal/util-logging/src/main/contraband-scala/sbt/internal/util/ProgressEvent.scala @@ -0,0 +1,75 @@ +/** + * This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.internal.util +/** used by super shell */ +final class ProgressEvent private ( + val level: String, + val items: Vector[sbt.internal.util.ProgressItem], + val lastTaskCount: Option[Int], + channelName: Option[String], + execId: Option[String], + val command: Option[String], + val skipIfActive: Option[Boolean]) extends sbt.internal.util.AbstractEntry(channelName, execId) with Serializable { + + private def this(level: String, items: Vector[sbt.internal.util.ProgressItem], lastTaskCount: Option[Int], channelName: Option[String], execId: Option[String]) = this(level, items, lastTaskCount, channelName, execId, None, None) + + override def equals(o: Any): Boolean = this.eq(o.asInstanceOf[AnyRef]) || (o match { + case x: ProgressEvent => (this.level == x.level) && (this.items == x.items) && (this.lastTaskCount == x.lastTaskCount) && (this.channelName == x.channelName) && (this.execId == x.execId) && (this.command == x.command) && (this.skipIfActive == x.skipIfActive) + case _ => false + }) + override def hashCode: Int = { + 37 * (37 * (37 * (37 * (37 * (37 * (37 * (37 * (17 + "sbt.internal.util.ProgressEvent".##) + level.##) + items.##) + lastTaskCount.##) + channelName.##) + execId.##) + command.##) + skipIfActive.##) + } + override def toString: String = { + "ProgressEvent(" + level + ", " + items + ", " + lastTaskCount + ", " + channelName + ", " + execId + ", " + command + ", " + skipIfActive + ")" + } + private[this] def copy(level: String = level, items: Vector[sbt.internal.util.ProgressItem] = items, lastTaskCount: Option[Int] = lastTaskCount, channelName: Option[String] = channelName, execId: Option[String] = execId, command: Option[String] = command, skipIfActive: Option[Boolean] = skipIfActive): ProgressEvent = { + new ProgressEvent(level, items, lastTaskCount, channelName, execId, command, skipIfActive) + } + def withLevel(level: String): ProgressEvent = { + copy(level = level) + } + def withItems(items: Vector[sbt.internal.util.ProgressItem]): ProgressEvent = { + copy(items = items) + } + def withLastTaskCount(lastTaskCount: Option[Int]): ProgressEvent = { + copy(lastTaskCount = lastTaskCount) + } + def withLastTaskCount(lastTaskCount: Int): ProgressEvent = { + copy(lastTaskCount = Option(lastTaskCount)) + } + def withChannelName(channelName: Option[String]): ProgressEvent = { + copy(channelName = channelName) + } + def withChannelName(channelName: String): ProgressEvent = { + copy(channelName = Option(channelName)) + } + def withExecId(execId: Option[String]): ProgressEvent = { + copy(execId = execId) + } + def withExecId(execId: String): ProgressEvent = { + copy(execId = Option(execId)) + } + def withCommand(command: Option[String]): ProgressEvent = { + copy(command = command) + } + def withCommand(command: String): ProgressEvent = { + copy(command = Option(command)) + } + def withSkipIfActive(skipIfActive: Option[Boolean]): ProgressEvent = { + copy(skipIfActive = skipIfActive) + } + def withSkipIfActive(skipIfActive: Boolean): ProgressEvent = { + copy(skipIfActive = Option(skipIfActive)) + } +} +object ProgressEvent { + + def apply(level: String, items: Vector[sbt.internal.util.ProgressItem], lastTaskCount: Option[Int], channelName: Option[String], execId: Option[String]): ProgressEvent = new ProgressEvent(level, items, lastTaskCount, channelName, execId) + def apply(level: String, items: Vector[sbt.internal.util.ProgressItem], lastTaskCount: Int, channelName: String, execId: String): ProgressEvent = new ProgressEvent(level, items, Option(lastTaskCount), Option(channelName), Option(execId)) + def apply(level: String, items: Vector[sbt.internal.util.ProgressItem], lastTaskCount: Option[Int], channelName: Option[String], execId: Option[String], command: Option[String], skipIfActive: Option[Boolean]): ProgressEvent = new ProgressEvent(level, items, lastTaskCount, channelName, execId, command, skipIfActive) + def apply(level: String, items: Vector[sbt.internal.util.ProgressItem], lastTaskCount: Int, channelName: String, execId: String, command: String, skipIfActive: Boolean): ProgressEvent = new ProgressEvent(level, items, Option(lastTaskCount), Option(channelName), Option(execId), Option(command), Option(skipIfActive)) +} diff --git a/internal/util-logging/src/main/contraband-scala/sbt/internal/util/ProgressItem.scala b/internal/util-logging/src/main/contraband-scala/sbt/internal/util/ProgressItem.scala new file mode 100644 index 000000000..cb3498842 --- /dev/null +++ b/internal/util-logging/src/main/contraband-scala/sbt/internal/util/ProgressItem.scala @@ -0,0 +1,41 @@ +/** + * This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.internal.util +/** + * used by super shell + * @param name name of a task + * @param elapsedMicros current elapsed time in micro seconds + */ +final class ProgressItem private ( + val name: String, + val elapsedMicros: Long) extends Serializable { + + + + override def equals(o: Any): Boolean = this.eq(o.asInstanceOf[AnyRef]) || (o match { + case x: ProgressItem => (this.name == x.name) && (this.elapsedMicros == x.elapsedMicros) + case _ => false + }) + override def hashCode: Int = { + 37 * (37 * (37 * (17 + "sbt.internal.util.ProgressItem".##) + name.##) + elapsedMicros.##) + } + override def toString: String = { + "ProgressItem(" + name + ", " + elapsedMicros + ")" + } + private[this] def copy(name: String = name, elapsedMicros: Long = elapsedMicros): ProgressItem = { + new ProgressItem(name, elapsedMicros) + } + def withName(name: String): ProgressItem = { + copy(name = name) + } + def withElapsedMicros(elapsedMicros: Long): ProgressItem = { + copy(elapsedMicros = elapsedMicros) + } +} +object ProgressItem { + + def apply(name: String, elapsedMicros: Long): ProgressItem = new ProgressItem(name, elapsedMicros) +} diff --git a/internal/util-logging/src/main/contraband-scala/sbt/internal/util/StringEvent.scala b/internal/util-logging/src/main/contraband-scala/sbt/internal/util/StringEvent.scala new file mode 100644 index 000000000..39bb8d716 --- /dev/null +++ b/internal/util-logging/src/main/contraband-scala/sbt/internal/util/StringEvent.scala @@ -0,0 +1,51 @@ +/** + * This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.internal.util +final class StringEvent private ( + val level: String, + val message: String, + channelName: Option[String], + execId: Option[String]) extends sbt.internal.util.AbstractEntry(channelName, execId) with Serializable { + + + + override def equals(o: Any): Boolean = this.eq(o.asInstanceOf[AnyRef]) || (o match { + case x: StringEvent => (this.level == x.level) && (this.message == x.message) && (this.channelName == x.channelName) && (this.execId == x.execId) + case _ => false + }) + override def hashCode: Int = { + 37 * (37 * (37 * (37 * (37 * (17 + "sbt.internal.util.StringEvent".##) + level.##) + message.##) + channelName.##) + execId.##) + } + override def toString: String = { + "StringEvent(" + level + ", " + message + ", " + channelName + ", " + execId + ")" + } + private[this] def copy(level: String = level, message: String = message, channelName: Option[String] = channelName, execId: Option[String] = execId): StringEvent = { + new StringEvent(level, message, channelName, execId) + } + def withLevel(level: String): StringEvent = { + copy(level = level) + } + def withMessage(message: String): StringEvent = { + copy(message = message) + } + def withChannelName(channelName: Option[String]): StringEvent = { + copy(channelName = channelName) + } + def withChannelName(channelName: String): StringEvent = { + copy(channelName = Option(channelName)) + } + def withExecId(execId: Option[String]): StringEvent = { + copy(execId = execId) + } + def withExecId(execId: String): StringEvent = { + copy(execId = Option(execId)) + } +} +object StringEvent { + + def apply(level: String, message: String, channelName: Option[String], execId: Option[String]): StringEvent = new StringEvent(level, message, channelName, execId) + def apply(level: String, message: String, channelName: String, execId: String): StringEvent = new StringEvent(level, message, Option(channelName), Option(execId)) +} diff --git a/internal/util-logging/src/main/contraband-scala/sbt/internal/util/SuccessEvent.scala b/internal/util-logging/src/main/contraband-scala/sbt/internal/util/SuccessEvent.scala new file mode 100644 index 000000000..12df7b1d9 --- /dev/null +++ b/internal/util-logging/src/main/contraband-scala/sbt/internal/util/SuccessEvent.scala @@ -0,0 +1,32 @@ +/** + * This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.internal.util +final class SuccessEvent private ( + val message: String) extends Serializable { + + + + override def equals(o: Any): Boolean = this.eq(o.asInstanceOf[AnyRef]) || (o match { + case x: SuccessEvent => (this.message == x.message) + case _ => false + }) + override def hashCode: Int = { + 37 * (37 * (17 + "sbt.internal.util.SuccessEvent".##) + message.##) + } + override def toString: String = { + "SuccessEvent(" + message + ")" + } + private[this] def copy(message: String = message): SuccessEvent = { + new SuccessEvent(message) + } + def withMessage(message: String): SuccessEvent = { + copy(message = message) + } +} +object SuccessEvent { + + def apply(message: String): SuccessEvent = new SuccessEvent(message) +} diff --git a/internal/util-logging/src/main/contraband-scala/sbt/internal/util/TraceEvent.scala b/internal/util-logging/src/main/contraband-scala/sbt/internal/util/TraceEvent.scala new file mode 100644 index 000000000..97cd54285 --- /dev/null +++ b/internal/util-logging/src/main/contraband-scala/sbt/internal/util/TraceEvent.scala @@ -0,0 +1,51 @@ +/** + * This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.internal.util +final class TraceEvent private ( + val level: String, + val message: Throwable, + channelName: Option[String], + execId: Option[String]) extends sbt.internal.util.AbstractEntry(channelName, execId) with Serializable { + + + + override def equals(o: Any): Boolean = this.eq(o.asInstanceOf[AnyRef]) || (o match { + case x: TraceEvent => (this.level == x.level) && (this.message == x.message) && (this.channelName == x.channelName) && (this.execId == x.execId) + case _ => false + }) + override def hashCode: Int = { + 37 * (37 * (37 * (37 * (37 * (17 + "sbt.internal.util.TraceEvent".##) + level.##) + message.##) + channelName.##) + execId.##) + } + override def toString: String = { + "TraceEvent(" + level + ", " + message + ", " + channelName + ", " + execId + ")" + } + private[this] def copy(level: String = level, message: Throwable = message, channelName: Option[String] = channelName, execId: Option[String] = execId): TraceEvent = { + new TraceEvent(level, message, channelName, execId) + } + def withLevel(level: String): TraceEvent = { + copy(level = level) + } + def withMessage(message: Throwable): TraceEvent = { + copy(message = message) + } + def withChannelName(channelName: Option[String]): TraceEvent = { + copy(channelName = channelName) + } + def withChannelName(channelName: String): TraceEvent = { + copy(channelName = Option(channelName)) + } + def withExecId(execId: Option[String]): TraceEvent = { + copy(execId = execId) + } + def withExecId(execId: String): TraceEvent = { + copy(execId = Option(execId)) + } +} +object TraceEvent { + + def apply(level: String, message: Throwable, channelName: Option[String], execId: Option[String]): TraceEvent = new TraceEvent(level, message, channelName, execId) + def apply(level: String, message: Throwable, channelName: String, execId: String): TraceEvent = new TraceEvent(level, message, Option(channelName), Option(execId)) +} diff --git a/internal/util-logging/src/main/contraband-scala/sbt/internal/util/codec/AbstractEntryFormats.scala b/internal/util-logging/src/main/contraband-scala/sbt/internal/util/codec/AbstractEntryFormats.scala new file mode 100644 index 000000000..396a9cdac --- /dev/null +++ b/internal/util-logging/src/main/contraband-scala/sbt/internal/util/codec/AbstractEntryFormats.scala @@ -0,0 +1,11 @@ +/** + * This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.internal.util.codec + +import _root_.sjsonnew.JsonFormat +trait AbstractEntryFormats { self: sjsonnew.BasicJsonProtocol with sbt.internal.util.codec.StringEventFormats with sbt.internal.util.codec.TraceEventFormats with sbt.internal.util.codec.ProgressItemFormats with sbt.internal.util.codec.ProgressEventFormats => +implicit lazy val AbstractEntryFormat: JsonFormat[sbt.internal.util.AbstractEntry] = flatUnionFormat3[sbt.internal.util.AbstractEntry, sbt.internal.util.StringEvent, sbt.internal.util.TraceEvent, sbt.internal.util.ProgressEvent]("type") +} diff --git a/internal/util-logging/src/main/contraband-scala/sbt/internal/util/codec/JsonProtocol.scala b/internal/util-logging/src/main/contraband-scala/sbt/internal/util/codec/JsonProtocol.scala new file mode 100644 index 000000000..36e6f72d2 --- /dev/null +++ b/internal/util-logging/src/main/contraband-scala/sbt/internal/util/codec/JsonProtocol.scala @@ -0,0 +1,15 @@ +/** + * This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.internal.util.codec +trait JsonProtocol extends sjsonnew.BasicJsonProtocol + with sbt.internal.util.codec.StringEventFormats + with sbt.internal.util.codec.TraceEventFormats + with sbt.internal.util.codec.ProgressItemFormats + with sbt.internal.util.codec.ProgressEventFormats + with sbt.internal.util.codec.AbstractEntryFormats + with sbt.internal.util.codec.SuccessEventFormats + with sbt.internal.util.codec.LogOptionFormats +object JsonProtocol extends JsonProtocol \ No newline at end of file diff --git a/internal/util-logging/src/main/contraband-scala/sbt/internal/util/codec/LogOptionFormats.scala b/internal/util-logging/src/main/contraband-scala/sbt/internal/util/codec/LogOptionFormats.scala new file mode 100644 index 000000000..6de8174bd --- /dev/null +++ b/internal/util-logging/src/main/contraband-scala/sbt/internal/util/codec/LogOptionFormats.scala @@ -0,0 +1,31 @@ +/** + * This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.internal.util.codec +import _root_.sjsonnew.{ Unbuilder, Builder, JsonFormat, deserializationError } +trait LogOptionFormats { self: sjsonnew.BasicJsonProtocol => +implicit lazy val LogOptionFormat: JsonFormat[sbt.internal.util.LogOption] = new JsonFormat[sbt.internal.util.LogOption] { + override def read[J](__jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.internal.util.LogOption = { + __jsOpt match { + case Some(__js) => + unbuilder.readString(__js) match { + case "Always" => sbt.internal.util.LogOption.Always + case "Never" => sbt.internal.util.LogOption.Never + case "Auto" => sbt.internal.util.LogOption.Auto + } + case None => + deserializationError("Expected JsString but found None") + } + } + override def write[J](obj: sbt.internal.util.LogOption, builder: Builder[J]): Unit = { + val str = obj match { + case sbt.internal.util.LogOption.Always => "Always" + case sbt.internal.util.LogOption.Never => "Never" + case sbt.internal.util.LogOption.Auto => "Auto" + } + builder.writeString(str) + } +} +} diff --git a/internal/util-logging/src/main/contraband-scala/sbt/internal/util/codec/ProgressEventFormats.scala b/internal/util-logging/src/main/contraband-scala/sbt/internal/util/codec/ProgressEventFormats.scala new file mode 100644 index 000000000..ea9c7b825 --- /dev/null +++ b/internal/util-logging/src/main/contraband-scala/sbt/internal/util/codec/ProgressEventFormats.scala @@ -0,0 +1,39 @@ +/** + * This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.internal.util.codec +import _root_.sjsonnew.{ Unbuilder, Builder, JsonFormat, deserializationError } +trait ProgressEventFormats { self: sbt.internal.util.codec.ProgressItemFormats with sjsonnew.BasicJsonProtocol => +implicit lazy val ProgressEventFormat: JsonFormat[sbt.internal.util.ProgressEvent] = new JsonFormat[sbt.internal.util.ProgressEvent] { + override def read[J](__jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.internal.util.ProgressEvent = { + __jsOpt match { + case Some(__js) => + unbuilder.beginObject(__js) + val level = unbuilder.readField[String]("level") + val items = unbuilder.readField[Vector[sbt.internal.util.ProgressItem]]("items") + val lastTaskCount = unbuilder.readField[Option[Int]]("lastTaskCount") + val channelName = unbuilder.readField[Option[String]]("channelName") + val execId = unbuilder.readField[Option[String]]("execId") + val command = unbuilder.readField[Option[String]]("command") + val skipIfActive = unbuilder.readField[Option[Boolean]]("skipIfActive") + unbuilder.endObject() + sbt.internal.util.ProgressEvent(level, items, lastTaskCount, channelName, execId, command, skipIfActive) + case None => + deserializationError("Expected JsObject but found None") + } + } + override def write[J](obj: sbt.internal.util.ProgressEvent, builder: Builder[J]): Unit = { + builder.beginObject() + builder.addField("level", obj.level) + builder.addField("items", obj.items) + builder.addField("lastTaskCount", obj.lastTaskCount) + builder.addField("channelName", obj.channelName) + builder.addField("execId", obj.execId) + builder.addField("command", obj.command) + builder.addField("skipIfActive", obj.skipIfActive) + builder.endObject() + } +} +} diff --git a/internal/util-logging/src/main/contraband-scala/sbt/internal/util/codec/ProgressItemFormats.scala b/internal/util-logging/src/main/contraband-scala/sbt/internal/util/codec/ProgressItemFormats.scala new file mode 100644 index 000000000..580e72d98 --- /dev/null +++ b/internal/util-logging/src/main/contraband-scala/sbt/internal/util/codec/ProgressItemFormats.scala @@ -0,0 +1,29 @@ +/** + * This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.internal.util.codec +import _root_.sjsonnew.{ Unbuilder, Builder, JsonFormat, deserializationError } +trait ProgressItemFormats { self: sjsonnew.BasicJsonProtocol => +implicit lazy val ProgressItemFormat: JsonFormat[sbt.internal.util.ProgressItem] = new JsonFormat[sbt.internal.util.ProgressItem] { + override def read[J](__jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.internal.util.ProgressItem = { + __jsOpt match { + case Some(__js) => + unbuilder.beginObject(__js) + val name = unbuilder.readField[String]("name") + val elapsedMicros = unbuilder.readField[Long]("elapsedMicros") + unbuilder.endObject() + sbt.internal.util.ProgressItem(name, elapsedMicros) + case None => + deserializationError("Expected JsObject but found None") + } + } + override def write[J](obj: sbt.internal.util.ProgressItem, builder: Builder[J]): Unit = { + builder.beginObject() + builder.addField("name", obj.name) + builder.addField("elapsedMicros", obj.elapsedMicros) + builder.endObject() + } +} +} diff --git a/internal/util-logging/src/main/contraband-scala/sbt/internal/util/codec/StringEventFormats.scala b/internal/util-logging/src/main/contraband-scala/sbt/internal/util/codec/StringEventFormats.scala new file mode 100644 index 000000000..abdcdf681 --- /dev/null +++ b/internal/util-logging/src/main/contraband-scala/sbt/internal/util/codec/StringEventFormats.scala @@ -0,0 +1,33 @@ +/** + * This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.internal.util.codec +import _root_.sjsonnew.{ Unbuilder, Builder, JsonFormat, deserializationError } +trait StringEventFormats { self: sjsonnew.BasicJsonProtocol => +implicit lazy val StringEventFormat: JsonFormat[sbt.internal.util.StringEvent] = new JsonFormat[sbt.internal.util.StringEvent] { + override def read[J](__jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.internal.util.StringEvent = { + __jsOpt match { + case Some(__js) => + unbuilder.beginObject(__js) + val level = unbuilder.readField[String]("level") + val message = unbuilder.readField[String]("message") + val channelName = unbuilder.readField[Option[String]]("channelName") + val execId = unbuilder.readField[Option[String]]("execId") + unbuilder.endObject() + sbt.internal.util.StringEvent(level, message, channelName, execId) + case None => + deserializationError("Expected JsObject but found None") + } + } + override def write[J](obj: sbt.internal.util.StringEvent, builder: Builder[J]): Unit = { + builder.beginObject() + builder.addField("level", obj.level) + builder.addField("message", obj.message) + builder.addField("channelName", obj.channelName) + builder.addField("execId", obj.execId) + builder.endObject() + } +} +} diff --git a/internal/util-logging/src/main/contraband-scala/sbt/internal/util/codec/SuccessEventFormats.scala b/internal/util-logging/src/main/contraband-scala/sbt/internal/util/codec/SuccessEventFormats.scala new file mode 100644 index 000000000..af6a35e39 --- /dev/null +++ b/internal/util-logging/src/main/contraband-scala/sbt/internal/util/codec/SuccessEventFormats.scala @@ -0,0 +1,27 @@ +/** + * This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.internal.util.codec +import _root_.sjsonnew.{ Unbuilder, Builder, JsonFormat, deserializationError } +trait SuccessEventFormats { self: sjsonnew.BasicJsonProtocol => +implicit lazy val SuccessEventFormat: JsonFormat[sbt.internal.util.SuccessEvent] = new JsonFormat[sbt.internal.util.SuccessEvent] { + override def read[J](__jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.internal.util.SuccessEvent = { + __jsOpt match { + case Some(__js) => + unbuilder.beginObject(__js) + val message = unbuilder.readField[String]("message") + unbuilder.endObject() + sbt.internal.util.SuccessEvent(message) + case None => + deserializationError("Expected JsObject but found None") + } + } + override def write[J](obj: sbt.internal.util.SuccessEvent, builder: Builder[J]): Unit = { + builder.beginObject() + builder.addField("message", obj.message) + builder.endObject() + } +} +} diff --git a/internal/util-logging/src/main/contraband-scala/sbt/internal/util/codec/TraceEventFormats.scala b/internal/util-logging/src/main/contraband-scala/sbt/internal/util/codec/TraceEventFormats.scala new file mode 100644 index 000000000..bef483c6c --- /dev/null +++ b/internal/util-logging/src/main/contraband-scala/sbt/internal/util/codec/TraceEventFormats.scala @@ -0,0 +1,33 @@ +/** + * This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.internal.util.codec +import _root_.sjsonnew.{ Unbuilder, Builder, JsonFormat, deserializationError } +trait TraceEventFormats { self: sjsonnew.BasicJsonProtocol => +implicit lazy val TraceEventFormat: JsonFormat[sbt.internal.util.TraceEvent] = new JsonFormat[sbt.internal.util.TraceEvent] { + override def read[J](__jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.internal.util.TraceEvent = { + __jsOpt match { + case Some(__js) => + unbuilder.beginObject(__js) + val level = unbuilder.readField[String]("level") + val message = unbuilder.readField[Throwable]("message") + val channelName = unbuilder.readField[Option[String]]("channelName") + val execId = unbuilder.readField[Option[String]]("execId") + unbuilder.endObject() + sbt.internal.util.TraceEvent(level, message, channelName, execId) + case None => + deserializationError("Expected JsObject but found None") + } + } + override def write[J](obj: sbt.internal.util.TraceEvent, builder: Builder[J]): Unit = { + builder.beginObject() + builder.addField("level", obj.level) + builder.addField("message", obj.message) + builder.addField("channelName", obj.channelName) + builder.addField("execId", obj.execId) + builder.endObject() + } +} +} diff --git a/internal/util-logging/src/main/contraband/interface.contra.txt b/internal/util-logging/src/main/contraband/interface.contra.txt new file mode 100644 index 000000000..3b5ed4986 --- /dev/null +++ b/internal/util-logging/src/main/contraband/interface.contra.txt @@ -0,0 +1,33 @@ +package sbt.internal.util +@target(Java) +@codecPackage("sbt.internal.util.codec") +@fullCodec("JsonProtocol") + +enum Severity +{ + Info, Warn, Error +} + +type Position { + line: Int + lineContent: String! + offset: Int + pointer: Int + pointerSpace: String + sourcePath: String + sourceFile: java.io.File + startOffset: Int + endOffset: Int + startLine: Int + startColumn: Int + endLine: Int + endColumn: Int +} + +type Problem { + category: String! + severity: Severity! + message: String! + position: Position! + rendered: String +} diff --git a/internal/util-logging/src/main/contraband/logging.contra b/internal/util-logging/src/main/contraband/logging.contra new file mode 100644 index 000000000..447daa3d0 --- /dev/null +++ b/internal/util-logging/src/main/contraband/logging.contra @@ -0,0 +1,53 @@ +package sbt.internal.util +@target(Scala) +@codecPackage("sbt.internal.util.codec") +@fullCodec("JsonProtocol") + +interface AbstractEntry { + channelName: String + execId: String +} + +type StringEvent implements sbt.internal.util.AbstractEntry { + level: String! + message: String! + channelName: String + execId: String +} + +type TraceEvent implements sbt.internal.util.AbstractEntry { + level: String! + message: Throwable! + channelName: String + execId: String +} + +## used by super shell +type ProgressEvent implements sbt.internal.util.AbstractEntry { + level: String! + items: [sbt.internal.util.ProgressItem] + lastTaskCount: Int + channelName: String + execId: String + command: String @since("1.4.0") + skipIfActive: Boolean @since("1.4.0") +} + +## used by super shell +type ProgressItem { + ## name of a task + name: String! + ## current elapsed time in micro seconds + elapsedMicros: Long! +} + +type SuccessEvent { + message: String! +} + +## value for logging options like color +enum LogOption { + Always + Never + Auto +} diff --git a/internal/util-logging/src/main/java/sbt/internal/util/DeprecatedJLine.java b/internal/util-logging/src/main/java/sbt/internal/util/DeprecatedJLine.java new file mode 100644 index 000000000..4e091a2a5 --- /dev/null +++ b/internal/util-logging/src/main/java/sbt/internal/util/DeprecatedJLine.java @@ -0,0 +1,21 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util; + +import org.jline.terminal.TerminalBuilder; + +/** + * This exists to a provide a wrapper to TerminalBuilder.setTerminalOverride that will not emit a + * deprecation warning when called from scala. + */ +public class DeprecatedJLine { + @SuppressWarnings("deprecation") + public static void setTerminalOverride(final org.jline.terminal.Terminal terminal) { + TerminalBuilder.setTerminalOverride(terminal); + } +} diff --git a/internal/util-logging/src/main/scala/com/github/ghik/silencer/silent.scala b/internal/util-logging/src/main/scala/com/github/ghik/silencer/silent.scala new file mode 100644 index 000000000..0918e75aa --- /dev/null +++ b/internal/util-logging/src/main/scala/com/github/ghik/silencer/silent.scala @@ -0,0 +1,17 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package com.github.ghik.silencer + +import scala.annotation.Annotation + +/** + * When silencer compiler plugin is enabled, this annotation suppresses all warnings emitted by scalac for some portion + * of source code. It can be applied on any definition (`class`, def`, `val`, `var`, etc.) or on arbitrary expression, + * e.g. {123; 456}: @silent` + */ +class silent extends Annotation diff --git a/internal/util-logging/src/main/scala/sbt/internal/util/BasicLogger.scala b/internal/util-logging/src/main/scala/sbt/internal/util/BasicLogger.scala new file mode 100644 index 000000000..802e60a42 --- /dev/null +++ b/internal/util-logging/src/main/scala/sbt/internal/util/BasicLogger.scala @@ -0,0 +1,23 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util + +import sbt.util._ + +/** Implements the level-setting methods of Logger.*/ +abstract class BasicLogger extends AbstractLogger { + private var traceEnabledVar: Int = java.lang.Integer.MAX_VALUE + private var level: Level.Value = Level.Info + private var successEnabledVar = true + def successEnabled: Boolean = synchronized { successEnabledVar } + def setSuccessEnabled(flag: Boolean): Unit = synchronized { successEnabledVar = flag } + def getLevel: Level.Value = synchronized { level } + def setLevel(newLevel: Level.Value): Unit = synchronized { level = newLevel } + def setTrace(level: Int): Unit = synchronized { traceEnabledVar = level } + def getTrace: Int = synchronized { traceEnabledVar } +} diff --git a/internal/util-logging/src/main/scala/sbt/internal/util/BufferedLogger.scala b/internal/util-logging/src/main/scala/sbt/internal/util/BufferedLogger.scala new file mode 100644 index 000000000..827cfa82f --- /dev/null +++ b/internal/util-logging/src/main/scala/sbt/internal/util/BufferedLogger.scala @@ -0,0 +1,251 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util + +import sbt.util._ +import scala.collection.mutable.ListBuffer +import org.apache.logging.log4j.core.{ LogEvent => XLogEvent } +import org.apache.logging.log4j.core.appender.AbstractAppender +import org.apache.logging.log4j.core.layout.PatternLayout +import java.util.concurrent.atomic.AtomicInteger +import java.util.concurrent.atomic.AtomicReference + +object BufferedAppender { + def generateName: String = + "buffered-" + generateId.incrementAndGet + + private val generateId: AtomicInteger = new AtomicInteger + + def apply(delegate: Appender): BufferedAppender = + apply(generateName, delegate) + + def apply(name: String, delegate: Appender): BufferedAppender = + new BufferedAppender(name, delegate) +} + +/** + * An appender that can buffer the logging done on it and then can flush the buffer + * to the delegate appender provided in the constructor. Use 'record()' to + * start buffering and then 'play' to flush the buffer to the backing appender. + * The logging level set at the time a message is originally logged is used, not + * the level at the time 'play' is called. + */ +class BufferedAppender(override val name: String, delegate: Appender) extends Appender { + override def close(): Unit = log4j.get match { + case null => + case a => a.stop() + } + override private[sbt] def properties: ConsoleAppender.Properties = delegate.properties + override private[sbt] def suppressedMessage: SuppressedTraceContext => Option[String] = + delegate.suppressedMessage + private[this] val log4j = new AtomicReference[AbstractAppender] + override private[sbt] def toLog4J = log4j.get match { + case null => + val a = new AbstractAppender( + delegate.name + "-log4j", + null, + PatternLayout.createDefaultLayout(), + true, + Array.empty + ) { + start() + override def append(event: XLogEvent): Unit = { + if (recording) { + Util.ignoreResult(buffer.add(Left(event.toImmutable))) + } else { + delegate.toLog4J.append(event) + } + } + } + log4j.set(a) + a + case a => a + } + + private[this] val buffer = + new java.util.Vector[Either[XLogEvent, (Level.Value, Option[String], Option[ObjectEvent[_]])]] + private[this] var recording = false + + override def appendLog(level: Level.Value, message: => String): Unit = { + if (recording) Util.ignoreResult(buffer.add(Right((level, Some(message), None)))) + else delegate.appendLog(level, message) + } + override private[sbt] def appendObjectEvent[T]( + level: Level.Value, + message: => ObjectEvent[T] + ): Unit = { + if (recording) Util.ignoreResult(buffer.add(Right(((level, None, Some(message)))))) + else delegate.appendObjectEvent(level, message) + } + + /** Enables buffering. */ + def record() = synchronized { recording = true } + def buffer[T](f: => T): T = { + record() + try { + f + } finally { + stopQuietly() + } + } + def bufferQuietly[T](f: => T): T = { + record() + try { + val result = f + clearBuffer() + result + } catch { case e: Throwable => stopQuietly(); throw e } + } + def stopQuietly() = synchronized { + try { + stopBuffer() + } catch { case _: Exception => () } + } + + /** + * Flushes the buffer to the delegate logger. This method calls logAll on the delegate + * so that the messages are written consecutively. The buffer is cleared in the process. + */ + def play(): Unit = + synchronized { + buffer.forEach { + case Right((l, Some(m), _)) => delegate.appendLog(l, m) + case Right((l, _, Some(oe))) => delegate.appendObjectEvent(l, oe) + case Left(x) => delegate.toLog4J.append(x) + case _ => + } + buffer.clear() + } + + /** Clears buffered events and disables buffering. */ + def clearBuffer(): Unit = synchronized { buffer.clear(); recording = false } + + /** Plays buffered events and disables buffering. */ + def stopBuffer(): Unit = synchronized { play(); clearBuffer() } + +} + +/** + * A logger that can buffer the logging done on it and then can flush the buffer + * to the delegate logger provided in the constructor. Use 'startRecording' to + * start buffering and then 'play' from to flush the buffer to the backing logger. + * The logging level set at the time a message is originally logged is used, not + * the level at the time 'play' is called. + * + * This class assumes that it is the only client of the delegate logger. + */ +class BufferedLogger(delegate: AbstractLogger) extends BasicLogger { + private[this] val buffer = new ListBuffer[LogEvent] + private[this] var recording = false + + /** Enables buffering. */ + def record() = synchronized { recording = true } + def buffer[T](f: => T): T = { + record() + try { + f + } finally { + stopQuietly() + } + } + def bufferQuietly[T](f: => T): T = { + record() + try { + val result = f + clear() + result + } catch { case e: Throwable => stopQuietly(); throw e } + } + def stopQuietly() = synchronized { + try { + stop() + } catch { case _: Exception => () } + } + + /** + * Flushes the buffer to the delegate logger. This method calls logAll on the delegate + * so that the messages are written consecutively. The buffer is cleared in the process. + */ + def play(): Unit = synchronized { delegate.logAll(buffer.toList); buffer.clear() } + + /** Clears buffered events and disables buffering. */ + def clear(): Unit = synchronized { buffer.clear(); recording = false } + + /** Plays buffered events and disables buffering. */ + def stop(): Unit = synchronized { play(); clear() } + + @deprecated("No longer used.", "1.0.0") + override def ansiCodesSupported = delegate.ansiCodesSupported + + override def setLevel(newLevel: Level.Value): Unit = synchronized { + super.setLevel(newLevel) + if (recording) + buffer += new SetLevel(newLevel) + else + delegate.setLevel(newLevel) + () + } + + override def setSuccessEnabled(flag: Boolean): Unit = synchronized { + super.setSuccessEnabled(flag) + if (recording) + buffer += new SetSuccess(flag) + else + delegate.setSuccessEnabled(flag) + () + } + + override def setTrace(level: Int): Unit = synchronized { + super.setTrace(level) + if (recording) + buffer += new SetTrace(level) + else + delegate.setTrace(level) + () + } + + def trace(t: => Throwable): Unit = doBufferableIf(traceEnabled, new Trace(t), _.trace(t)) + + def success(message: => String): Unit = + doBufferable(Level.Info, new Success(message), _.success(message)) + + def log(level: Level.Value, message: => String): Unit = + doBufferable(level, new Log(level, message), _.log(level, message)) + + def logAll(events: Seq[LogEvent]): Unit = synchronized { + if (recording) + buffer ++= events + else + delegate.logAll(events) + () + } + + def control(event: ControlEvent.Value, message: => String): Unit = + doBufferable(Level.Info, new ControlEvent(event, message), _.control(event, message)) + + private def doBufferable( + level: Level.Value, + appendIfBuffered: => LogEvent, + doUnbuffered: AbstractLogger => Unit + ): Unit = + doBufferableIf(atLevel(level), appendIfBuffered, doUnbuffered) + + private def doBufferableIf( + condition: => Boolean, + appendIfBuffered: => LogEvent, + doUnbuffered: AbstractLogger => Unit + ): Unit = synchronized { + if (condition) { + if (recording) + buffer += appendIfBuffered + else + doUnbuffered(delegate) + } + () + } +} diff --git a/internal/util-logging/src/main/scala/sbt/internal/util/ConsoleAppender.scala b/internal/util-logging/src/main/scala/sbt/internal/util/ConsoleAppender.scala new file mode 100644 index 000000000..7cfed50e6 --- /dev/null +++ b/internal/util-logging/src/main/scala/sbt/internal/util/ConsoleAppender.scala @@ -0,0 +1,592 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util + +import java.io.{ PrintStream, PrintWriter } +import java.lang.StringBuilder +import java.nio.channels.ClosedChannelException +import java.util.concurrent.atomic.{ AtomicBoolean, AtomicInteger } + +import org.apache.logging.log4j.core.appender.AbstractAppender +import org.apache.logging.log4j.core.{ Appender => XAppender, LogEvent => XLogEvent } +import org.apache.logging.log4j.message.{ Message, ObjectMessage, ReusableObjectMessage } +import org.apache.logging.log4j.{ Level => XLevel } +import sbt.internal.util.ConsoleAppender._ +import sbt.util._ +import org.apache.logging.log4j.core.AbstractLogEvent +import org.apache.logging.log4j.message.StringFormatterMessageFactory +import java.util.concurrent.atomic.AtomicReference + +object ConsoleLogger { + // These are provided so other modules do not break immediately. + @deprecated("Use EscHelpers.ESC instead", "0.13.x") + final val ESC = EscHelpers.ESC + @deprecated("Use EscHelpers.isEscapeTerminator instead", "0.13.x") + private[sbt] def isEscapeTerminator(c: Char): Boolean = EscHelpers.isEscapeTerminator(c) + @deprecated("Use EscHelpers.hasEscapeSequence instead", "0.13.x") + def hasEscapeSequence(s: String): Boolean = EscHelpers.hasEscapeSequence(s) + @deprecated("Use EscHelpers.removeEscapeSequences instead", "0.13.x") + def removeEscapeSequences(s: String): String = EscHelpers.removeEscapeSequences(s) + @deprecated("Use ConsoleAppender.formatEnabledInEnv instead", "0.13.x") + lazy val formatEnabled = ConsoleAppender.formatEnabledInEnv + @deprecated("Use ConsoleAppender.noSuppressedMessage instead", "0.13.x") + val noSuppressedMessage = ConsoleAppender.noSuppressedMessage + + /** + * A new `ConsoleLogger` that logs to `out`. + * + * @param out Where to log the messages. + * @return A new `ConsoleLogger` that logs to `out`. + */ + def apply(out: PrintStream): ConsoleLogger = apply(ConsoleOut.printStreamOut(out)) + + /** + * A new `ConsoleLogger` that logs to `out`. + * + * @param out Where to log the messages. + * @return A new `ConsoleLogger` that logs to `out`. + */ + def apply(out: PrintWriter): ConsoleLogger = apply(ConsoleOut.printWriterOut(out)) + + /** + * A new `ConsoleLogger` that logs to `out`. + * + * @param out Where to log the messages. + * @param ansiCodesSupported `true` if `out` supported ansi codes, `false` otherwise. + * @param useFormat `true` to show formatting, `false` to remove it from messages. + * @param suppressedMessage How to show suppressed stack traces. + * @return A new `ConsoleLogger` that logs to `out`. + */ + def apply( + out: ConsoleOut = ConsoleOut.systemOut, + ansiCodesSupported: Boolean = Terminal.isAnsiSupported, + useFormat: Boolean = Terminal.isColorEnabled, + suppressedMessage: SuppressedTraceContext => Option[String] = + ConsoleAppender.noSuppressedMessage + ): ConsoleLogger = + new ConsoleLogger(out, ansiCodesSupported, useFormat, suppressedMessage) +} + +/** + * A logger that logs to the console. On supported systems, the level labels are + * colored. + */ +class ConsoleLogger private[ConsoleLogger] ( + out: ConsoleOut, + override val ansiCodesSupported: Boolean, + useFormat: Boolean, + suppressedMessage: SuppressedTraceContext => Option[String] +) extends BasicLogger { + + private[sbt] val appender: Appender = + ConsoleAppender(generateName(), out, ansiCodesSupported, useFormat, suppressedMessage) + + override def control(event: ControlEvent.Value, message: => String): Unit = + appender.control(event, message) + + override def log(level: Level.Value, message: => String): Unit = + if (atLevel(level)) { + appender.appendLog(level, message) + } + + override def success(message: => String): Unit = + if (successEnabled) { + appender.success(message) + } + + override def trace(t: => Throwable): Unit = + appender.trace(t, getTrace) + + override def logAll(events: Seq[LogEvent]) = events.foreach(log) +} + +object ConsoleAppender { + private[sbt] def cursorLeft(n: Int): String = s"\u001B[${n}D" + private[sbt] def cursorUp(n: Int): String = s"\u001B[${n}A" + private[sbt] def cursorDown(n: Int): String = s"\u001B[${n}B" + private[sbt] def scrollUp(n: Int): String = s"\u001B[${n}S" + private[sbt] def clearScreen(n: Int): String = s"\u001B[${n}J" + private[sbt] def clearLine(n: Int): String = s"\u001B[${n}K" + private[sbt] final val DeleteLine = "\u001B[2K" + private[sbt] final val ClearScreenAfterCursor = clearScreen(0) + private[sbt] final val CursorLeft1000 = cursorLeft(1000) + private[sbt] final val CursorDown1 = cursorDown(1) + private[sbt] final val ClearPromptLine = CursorLeft1000 + ClearScreenAfterCursor + private[this] val showProgressHolder: AtomicBoolean = new AtomicBoolean(false) + def setShowProgress(b: Boolean): Unit = showProgressHolder.set(b) + def showProgress: Boolean = showProgressHolder.get + private[sbt] trait Properties { + def isAnsiSupported: Boolean + def isColorEnabled: Boolean + def out: ConsoleOut + } + private[sbt] object Properties { + def from(terminal: Terminal): Properties = new Properties { + override def isAnsiSupported: Boolean = terminal.isAnsiSupported + override def isColorEnabled: Boolean = terminal.isColorEnabled + override def out = ConsoleOut.terminalOut(terminal) + } + def from(o: ConsoleOut, ansi: Boolean, color: Boolean): Properties = new Properties { + override def isAnsiSupported: Boolean = ansi + override def isColorEnabled: Boolean = color + override def out = o + } + } + + /** Hide stack trace altogether. */ + val noSuppressedMessage = (_: SuppressedTraceContext) => None + + /** + * Indicates whether formatting has been disabled in environment variables. + * 1. -Dsbt.log.noformat=true means no formatting. + * 2. -Dsbt.color=always/auto/never/true/false + * 3. -Dsbt.colour=always/auto/never/true/false + * 4. -Dsbt.log.format=always/auto/never/true/false + */ + @deprecated("Use Terminal.isAnsiSupported or Terminal.isColorEnabled", "1.4.0") + lazy val formatEnabledInEnv: Boolean = Terminal.isAnsiSupported + + private[sbt] def parseLogOption(s: String): LogOption = Terminal.parseLogOption(s) match { + case Some(true) => LogOption.Always + case Some(false) => LogOption.Never + case _ => LogOption.Auto + } + + private[this] val generateId: AtomicInteger = new AtomicInteger + + /** + * A new `ConsoleAppender` that writes to standard output. + * + * @return A new `ConsoleAppender` that writes to standard output. + */ + def apply(): Appender = apply(ConsoleOut.systemOut) + + /** + * A new `ConsoleAppender` that appends log message to `out`. + * + * @param out Where to write messages. + * @return A new `ConsoleAppender`. + */ + def apply(out: PrintStream): Appender = apply(ConsoleOut.printStreamOut(out)) + + /** + * A new `ConsoleAppender` that appends log messages to `out`. + * + * @param out Where to write messages. + * @return A new `ConsoleAppender`. + */ + def apply(out: PrintWriter): Appender = apply(ConsoleOut.printWriterOut(out)) + + /** + * A new `ConsoleAppender` that writes to `out`. + * + * @param out Where to write messages. + * @return A new `ConsoleAppender that writes to `out`. + */ + def apply(out: ConsoleOut): Appender = apply(generateName(), out) + + /** + * A new `ConsoleAppender` identified by `name`, and that writes to standard output. + * + * @param name An identifier for the `ConsoleAppender`. + * @return A new `ConsoleAppender` that writes to standard output. + */ + def apply(name: String): Appender = apply(name, ConsoleOut.systemOut) + + /** + * A new `ConsoleAppender` identified by `name`, and that writes to `out`. + * + * @param name An identifier for the `ConsoleAppender`. + * @param out Where to write messages. + * @return A new `ConsoleAppender` that writes to `out`. + */ + def apply(name: String, out: ConsoleOut): Appender = apply(name, out, Terminal.isAnsiSupported) + + /** + * A new `ConsoleAppender` identified by `name`, and that writes to `out`. + * + * @param name An identifier for the `ConsoleAppender`. + * @param out Where to write messages. + * @param suppressedMessage How to handle stack traces. + * @return A new `ConsoleAppender` that writes to `out`. + */ + def apply( + name: String, + out: ConsoleOut, + suppressedMessage: SuppressedTraceContext => Option[String] + ): Appender = { + val ansi = Terminal.isAnsiSupported + apply(name, out, ansi, ansi, suppressedMessage) + } + + /** + * A new `ConsoleAppender` identified by `name`, and that writes to `out`. + * + * @param name An identifier for the `ConsoleAppender`. + * @param out Where to write messages. + * @param useFormat `true` to enable format (color, bold, etc.), `false` to remove formatting. + * @return A new `ConsoleAppender` that writes to `out`. + */ + def apply(name: String, out: ConsoleOut, useFormat: Boolean): Appender = + apply(name, out, useFormat || Terminal.isAnsiSupported, useFormat, noSuppressedMessage) + + /** + * A new `ConsoleAppender` identified by `name`, and that writes to `out`. + * + * @param name An identifier for the `ConsoleAppender`. + * @param terminal The terminal to which this appender corresponds + * @return A new `ConsoleAppender` that writes to `out`. + */ + def apply(name: String, terminal: Terminal): Appender = { + new ConsoleAppender(name, Properties.from(terminal), noSuppressedMessage) + } + + /** + * A new `ConsoleAppender` identified by `name`, and that writes to `out`. + * + * @param name An identifier for the `ConsoleAppender`. + * @param terminal The terminal to which this appender corresponds + * @param suppressedMessage How to handle stack traces. + * @return A new `ConsoleAppender` that writes to `out`. + */ + def apply( + name: String, + terminal: Terminal, + suppressedMessage: SuppressedTraceContext => Option[String] + ): Appender = { + new ConsoleAppender(name, Properties.from(terminal), suppressedMessage) + } + + /** + * A new `ConsoleAppender` identified by `name`, and that writes to `out`. + * + * @param name An identifier for the `ConsoleAppender`. + * @param out Where to write messages. + * @param ansiCodesSupported `true` if the output stream supports ansi codes, `false` otherwise. + * @param useFormat `true` to enable format (color, bold, etc.), `false` to remove + * formatting. + * @return A new `ConsoleAppender` that writes to `out`. + */ + def apply( + name: String, + out: ConsoleOut, + ansiCodesSupported: Boolean, + useFormat: Boolean, + suppressedMessage: SuppressedTraceContext => Option[String] + ): Appender = { + new ConsoleAppender( + name, + Properties.from(out, ansiCodesSupported, useFormat), + suppressedMessage + ) + } + + /** + * Converts the Log4J `level` to the corresponding sbt level. + * + * @param level A level, as represented by Log4J. + * @return The corresponding level in sbt's world. + */ + def toLevel(level: XLevel): Level.Value = + level match { + case XLevel.OFF => Level.Debug + case XLevel.FATAL => Level.Error + case XLevel.ERROR => Level.Error + case XLevel.WARN => Level.Warn + case XLevel.INFO => Level.Info + case XLevel.DEBUG => Level.Debug + case _ => Level.Debug + } + + /** + * Converts the sbt `level` to the corresponding Log4J level. + * + * @param level A level, as represented by sbt. + * @return The corresponding level in Log4J's world. + */ + def toXLevel(level: Level.Value): XLevel = + level match { + case Level.Error => XLevel.ERROR + case Level.Warn => XLevel.WARN + case Level.Info => XLevel.INFO + case Level.Debug => XLevel.DEBUG + } + + private[sbt] def generateName(): String = "out-" + generateId.incrementAndGet + + private[this] def ansiSupported: Boolean = Terminal.console.isAnsiSupported +} + +// See http://stackoverflow.com/questions/24205093/how-to-create-a-custom-appender-in-log4j2 +// for custom appender using Java. +// http://logging.apache.org/log4j/2.x/manual/customconfig.html +// https://logging.apache.org/log4j/2.x/log4j-core/apidocs/index.html + +/** + * A logger that logs to the console. On supported systems, the level labels are + * colored. + * + * This logger is not thread-safe. + */ +class ConsoleAppender( + override private[sbt] val name: String, + override private[sbt] val properties: Properties, + override private[sbt] val suppressedMessage: SuppressedTraceContext => Option[String] +) extends Appender { + private[this] val log4j = new AtomicReference[XAppender](null) + override private[sbt] lazy val toLog4J = log4j.get match { + case null => + log4j.synchronized { + log4j.get match { + case null => + val l = new Log4JConsoleAppender(name, properties, suppressedMessage, { event => + val level = ConsoleAppender.toLevel(event.getLevel) + val message = event.getMessage + try appendMessage(level, message) + catch { case _: ClosedChannelException => } + }) + log4j.set(l) + l + case l => l + } + } + } + override def close(): Unit = log4j.get match { + case null => + case a => a.stop() + } +} +trait Appender extends AutoCloseable { + private[sbt] def name: String + private[sbt] def properties: Properties + private[sbt] def suppressedMessage: SuppressedTraceContext => Option[String] + import scala.Console.{ BLUE, GREEN, RED, YELLOW } + + private[util] def out: ConsoleOut = properties.out + private[util] def ansiCodesSupported: Boolean = properties.isAnsiSupported + private[util] def useFormat: Boolean = properties.isColorEnabled + + private def reset: String = scala.Console.RESET + + private val SUCCESS_LABEL_COLOR = GREEN + private val SUCCESS_MESSAGE_COLOR = reset + private val NO_COLOR = reset + + private var traceEnabledVar: Int = Int.MaxValue + + def setTrace(level: Int): Unit = synchronized { traceEnabledVar = level } + + /** + * Returns the number of lines for stacktrace. + */ + def getTrace: Int = synchronized { traceEnabledVar } + + private[sbt] def toLog4J: XAppender + + /** + * Logs the stack trace of `t`, possibly shortening it. + * + * The `traceLevel` parameter configures how the stack trace will be shortened. + * See `StackTrace.trimmed`. + * + * @param t The `Throwable` whose stack trace to log. + * @param traceLevel How to shorten the stack trace. + */ + def trace(t: => Throwable, traceLevel: Int): Unit = { + if (traceLevel >= 0) + write(StackTrace.trimmed(t, traceLevel)) + if (traceLevel <= 2) { + val ctx = new SuppressedTraceContext(traceLevel, ansiCodesSupported && useFormat) + for (msg <- suppressedMessage(ctx)) + appendLog(NO_COLOR, "trace", NO_COLOR, msg) + } + } + + /** + * Logs a `ControlEvent` to the log. + * + * @param event The kind of `ControlEvent`. + * @param message The message to log. + */ + def control(event: ControlEvent.Value, message: => String): Unit = + appendLog(labelColor(Level.Info), Level.Info.toString, BLUE, message) + + /** + * Appends the message `message` to the to the log at level `level`. + * + * @param level The importance level of the message. + * @param message The message to log. + */ + def appendLog(level: Level.Value, message: => String): Unit = { + appendLog(labelColor(level), level.toString, NO_COLOR, message) + } + + /** + * Select the right color for the label given `level`. + * + * @param level The label to consider to select the color. + * @return The color to use to color the label. + */ + private def labelColor(level: Level.Value): String = + level match { + case Level.Error => RED + case Level.Warn => YELLOW + case _ => NO_COLOR + } + + /** + * Appends a full message to the log. Each line is prefixed with `[$label]`, written in + * `labelColor` if formatting is enabled. The lines of the messages are colored with + * `messageColor` if formatting is enabled. + * + * @param labelColor The color to use to format the label. + * @param label The label to prefix each line with. The label is shown between square + * brackets. + * @param messageColor The color to use to format the message. + * @param message The message to write. + */ + private def appendLog( + labelColor: String, + label: String, + messageColor: String, + message: String + ): Unit = + try { + // according to https://github.com/sbt/sbt/issues/5608, sometimes we get a null message + if (message == null) () + else { + val len = labelColor.length + label.length + messageColor.length + reset.length * 3 + val builder: StringBuilder = new StringBuilder(len) + message.linesIterator.foreach { line => + builder.ensureCapacity(len + line.length + 4) + builder.setLength(0) + + def fmted(a: String, b: String) = { + if (useFormat) builder.append(reset).append(a).append(b).append(reset) + else builder.append(b) + } + + if (useFormat) builder.append(reset) + builder.append('[') + fmted(labelColor, label) + builder.append("] ") + fmted(messageColor, line) + write(builder.toString) + } + } + } catch { case _: InterruptedException => } + + // success is called by ConsoleLogger. + private[sbt] def success(message: => String): Unit = { + appendLog(SUCCESS_LABEL_COLOR, Level.SuccessLabel, SUCCESS_MESSAGE_COLOR, message) + } + + private def write(msg: String): Unit = { + // There is no api for removing only colors but not other ansi escape sequences + // so we do nothing if useFormat is false but ansiCodesSupported is true which is + // a rare use case but if ansiCodesSupported is true, color codes should work so + // the output may have unwanted colors but it would still be legible. This should + // only be relevant if the log message string itself contains ansi escape sequences + // other than color codes which is very unlikely. + val toWrite = if ((!ansiCodesSupported || !useFormat) && msg.getBytes.contains(27.toByte)) { + val (bytes, len) = + EscHelpers.strip(msg.getBytes, stripAnsi = !ansiCodesSupported, stripColor = !useFormat) + new String(bytes, 0, len) + } else msg + out.println(toWrite) + } + + private[util] def appendMessage(level: Level.Value, msg: Message): Unit = + msg match { + case o: ObjectMessage => appendMessageContent(level, o.getParameter) + case o: ReusableObjectMessage => appendMessageContent(level, o.getParameter) + case _ => appendLog(level, msg.getFormattedMessage) + } + + private def appendTraceEvent(te: TraceEvent): Unit = { + val traceLevel = getTrace + if (traceLevel >= 0) { + val throwableShowLines: ShowLines[Throwable] = + ShowLines[Throwable]((t: Throwable) => { + List(StackTrace.trimmed(t, traceLevel)) + }) + val codec: ShowLines[TraceEvent] = + ShowLines[TraceEvent]((t: TraceEvent) => { + throwableShowLines.showLines(t.message) + }) + codec.showLines(te).toVector foreach { appendLog(Level.Error, _) } + } + if (traceLevel <= 2) { + suppressedMessage(new SuppressedTraceContext(traceLevel, ansiCodesSupported && useFormat)) foreach { + appendLog(Level.Error, _) + } + } + } + + private def appendMessageContent(level: Level.Value, o: AnyRef): Unit = { + def appendEvent(oe: ObjectEvent[_]): Unit = { + val contentType = oe.contentType + contentType match { + case "sbt.internal.util.TraceEvent" => appendTraceEvent(oe.message.asInstanceOf[TraceEvent]) + case "sbt.internal.util.ProgressEvent" => + case _ => + LogExchange.stringCodec[AnyRef](contentType) match { + case Some(codec) if contentType == "sbt.internal.util.SuccessEvent" => + codec.showLines(oe.message.asInstanceOf[AnyRef]).toVector foreach { success(_) } + case Some(codec) => + codec.showLines(oe.message.asInstanceOf[AnyRef]).toVector foreach (appendLog( + level, + _ + )) + case _ => appendLog(level, oe.message.toString) + } + } + } + + o match { + case x: StringEvent => Vector(x.message) foreach { appendLog(level, _) } + case x: ObjectEvent[_] => appendEvent(x) + case _ => Vector(o.toString) foreach { appendLog(level, _) } + } + } + private[sbt] def appendObjectEvent[T](level: Level.Value, message: => ObjectEvent[T]): Unit = + appendMessageContent(level, message) + +} +private[internal] class Log4JConsoleAppender( + override private[sbt] val name: String, + override private[sbt] val properties: Properties, + override private[sbt] val suppressedMessage: SuppressedTraceContext => Option[String], + appendEvent: XLogEvent => Unit, +) extends AbstractAppender(name, null, LogExchange.dummyLayout, true, Array.empty) + with Appender { + start() + override def close(): Unit = stop() + override private[sbt] def toLog4J: XAppender = this + override def append(event: XLogEvent): Unit = appendEvent(event) +} +private[sbt] class ConsoleAppenderFromLog4J( + override private[sbt] val name: String, + override private[sbt] val properties: Properties, + override private[sbt] val suppressedMessage: SuppressedTraceContext => Option[String], + val delegate: XAppender, +) extends Appender { + def this(name: String, delegate: XAppender) = + this(name, Properties.from(Terminal.get), _ => None, delegate) + override def close(): Unit = delegate.stop() + private[sbt] def toLog4J: XAppender = delegate + override def appendLog(level: sbt.util.Level.Value, message: => String): Unit = { + delegate.append(new AbstractLogEvent { + override def getLevel(): XLevel = ConsoleAppender.toXLevel(level) + override def getMessage(): Message = + StringFormatterMessageFactory.INSTANCE.newMessage(message.toString, Array.empty) + }) + } +} + +final class SuppressedTraceContext(val traceLevel: Int, val useFormat: Boolean) diff --git a/internal/util-logging/src/main/scala/sbt/internal/util/ConsoleOut.scala b/internal/util-logging/src/main/scala/sbt/internal/util/ConsoleOut.scala new file mode 100644 index 000000000..b946a0cc8 --- /dev/null +++ b/internal/util-logging/src/main/scala/sbt/internal/util/ConsoleOut.scala @@ -0,0 +1,132 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util + +import java.io.{ BufferedWriter, PrintStream, PrintWriter } +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.atomic.AtomicReference + +sealed trait ConsoleOut { + val lockObject: AnyRef + def print(s: String): Unit + def println(s: String): Unit + def println(): Unit + def flush(): Unit +} + +object ConsoleOut { + def systemOut: ConsoleOut = terminalOut + private[sbt] object NullConsoleOut extends ConsoleOut { + override val lockObject: AnyRef = this + override def print(s: String): Unit = {} + override def println(): Unit = {} + override def println(s: String): Unit = {} + override def flush(): Unit = {} + } + private[sbt] def globalProxy: ConsoleOut = Proxy + private[sbt] def setGlobalProxy(out: ConsoleOut): Unit = Proxy.set(out) + private[sbt] def getGlobalProxy: ConsoleOut = Proxy.proxy.get + private object Proxy extends ConsoleOut { + private[ConsoleOut] val proxy = new AtomicReference[ConsoleOut](systemOut) + private[this] def get: ConsoleOut = proxy.get + def set(proxy: ConsoleOut): Unit = this.proxy.set(proxy) + override val lockObject: AnyRef = proxy + override def print(s: String): Unit = get.print(s) + override def println(s: String): Unit = get.println(s) + override def println(): Unit = get.println() + override def flush(): Unit = get.flush() + override def toString: String = s"ProxyConsoleOut" + } + + def overwriteContaining(s: String): (String, String) => Boolean = + (cur, prev) => cur.contains(s) && prev.contains(s) + + /** Move to beginning of previous line and clear the line. */ + private[this] final val OverwriteLine = "\u001B[A\r\u001B[2K" + + /** + * ConsoleOut instance that is backed by System.out. It overwrites the previously printed line + * if the function `f(lineToWrite, previousLine)` returns true. + * + * The ConsoleOut returned by this method assumes that the only newlines are from println calls + * and not in the String arguments. + */ + def systemOutOverwrite(f: (String, String) => Boolean): ConsoleOut = new ConsoleOut { + val lockObject = System.out + private[this] var last: Option[String] = None + private[this] var current = new java.lang.StringBuffer + def print(s: String): Unit = synchronized { current.append(s); () } + def println(s: String): Unit = synchronized { current.append(s); println() } + def println(): Unit = synchronized { + val s = current.toString + if (Terminal.isAnsiSupported && last.exists(lmsg => f(s, lmsg))) + lockObject.print(OverwriteLine) + lockObject.println(s) + last = Some(s) + current.setLength(0) + } + def flush(): Unit = synchronized { + val s = current.toString + if (Terminal.isAnsiSupported && last.exists(lmsg => f(s, lmsg))) + lockObject.print(OverwriteLine) + lockObject.print(s) + last = Some(s) + current.setLength(0) + } + override def toString: String = s"SystemOutOverwrite@${System.identityHashCode(this)}" + } + + def terminalOut: ConsoleOut = new ConsoleOut { + override val lockObject: AnyRef = System.out + override def print(s: String): Unit = Terminal.get.printStream.print(s) + override def println(s: String): Unit = Terminal.get.printStream.println(s) + override def println(): Unit = Terminal.get.printStream.println() + override def flush(): Unit = Terminal.get.printStream.flush() + override def toString: String = s"TerminalOut" + } + + private[this] val consoleOutPerTerminal = new ConcurrentHashMap[Terminal, ConsoleOut] + def terminalOut(terminal: Terminal): ConsoleOut = consoleOutPerTerminal.get(terminal) match { + case null => + val res = new ConsoleOut { + override val lockObject: AnyRef = terminal + override def print(s: String): Unit = terminal.printStream.print(s) + override def println(s: String): Unit = terminal.printStream.println(s) + override def println(): Unit = terminal.printStream.println() + override def flush(): Unit = terminal.printStream.flush() + override def toString: String = s"TerminalOut($terminal)" + } + consoleOutPerTerminal.put(terminal, res) + res + case c => c + } + def printStreamOut(out: PrintStream): ConsoleOut = new ConsoleOut { + val lockObject = out + def print(s: String) = out.print(s) + def println(s: String) = out.println(s) + def println() = out.println() + def flush() = out.flush() + override def toString: String = s"PrintStreamConsoleOut($out)" + } + def printWriterOut(out: PrintWriter): ConsoleOut = new ConsoleOut { + val lockObject = out + def print(s: String) = out.print(s) + def println(s: String) = { out.println(s); flush() } + def println() = { out.println(); flush() } + def flush() = { out.flush() } + override def toString: String = s"PrintWriterConsoleOut($out)" + } + def bufferedWriterOut(out: BufferedWriter): ConsoleOut = new ConsoleOut { + val lockObject = out + def print(s: String) = out.write(s) + def println(s: String) = { out.write(s); println() } + def println() = { out.newLine(); flush() } + def flush() = { out.flush() } + override def toString: String = s"BufferedWriterConsoleOut($out)" + } +} diff --git a/internal/util-logging/src/main/scala/sbt/internal/util/EscHelpers.scala b/internal/util-logging/src/main/scala/sbt/internal/util/EscHelpers.scala new file mode 100644 index 000000000..ee84680c6 --- /dev/null +++ b/internal/util-logging/src/main/scala/sbt/internal/util/EscHelpers.scala @@ -0,0 +1,253 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util + +import scala.collection.mutable.ArrayBuffer +import scala.util.Try + +object EscHelpers { + + /** Escape character, used to introduce an escape sequence. */ + final val ESC = '\u001B' + + /** + * An escape terminator is a character in the range `@` (decimal value 64) to `~` (decimal value 126). + * It is the final character in an escape sequence. + * + * cf. http://en.wikipedia.org/wiki/ANSI_escape_code#CSI_codes + */ + private[sbt] def isEscapeTerminator(c: Char): Boolean = + c >= '@' && c <= '~' + + /** + * Test if the character AFTER an ESC is the ANSI CSI. + * + * see: http://en.wikipedia.org/wiki/ANSI_escape_code + * + * The CSI (control sequence instruction) codes start with ESC + '['. This is for testing the second character. + * + * There is an additional CSI (one character) that we could test for, but is not frequnetly used, and we don't + * check for it. + * + * cf. http://en.wikipedia.org/wiki/ANSI_escape_code#CSI_codes + */ + private def isCSI(c: Char): Boolean = c == '[' + + /** + * Tests whether or not a character needs to immediately terminate the ANSI sequence. + * + * c.f. http://en.wikipedia.org/wiki/ANSI_escape_code#Sequence_elements + */ + private def isAnsiTwoCharacterTerminator(c: Char): Boolean = + (c >= '@') && (c <= '_') + + /** + * Returns true if the string contains the ESC character. + * + * TODO - this should handle raw CSI (not used much) + */ + def hasEscapeSequence(s: String): Boolean = + s.indexOf(ESC) >= 0 + + /** + * Returns the string `s` with escape sequences removed. + * An escape sequence starts with the ESC character (decimal value 27) and ends with an escape terminator. + * @see isEscapeTerminator + */ + def removeEscapeSequences(s: String): String = + if (s.isEmpty || !hasEscapeSequence(s)) + s + else { + val sb = new java.lang.StringBuilder + nextESC(s, 0, sb) + sb.toString + } + + private[this] def nextESC(s: String, start: Int, sb: java.lang.StringBuilder): Unit = { + val escIndex = s.indexOf(ESC, start) + if (escIndex < 0) { + sb.append(s, start, s.length) + () + } else { + sb.append(s, start, escIndex) + val next: Int = + if (escIndex + 1 >= s.length) skipESC(s, escIndex + 1) + // If it's a CSI we skip past it and then look for a terminator. + else if (isCSI(s.charAt(escIndex + 1))) skipESC(s, escIndex + 2) + else if (isAnsiTwoCharacterTerminator(s.charAt(escIndex + 1))) escIndex + 2 + else { + // There could be non-ANSI character sequences we should make sure we handle here. + skipESC(s, escIndex + 1) + } + nextESC(s, next, sb) + } + } + private[this] val esc = 1 + private[this] val csi = 2 + def cursorPosition(s: String): Int = { + val bytes = s.getBytes + var i = 0 + var index = 0 + var state = 0 + val digit = new ArrayBuffer[Byte] + var leftDigit = -1 + while (i < bytes.length) { + bytes(i) match { + case 27 => state = esc + case b if (state == esc || state == csi) && b >= 48 && b < 58 => + state = csi + digit += b + case '[' if state == esc => state = csi + case 8 => + state = 0 + index = index - 1 + case b if state == csi => + leftDigit = Try(new String(digit.toArray).toInt).getOrElse(0) + state = 0 + b.toChar match { + case 'D' => index = math.max(index - leftDigit, 0) + case 'C' => index += leftDigit + case 'K' => + case 'J' => if (leftDigit == 2) index = 0 + case 'm' => + case ';' => state = csi + case _ => + } + digit.clear() + case _ => + index += 1 + } + i += 1 + } + index + } + + /** + * Strips ansi escape and color codes from an input string. + * + * @param bytes the input bytes + * @param stripAnsi toggles whether or not to remove general ansi escape codes + * @param stripColor toggles whether or not to remove ansi color codes + * @return a string with the escape and color codes removed depending on the input + * parameter along with the length of the output string (which may be smaller than + * the returned array) + */ + def strip(bytes: Array[Byte], stripAnsi: Boolean, stripColor: Boolean): (Array[Byte], Int) = { + val res = Array.fill[Byte](bytes.length)(0) + var i = 0 + var index = 0 + var state = 0 + var limit = 0 + val digit = new ArrayBuffer[Byte] + var leftDigit = -1 + var escIndex = -1 + bytes.foreach { b => + if (index < res.length) res(index) = b + index += 1 + limit = math.max(limit, index) + if (state == 0) escIndex = -1 + b match { + case 27 => + escIndex = index - 1 + state = esc + case b if (state == esc || state == csi) && b >= 48 && b < 58 => + state = csi + digit += b + case '[' if state == esc => state = csi + case 8 => + state = 0 + index = math.max(index - 1, 0) + case b if state == csi => + leftDigit = Try(new String(digit.toArray).toInt).getOrElse(0) + state = 0 + b.toChar match { + case 'h' | 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'J' | 'K' => + if (stripAnsi) index = math.max(escIndex, 0) + case 'm' => if (stripColor) index = escIndex + case ';' | 's' | 'u' | '?' => state = csi + case b => + } + digit.clear() + case b if state == esc => state = 0 + case b => + } + } + (res, index) + } + @deprecated("use EscHelpers.strip", "1.4.2") + def stripMoves(s: String): String = { + val (bytes, len) = strip(s.getBytes, stripAnsi = true, stripColor = false) + new String(bytes, 0, len) + } + + /** + * Removes the ansi escape sequences from a string and makes a best attempt at + * calculating any ansi moves by hand. For example, if the string contains + * a backspace character followed by a character, the output string would + * replace the character preceding the backspaces with the character proceding it. + * This is in contrast to `strip` which just removes all ansi codes entirely. + * + * @param s the input string + * @return a string containing the original characters of the input stream with + * the ansi escape codes removed. + */ + def stripColorsAndMoves(s: String): String = { + val bytes = s.getBytes + val res = Array.fill[Byte](bytes.length)(0) + var i = 0 + var index = 0 + var state = 0 + var limit = 0 + val digit = new ArrayBuffer[Byte] + var leftDigit = -1 + bytes.foreach { + case 27 => state = esc + case b if (state == esc || state == csi) && b >= 48 && b < 58 => + state = csi + digit += b + case '[' if state == esc => state = csi + case 8 => + state = 0 + index = math.max(index - 1, 0) + case b if state == csi => + leftDigit = Try(new String(digit.toArray).toInt).getOrElse(0) + state = 0 + b.toChar match { + case 'h' => index = math.max(index - 1, 0) + case 'D' => index = math.max(index - leftDigit, 0) + case 'C' => index = math.min(limit, math.min(index + leftDigit, res.length - 1)) + case 'K' | 'J' => + if (leftDigit > 0) (0 until index).foreach(res(_) = 32) + else res(index) = 32 + case 'm' => + case ';' => state = csi + case b => state = csi + } + digit.clear() + case b if state == esc => state = 0 + case b => + res(index) = b + index += 1 + limit = math.max(limit, index) + } + (res, limit) + new String(res, 0, limit) + } + + /** Skips the escape sequence starting at `i-1`. `i` should be positioned at the character after the ESC that starts the sequence. */ + private[this] def skipESC(s: String, i: Int): Int = { + if (i >= s.length) { + i + } else if (isEscapeTerminator(s.charAt(i))) { + i + 1 + } else { + skipESC(s, i + 1) + } + } + +} diff --git a/internal/util-logging/src/main/scala/sbt/internal/util/FilterLogger.scala b/internal/util-logging/src/main/scala/sbt/internal/util/FilterLogger.scala new file mode 100644 index 000000000..57cad95a9 --- /dev/null +++ b/internal/util-logging/src/main/scala/sbt/internal/util/FilterLogger.scala @@ -0,0 +1,40 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util + +import sbt.util._ +import scala.annotation.nowarn + +/** + * A filter logger is used to delegate messages but not the logging level to another logger. This means + * that messages are logged at the higher of the two levels set by this logger and its delegate. + */ +class FilterLogger(delegate: AbstractLogger) extends BasicLogger { + @nowarn override lazy val ansiCodesSupported = delegate.ansiCodesSupported + def trace(t: => Throwable): Unit = { + if (traceEnabled) + delegate.trace(t) + } + override def setSuccessEnabled(flag: Boolean): Unit = delegate.setSuccessEnabled(flag) + override def successEnabled = delegate.successEnabled + override def setTrace(level: Int): Unit = delegate.setTrace(level) + override def getTrace = delegate.getTrace + def log(level: Level.Value, message: => String): Unit = { + if (atLevel(level)) + delegate.log(level, message) + } + def success(message: => String): Unit = { + if (successEnabled) + delegate.success(message) + } + def control(event: ControlEvent.Value, message: => String): Unit = { + if (atLevel(Level.Info)) + delegate.control(event, message) + } + def logAll(events: Seq[LogEvent]): Unit = delegate.logAll(events) +} diff --git a/internal/util-logging/src/main/scala/sbt/internal/util/FullLogger.scala b/internal/util-logging/src/main/scala/sbt/internal/util/FullLogger.scala new file mode 100644 index 000000000..3298d26a0 --- /dev/null +++ b/internal/util-logging/src/main/scala/sbt/internal/util/FullLogger.scala @@ -0,0 +1,39 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util + +import sbt.util._ +import com.github.ghik.silencer.silent + +/** Promotes the simple Logger interface to the full AbstractLogger interface. */ +class FullLogger(delegate: Logger) extends BasicLogger { + @deprecated("No longer used.", "1.0.0") + @silent override val ansiCodesSupported: Boolean = delegate.ansiCodesSupported + + def trace(t: => Throwable): Unit = { + if (traceEnabled) + delegate.trace(t) + } + def log(level: Level.Value, message: => String): Unit = { + if (atLevel(level)) + delegate.log(level, message) + } + def success(message: => String): Unit = + if (successEnabled) + delegate.success(message) + def control(event: ControlEvent.Value, message: => String): Unit = + info(message) + def logAll(events: Seq[LogEvent]): Unit = events.foreach(log) +} +object FullLogger { + def apply(delegate: Logger): AbstractLogger = + delegate match { + case d: AbstractLogger => d + case _ => new FullLogger(delegate) + } +} diff --git a/internal/util-logging/src/main/scala/sbt/internal/util/GlobalLogging.scala b/internal/util-logging/src/main/scala/sbt/internal/util/GlobalLogging.scala new file mode 100644 index 000000000..9be015da7 --- /dev/null +++ b/internal/util-logging/src/main/scala/sbt/internal/util/GlobalLogging.scala @@ -0,0 +1,90 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util + +import sbt.util._ +import java.io.{ File, PrintWriter } + +/** + * Provides the current global logging configuration. + * + * `full` is the current global logger. It should not be set directly because it is generated as needed from `backing.newLogger`. + * `console` is where all logging from all ConsoleLoggers should go. + * `backed` is the Logger that other loggers should feed into. + * `backing` tracks the files that persist the global logging. + * `newLogger` creates a new global logging configuration from a sink and backing configuration. + */ +final case class GlobalLogging( + full: ManagedLogger, + console: ConsoleOut, + backed: Appender, + backing: GlobalLogBacking, + newAppender: (ManagedLogger, PrintWriter, GlobalLogBacking, LoggerContext) => GlobalLogging +) + +final case class GlobalLogging1( + full: Logger, + console: ConsoleOut, + backed: AbstractLogger, + backing: GlobalLogBacking, + newLogger: (PrintWriter, GlobalLogBacking) => GlobalLogging1 +) + +/** + * Tracks the files that persist the global logging. + * `file` is the current backing file. `last` is the previous backing file, if there is one. + * `newBackingFile` creates a new temporary location for the next backing file. + */ +final case class GlobalLogBacking(file: File, last: Option[File], newBackingFile: () => File) { + + /** Shifts the current backing file to `last` and sets the current backing to `newFile`. */ + def shift(newFile: File) = GlobalLogBacking(newFile, Some(file), newBackingFile) + + /** Shifts the current backing file to `last` and sets the current backing to a new temporary file generated by `newBackingFile`. */ + def shiftNew() = shift(newBackingFile()) + + /** + * If there is a previous backing file in `last`, that becomes the current backing file and the previous backing is cleared. + * Otherwise, no changes are made. + */ + def unshift = GlobalLogBacking(last getOrElse file, None, newBackingFile) + +} + +object GlobalLogBacking { + def apply(newBackingFile: => File): GlobalLogBacking = + GlobalLogBacking(newBackingFile, None, newBackingFile _) +} + +object GlobalLogging { + import java.util.concurrent.atomic.AtomicInteger + + private def generateName: String = "GlobalLogging" + generateId.incrementAndGet + private val generateId: AtomicInteger = new AtomicInteger + + def initial1( + newLogger: (PrintWriter, GlobalLogBacking) => GlobalLogging1, + newBackingFile: => File, + console: ConsoleOut + ): GlobalLogging1 = { + val log = ConsoleLogger(console) + GlobalLogging1(log, console, log, GlobalLogBacking(newBackingFile), newLogger) + } + + def initial( + newAppender: (ManagedLogger, PrintWriter, GlobalLogBacking, LoggerContext) => GlobalLogging, + newBackingFile: => File, + console: ConsoleOut + ): GlobalLogging = { + val loggerName = generateName + val log = LoggerContext.globalContext.logger(loggerName, None, None) + val appender = ConsoleAppender(ConsoleAppender.generateName, console) + LoggerContext.globalContext.addAppender(loggerName, appender -> Level.Info) + GlobalLogging(log, console, appender, GlobalLogBacking(newBackingFile), newAppender) + } +} diff --git a/internal/util-logging/src/main/scala/sbt/internal/util/JLine3.scala b/internal/util-logging/src/main/scala/sbt/internal/util/JLine3.scala new file mode 100644 index 000000000..a4811a2cc --- /dev/null +++ b/internal/util-logging/src/main/scala/sbt/internal/util/JLine3.scala @@ -0,0 +1,326 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util + +import java.io.{ InputStream, OutputStream, PrintWriter } +import java.nio.ByteBuffer +import java.nio.charset.{ CharacterCodingException, Charset, CharsetDecoder } +import java.util.{ Arrays, EnumSet } +import java.util.concurrent.atomic.{ AtomicBoolean, AtomicReference } +import org.jline.utils.InfoCmp.Capability +import org.jline.utils.{ ClosedException, NonBlockingReader } +import org.jline.terminal.{ Attributes, Size, Terminal => JTerminal } +import org.jline.terminal.Attributes.{ InputFlag, LocalFlag } +import org.jline.terminal.Terminal.SignalHandler +import org.jline.terminal.impl.{ AbstractTerminal, DumbTerminal } +import org.jline.terminal.impl.jansi.JansiSupportImpl +import org.jline.terminal.impl.jansi.win.JansiWinSysTerminal +import org.jline.utils.OSUtils +import scala.collection.JavaConverters._ +import scala.util.Try +import java.util.concurrent.LinkedBlockingQueue + +private[sbt] object JLine3 { + private[util] val initialAttributes = new AtomicReference[Attributes] + + private[this] val forceWindowsJansiHolder = new AtomicBoolean(false) + private[sbt] def forceWindowsJansi(): Unit = forceWindowsJansiHolder.set(true) + private[this] def windowsJansi(): org.jline.terminal.Terminal = { + val support = new JansiSupportImpl + val winConsole = support.isWindowsConsole(); + val termType = sys.props.get("org.jline.terminal.type").orElse(sys.env.get("TERM")).orNull + val term = JansiWinSysTerminal.createTerminal( + "console", + termType, + OSUtils.IS_CONEMU, + Charset.forName("UTF-8"), + -1, + false, + SignalHandler.SIG_DFL, + true + ) + term.disableScrolling() + term + } + private val jansi = { + val (major, minor) = + (JansiSupportImpl.getJansiMajorVersion, JansiSupportImpl.getJansiMinorVersion) + (major > 1 || minor >= 18) && Util.isWindows + } + private[util] def system: org.jline.terminal.Terminal = { + val term = + if (forceWindowsJansiHolder.get) windowsJansi() + else { + // Only use jna on windows. Both jna and jansi use illegal reflective + // accesses on posix system. + org.jline.terminal.TerminalBuilder + .builder() + .system(System.console != null) + .jna(Util.isWindows && !jansi) + .jansi(jansi) + .paused(true) + .build() + } + initialAttributes.get match { + case null => initialAttributes.set(term.getAttributes) + case _ => + } + term + } + private[sbt] def apply(term: Terminal): JTerminal = { + if (System.getProperty("jline.terminal", "") == "none") + new DumbTerminal(term.inputStream, term.outputStream) + else wrapTerminal(term) + } + private[util] def decodeInput(decoder: CharsetDecoder, inputStream: InputStream): Int = { + val bytes = new Array[Byte](4) + var i = 0 + var res = -2 + do { + inputStream.read() match { + case -1 => res = -1 + case byte => + bytes(i) = byte.toByte + i += 1 + val bb = ByteBuffer.wrap(bytes, 0, i) + try { + val cb = decoder.decode(bb) + val it = cb.codePoints().iterator + if (it.hasNext) res = it.next + } catch { case _: CharacterCodingException => } + } + + } while (i < 4 && res == -2) + res + } + private[this] def wrapTerminal(term: Terminal): JTerminal = { + new AbstractTerminal( + term.name, + "nocapabilities", + Charset.forName("UTF-8"), + SignalHandler.SIG_DFL + ) { + val closed = new AtomicBoolean(false) + setOnClose { () => + doClose() + reader.close() + if (closed.compareAndSet(false, true)) { + // This is necessary to shutdown the non blocking input reader + // so that it doesn't keep blocking + term.inputStream match { + case w: Terminal.WriteableInputStream => w.cancel() + case _ => + } + } + } + override val input: InputStream = new InputStream { + override def read: Int = { + val res = term.inputStream match { + case w: Terminal.WriteableInputStream => + val result = new LinkedBlockingQueue[Integer] + try { + w.read(result) + result.poll match { + case null => throw new ClosedException + case i => i.toInt + } + } catch { + case _: InterruptedException => + w.cancel() + throw new ClosedException + } + case _ => throw new ClosedException + } + res match { + case 3 /* ctrl+c */ => throw new ClosedException + case r => r + } + } + } + override val output: OutputStream = new OutputStream { + override def write(b: Int): Unit = write(Array[Byte](b.toByte)) + override def write(b: Array[Byte]): Unit = if (!closed.get) term.withPrintStream { ps => + ps.write(b) + term.prompt match { + case a: Prompt.AskUser => a.write(b) + case _ => + } + } + override def write(b: Array[Byte], offset: Int, len: Int) = + write(Arrays.copyOfRange(b, offset, offset + len)) + override def flush(): Unit = term.withPrintStream(_.flush()) + } + + override val reader = new NonBlockingReader { + val buffer = new LinkedBlockingQueue[Integer] + val thread = new AtomicReference[Thread] + private def fillBuffer(): Unit = thread.synchronized { + thread.set(Thread.currentThread) + try buffer.put(decodeInput(encoding.newDecoder, term.inputStream)) + catch { case _: InterruptedException => buffer.put(-3) } + } + override def close(): Unit = thread.get match { + case null => + case t => t.interrupt() + } + override def read(timeout: Long, peek: Boolean) = { + if (buffer.isEmpty && !peek) fillBuffer() + (if (peek) buffer.peek else buffer.take) match { + case null => -2 + case i => if (i == -3) throw new InterruptedException else i + } + } + override def peek(timeout: Long): Int = buffer.peek() match { + case null => -1 + case i => i.toInt + } + override def readBuffered(buf: Array[Char]): Int = { + if (buffer.isEmpty) fillBuffer() + buffer.take match { + case i if i == -1 => -1 + case i => + buf(0) = i.toChar + 1 + } + } + } + override val writer: PrintWriter = new PrintWriter(output, true) + /* + * For now assume that the terminal capabilities for client and server + * are the same. + */ + override def getStringCapability(cap: Capability): String = { + term.getStringCapability(cap.toString) match { + case null if cap == Capability.key_dc && Util.isWindows => "\\E[3~" + case null if cap == Capability.key_end && Util.isWindows => "\\E[4~" + case null if cap == Capability.key_ic && Util.isWindows => "\\E[2~" + case c => c + } + } + override def getNumericCapability(cap: Capability): Integer = { + if (cap == Capability.max_colors && !term.isColorEnabled) 1 + else term.getNumericCapability(cap.toString) + } + override def getBooleanCapability(cap: Capability): Boolean = + term.getBooleanCapability(cap.toString) + def getAttributes(): Attributes = attributesFromMap(term.getAttributes) + def getSize(): Size = new Size(term.getWidth, term.getHeight) + def setAttributes(a: Attributes): Unit = {} // don't allow the jline line reader to change attributes + def setSize(size: Size): Unit = term.setSize(size.getColumns, size.getRows) + + override def enterRawMode(): Attributes = { + // don't actually modify the term, that is handled by LineReader + attributesFromMap(term.getAttributes) + } + } + } + private def enterRawModeImpl(term: JTerminal): Attributes = { + val prvAttr = term.getAttributes() + val newAttr = new Attributes(prvAttr) + newAttr.setLocalFlags(EnumSet.of(LocalFlag.ICANON, LocalFlag.ECHO, LocalFlag.IEXTEN), false) + newAttr.setInputFlags(EnumSet.of(InputFlag.IXON, InputFlag.ICRNL, InputFlag.INLCR), false) + term.setAttributes(newAttr) + prvAttr + } + // We need to set the ENABLE_PROCESS_INPUT flag for ctrl+c to be treated as a signal in windows + // https://docs.microsoft.com/en-us/windows/console/setconsolemode + private[this] val ENABLE_PROCESS_INPUT = 1 + private[util] def setEnableProcessInput(): Unit = if (Util.isWindows) { + WindowsSupport.setConsoleMode(WindowsSupport.getConsoleMode | ENABLE_PROCESS_INPUT) + } + private[util] def enterRawMode(term: JTerminal): Unit = { + val prevAttr = initialAttributes.get + val newAttr = new Attributes(prevAttr) + // These flags are copied from the jline3 enterRawMode but the jline implementation + // also puts the input stream in non blocking mode, which we do not want. + newAttr.setLocalFlags(EnumSet.of(LocalFlag.ICANON, LocalFlag.IEXTEN, LocalFlag.ECHO), false) + newAttr.setInputFlags(EnumSet.of(InputFlag.IXON, InputFlag.ICRNL, InputFlag.INLCR), false) + term.setAttributes(newAttr) + setEnableProcessInput() + } + private[util] def exitRawMode(term: JTerminal): Unit = { + val initAttr = initialAttributes.get + val newAttr = new Attributes(initAttr) + newAttr.setLocalFlags(EnumSet.of(LocalFlag.ICANON, LocalFlag.ECHO), true) + term.setAttributes(newAttr) + setEnableProcessInput() + } + private[util] def toMap(jattributes: Attributes): Map[String, String] = { + val result = new java.util.LinkedHashMap[String, String] + result.put( + "iflag", + jattributes.getInputFlags.iterator.asScala.map(_.name.toLowerCase).mkString(" ") + ) + result.put( + "oflag", + jattributes.getOutputFlags.iterator.asScala.map(_.name.toLowerCase).mkString(" ") + ) + result.put( + "cflag", + jattributes.getControlFlags.iterator.asScala.map(_.name.toLowerCase).mkString(" ") + ) + result.put( + "lflag", + jattributes.getLocalFlags.iterator.asScala.map(_.name.toLowerCase).mkString(" ") + ) + result.put( + "cchars", + jattributes.getControlChars.entrySet.iterator.asScala + .map { e => + s"${e.getKey.name.toLowerCase},${e.getValue}" + } + .mkString(" ") + ) + result.asScala.toMap + } + private[this] val iflagMap: Map[String, InputFlag] = + InputFlag.values.map(f => f.name.toLowerCase -> f).toMap + private[this] val oflagMap: Map[String, Attributes.OutputFlag] = + Attributes.OutputFlag.values.map(f => f.name.toLowerCase -> f).toMap + private[this] val cflagMap: Map[String, Attributes.ControlFlag] = + Attributes.ControlFlag.values.map(f => f.name.toLowerCase -> f).toMap + private[this] val lflagMap: Map[String, LocalFlag] = + LocalFlag.values.map(f => f.name.toLowerCase -> f).toMap + private[this] val charMap: Map[String, Attributes.ControlChar] = + Attributes.ControlChar.values().map(f => f.name.toLowerCase -> f).toMap + private[sbt] def setMode(term: Terminal, canonical: Boolean, echo: Boolean): Unit = { + val prev = attributesFromMap(term.getAttributes) + val newAttrs = new Attributes(prev) + newAttrs.setLocalFlag(LocalFlag.ICANON, canonical) + newAttrs.setLocalFlag(LocalFlag.ECHO, echo) + term.setAttributes(toMap(newAttrs)) + } + private[util] def attributesFromMap(map: Map[String, String]): Attributes = { + val attributes = new Attributes + map.get("iflag").foreach { flags => + flags.split(" ").foreach(f => iflagMap.get(f).foreach(attributes.setInputFlag(_, true))) + } + map.get("oflag").foreach { flags => + flags.split(" ").foreach(f => oflagMap.get(f).foreach(attributes.setOutputFlag(_, true))) + } + map.get("cflag").foreach { flags => + flags.split(" ").foreach(f => cflagMap.get(f).foreach(attributes.setControlFlag(_, true))) + } + map.get("lflag").foreach { flags => + flags.split(" ").foreach(f => lflagMap.get(f).foreach(attributes.setLocalFlag(_, true))) + } + map.get("cchars").foreach { chars => + chars.split(" ").foreach { keyValue => + keyValue.split(",") match { + case Array(k, v) => + Try(v.toInt).foreach(i => charMap.get(k).foreach(c => attributes.setControlChar(c, i))) + case _ => + } + } + } + attributes + } + private[sbt] def isEchoEnabled(map: Map[String, String]): Boolean = { + attributesFromMap(map).getLocalFlag(LocalFlag.ECHO) + } +} diff --git a/internal/util-logging/src/main/scala/sbt/internal/util/LoggerWriter.scala b/internal/util-logging/src/main/scala/sbt/internal/util/LoggerWriter.scala new file mode 100644 index 000000000..8c8fe34b7 --- /dev/null +++ b/internal/util-logging/src/main/scala/sbt/internal/util/LoggerWriter.scala @@ -0,0 +1,64 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util + +import sbt.util._ + +/** + * Provides a `java.io.Writer` interface to a `Logger`. Content is line-buffered and logged at `level`. + * A line is delimited by `nl`, which is by default the platform line separator. + */ +class LoggerWriter( + delegate: Logger, + unbufferedLevel: Option[Level.Value], + nl: String = System.getProperty("line.separator") +) extends java.io.Writer { + def this(delegate: Logger, level: Level.Value) = this(delegate, Some(level)) + def this(delegate: Logger) = this(delegate, None) + + private[this] val buffer = new StringBuilder + private[this] val lines = new collection.mutable.ListBuffer[String] + + override def close() = flush() + + override def flush(): Unit = + synchronized { + if (buffer.nonEmpty) { + log(buffer.toString) + buffer.clear() + } + } + + def flushLines(level: Level.Value): Unit = + synchronized { + for (line <- lines) + delegate.log(level, line) + lines.clear() + } + + override def write(content: Array[Char], offset: Int, length: Int): Unit = + synchronized { + buffer.appendAll(content, offset, length) + process() + } + + private[this] def process(): Unit = { + val i = buffer.indexOf(nl) + if (i >= 0) { + log(buffer.substring(0, i)) + buffer.delete(0, i + nl.length) + process() + } + } + + private[this] def log(s: String): Unit = unbufferedLevel match { + case None => + lines += s; () + case Some(level) => delegate.log(level, s) + } +} diff --git a/internal/util-logging/src/main/scala/sbt/internal/util/MainLogging.scala b/internal/util-logging/src/main/scala/sbt/internal/util/MainLogging.scala new file mode 100644 index 000000000..db5d2e25e --- /dev/null +++ b/internal/util-logging/src/main/scala/sbt/internal/util/MainLogging.scala @@ -0,0 +1,117 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util + +import sbt.util._ +import java.io.PrintWriter + +object MainAppender { + import java.util.concurrent.atomic.AtomicInteger + private def generateGlobalBackingName: String = + "GlobalBacking" + generateId.incrementAndGet + private val generateId: AtomicInteger = new AtomicInteger + + def multiLogger( + log: ManagedLogger, + config: MainAppenderConfig, + context: LoggerContext + ): ManagedLogger = { + import config._ + // TODO + // backed setTrace backingTrace + // multi: Logger + + context.clearAppenders(log.name) + consoleOpt match { + case Some(a: ConsoleAppender) => + a.setTrace(screenTrace) + context.addAppender(log.name, a -> screenLevel) + case _ => + } + context.addAppender(log.name, backed -> backingLevel) + extra.foreach(a => context.addAppender(log.name, a -> Level.Info)) + log + } + + def globalDefault( + console: ConsoleOut + ): (ManagedLogger, PrintWriter, GlobalLogBacking, LoggerContext) => GlobalLogging = { + lazy val newAppender + : (ManagedLogger, PrintWriter, GlobalLogBacking, LoggerContext) => GlobalLogging = + (log, writer, backing, lc) => { + val backed: Appender = defaultBacked(generateGlobalBackingName)(writer) + val full = multiLogger(log, defaultMultiConfig(Option(console), backed, Nil), lc) + GlobalLogging(full, console, backed, backing, newAppender) + } + newAppender + } + + def defaultMultiConfig( + consoleOpt: Option[ConsoleOut], + backing: Appender, + extra: List[Appender] + ): MainAppenderConfig = + MainAppenderConfig( + consoleOpt map { defaultScreen(_, ConsoleAppender.noSuppressedMessage) }, + backing, + extra, + Level.Info, + Level.Debug, + -1, + Int.MaxValue + ) + + def defaultScreen(console: ConsoleOut): Appender = + ConsoleAppender(ConsoleAppender.generateName, console) + + def defaultScreen( + console: ConsoleOut, + suppressedMessage: SuppressedTraceContext => Option[String] + ): Appender = { + ConsoleAppender( + ConsoleAppender.generateName, + Terminal.get, + suppressedMessage = suppressedMessage + ) + } + + def defaultScreen( + name: String, + console: ConsoleOut, + suppressedMessage: SuppressedTraceContext => Option[String] + ): Appender = + ConsoleAppender(name, console, suppressedMessage = suppressedMessage) + + def defaultBacked: PrintWriter => Appender = + defaultBacked(generateGlobalBackingName, Terminal.isAnsiSupported) + + def defaultBacked(loggerName: String): PrintWriter => Appender = + defaultBacked(loggerName, Terminal.isAnsiSupported) + + def defaultBacked(useFormat: Boolean): PrintWriter => Appender = + defaultBacked(generateGlobalBackingName, useFormat) + + def defaultBacked(loggerName: String, useFormat: Boolean): PrintWriter => Appender = + to => { + ConsoleAppender( + ConsoleAppender.generateName, + ConsoleOut.printWriterOut(to), + useFormat = useFormat + ) + } + + final case class MainAppenderConfig( + consoleOpt: Option[Appender], + backed: Appender, + extra: List[Appender], + screenLevel: Level.Value, + backingLevel: Level.Value, + screenTrace: Int, + backingTrace: Int + ) +} diff --git a/internal/util-logging/src/main/scala/sbt/internal/util/ManagedLogger.scala b/internal/util-logging/src/main/scala/sbt/internal/util/ManagedLogger.scala new file mode 100644 index 000000000..21e2bbfa6 --- /dev/null +++ b/internal/util-logging/src/main/scala/sbt/internal/util/ManagedLogger.scala @@ -0,0 +1,97 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util + +import sbt.internal.util.codec.JsonProtocol._ +import sbt.util._ +import scala.reflect.runtime.universe.TypeTag +import sjsonnew.JsonFormat + +private[sbt] trait MiniLogger { + def log[T](level: Level.Value, message: ObjectEvent[T]): Unit + def log(level: Level.Value, message: => String): Unit +} + +/** + * Delegates log events to the associated LogExchange. + */ +class ManagedLogger( + val name: String, + val channelName: Option[String], + val execId: Option[String], + xlogger: MiniLogger, + terminal: Option[Terminal], + private[sbt] val context: LoggerContext, +) extends Logger { + def this( + name: String, + channelName: Option[String], + execId: Option[String], + xlogger: MiniLogger + ) = + this(name, channelName, execId, xlogger, None, LoggerContext.globalContext) + override def trace(t: => Throwable): Unit = + logEvent(Level.Error, TraceEvent("Error", t, channelName, execId)) + override def log(level: Level.Value, message: => String): Unit = + xlogger.log(level, message) + + // send special event for success since it's not a real log level + override def success(message: => String): Unit = { + if (terminal.fold(true)(_.isSuccessEnabled)) { + infoEvent[SuccessEvent](SuccessEvent(message))( + implicitly[JsonFormat[SuccessEvent]], + StringTypeTag.fast[SuccessEvent], + ) + } + } + + @deprecated("Use macro-powered StringTypeTag.fast instead", "1.4.0") + def registerStringCodec[A]( + s: ShowLines[A], + tt: scala.reflect.runtime.universe.TypeTag[A] + ): Unit = { + LogExchange.registerStringCodec[A](s, tt) + } + def registerStringCodec[A: ShowLines: StringTypeTag]: Unit = { + LogExchange.registerStringCodec[A] + } + + @deprecated("Use macro-powered StringTypeTag.fast instead", "1.4.0") + final def debugEvent[A](event: => A, f: JsonFormat[A], t: TypeTag[A]): Unit = + debugEvent(event)(f, StringTypeTag.apply(t)) + @deprecated("Use macro-powered StringTypeTag.fast instead", "1.4.0") + final def infoEvent[A](event: => A, f: JsonFormat[A], t: TypeTag[A]): Unit = + infoEvent(event)(f, StringTypeTag.apply(t)) + @deprecated("Use macro-powered StringTypeTag.fast instead", "1.4.0") + final def warnEvent[A](event: => A, f: JsonFormat[A], t: TypeTag[A]): Unit = + warnEvent(event)(f, StringTypeTag.apply(t)) + @deprecated("Use macro-powered StringTypeTag.fast instead", "1.4.0") + final def errorEvent[A](event: => A, f: JsonFormat[A], t: TypeTag[A]): Unit = + errorEvent(event)(f, StringTypeTag.apply(t)) + + final def debugEvent[A: JsonFormat: StringTypeTag](event: => A): Unit = + logEvent(Level.Debug, event) + final def infoEvent[A: JsonFormat: StringTypeTag](event: => A): Unit = logEvent(Level.Info, event) + final def warnEvent[A: JsonFormat: StringTypeTag](event: => A): Unit = logEvent(Level.Warn, event) + final def errorEvent[A: JsonFormat: StringTypeTag](event: => A): Unit = + logEvent(Level.Error, event) + @deprecated("Use macro-powered StringTypeTag.fast instead", "1.4.0") + def logEvent[A](level: Level.Value, event: => A, f: JsonFormat[A], t: TypeTag[A]): Unit = + logEvent(level, event)(f, StringTypeTag.apply(t)) + def logEvent[A: JsonFormat](level: Level.Value, event: => A)( + implicit tag: StringTypeTag[A] + ): Unit = { + val v: A = event + // println("logEvent " + tag.key) + val entry: ObjectEvent[A] = ObjectEvent(level, v, channelName, execId, tag.key) + xlogger.log(level, entry) + } + + @deprecated("No longer used.", "1.0.0") + override def ansiCodesSupported = ConsoleAppender.formatEnabledInEnv +} diff --git a/internal/util-logging/src/main/scala/sbt/internal/util/MultiLogger.scala b/internal/util-logging/src/main/scala/sbt/internal/util/MultiLogger.scala new file mode 100644 index 000000000..3169b868d --- /dev/null +++ b/internal/util-logging/src/main/scala/sbt/internal/util/MultiLogger.scala @@ -0,0 +1,48 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util + +import sbt.util._ +import scala.annotation.nowarn + +// note that setting the logging level on this logger has no effect on its behavior, only +// on the behavior of the delegates. +class MultiLogger(delegates: List[AbstractLogger]) extends BasicLogger { + @deprecated("No longer used.", "1.0.0") + override lazy val ansiCodesSupported = delegates exists supported + @nowarn private[this] def supported = (_: AbstractLogger).ansiCodesSupported + + override def setLevel(newLevel: Level.Value): Unit = { + super.setLevel(newLevel) + dispatch(new SetLevel(newLevel)) + } + + override def setTrace(level: Int): Unit = { + super.setTrace(level) + dispatch(new SetTrace(level)) + } + + override def setSuccessEnabled(flag: Boolean): Unit = { + super.setSuccessEnabled(flag) + dispatch(new SetSuccess(flag)) + } + + def trace(t: => Throwable): Unit = dispatch(new Trace(t)) + def log(level: Level.Value, message: => String): Unit = dispatch(new Log(level, message)) + def success(message: => String): Unit = dispatch(new Success(message)) + def logAll(events: Seq[LogEvent]): Unit = delegates.foreach(_.logAll(events)) + + def control(event: ControlEvent.Value, message: => String): Unit = + delegates.foreach(_.control(event, message)) + + private[this] def dispatch(event: LogEvent): Unit = { + for (d <- delegates) { + d.log(event) + } + } +} diff --git a/internal/util-logging/src/main/scala/sbt/internal/util/ObjectEvent.scala b/internal/util-logging/src/main/scala/sbt/internal/util/ObjectEvent.scala new file mode 100644 index 000000000..e73b76183 --- /dev/null +++ b/internal/util-logging/src/main/scala/sbt/internal/util/ObjectEvent.scala @@ -0,0 +1,45 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt +package internal +package util + +import sbt.util.Level +import sjsonnew.JsonFormat +import sjsonnew.support.scalajson.unsafe.Converter +import sjsonnew.shaded.scalajson.ast.unsafe.JValue + +final class ObjectEvent[A]( + val level: Level.Value, + val message: A, + val channelName: Option[String], + val execId: Option[String], + val contentType: String, + val json: JValue +) extends Serializable { + override def toString: String = + s"ObjectEvent($level, $message, $channelName, $execId, $contentType, $json)" +} + +object ObjectEvent { + def apply[A: JsonFormat]( + level: Level.Value, + message: A, + channelName: Option[String], + execId: Option[String], + contentType: String + ): ObjectEvent[A] = + new ObjectEvent( + level, + message, + channelName, + execId, + contentType, + Converter.toJsonUnsafe(message) + ) +} diff --git a/internal/util-logging/src/main/scala/sbt/internal/util/ProgressState.scala b/internal/util-logging/src/main/scala/sbt/internal/util/ProgressState.scala new file mode 100644 index 000000000..37ba3ccab --- /dev/null +++ b/internal/util-logging/src/main/scala/sbt/internal/util/ProgressState.scala @@ -0,0 +1,225 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util + +import java.io.PrintStream +import java.util.concurrent.ArrayBlockingQueue +import java.util.concurrent.atomic.{ AtomicInteger, AtomicReference } + +import sbt.internal.util.ConsoleAppender.{ + ClearScreenAfterCursor, + CursorLeft1000, + DeleteLine, + cursorUp, + setShowProgress, +} + +import scala.collection.mutable.ArrayBuffer +import scala.collection.JavaConverters._ + +private[sbt] final class ProgressState( + val progressLines: AtomicReference[Seq[String]], + val padding: AtomicInteger, + val blankZone: Int, + val currentLineBytes: AtomicReference[ArrayBuffer[Byte]], + val maxItems: Int, +) { + def this(blankZone: Int, maxItems: Int) = this( + new AtomicReference(Nil), + new AtomicInteger(0), + blankZone, + new AtomicReference(new ArrayBuffer[Byte]), + maxItems, + ) + def this(blankZone: Int) = this(blankZone, 8) + def currentLine: Option[String] = + new String(currentLineBytes.get.toArray, "UTF-8").linesIterator.toSeq.lastOption + .map(EscHelpers.stripColorsAndMoves) + .filter(_.nonEmpty) + def reset(): Unit = { + progressLines.set(Nil) + padding.set(0) + currentLineBytes.set(new ArrayBuffer[Byte]) + } + private[this] val lineBuffer = new ArrayBlockingQueue[String](300) + private[util] def getLines: Seq[String] = lineBuffer.asScala.toVector + private[this] def appendLine(line: String) = while (!lineBuffer.offer(line)) { lineBuffer.poll } + private[util] def clearBytes(): Unit = { + val pad = padding.get + if (currentLineBytes.get.isEmpty && pad > 0) padding.decrementAndGet() + currentLineBytes.set(new ArrayBuffer[Byte]) + } + + private[this] val lineSeparatorBytes: Array[Byte] = System.lineSeparator.getBytes("UTF-8") + private[util] def addBytes(terminal: Terminal, bytes: Seq[Byte]): Unit = { + val previous: ArrayBuffer[Byte] = currentLineBytes.get + val padding = this.padding.get + val prevLineCount = if (padding > 0) terminal.lineCount(new String(previous.toArray)) else 0 + previous ++= bytes + if (padding > 0) { + val newLineCount = terminal.lineCount(new String(previous.toArray)) + val diff = newLineCount - prevLineCount + this.padding.set(math.max(padding - diff, 0)) + } + val lines = new String(previous.toArray, "UTF-8") + if (lines.contains(System.lineSeparator)) { + currentLineBytes.set(new ArrayBuffer[Byte]) + if (!lines.endsWith(System.lineSeparator)) { + val allLines = lines.split(System.lineSeparator) + allLines.dropRight(1).foreach(appendLine) + allLines.lastOption.foreach(currentLineBytes.get ++= _.getBytes("UTF-8")) + } else if (lines.contains(System.lineSeparator)) { + lines.split(System.lineSeparator).foreach(appendLine) + } + } + } + + private[util] def getPrompt(terminal: Terminal): Array[Byte] = { + if (terminal.prompt.isInstanceOf[Prompt.AskUser]) { + val prefix = if (terminal.isAnsiSupported) s"$DeleteLine$CursorLeft1000" else "" + prefix.getBytes ++ terminal.prompt.render().getBytes("UTF-8") + } else Array.empty + } + private[this] val cleanPrompt = + (DeleteLine + ClearScreenAfterCursor + CursorLeft1000).getBytes("UTF-8") + private[this] val clearScreenBytes = ClearScreenAfterCursor.getBytes("UTF-8") + private[util] def write( + terminal: Terminal, + bytes: Array[Byte], + printStream: PrintStream, + hasProgress: Boolean + ): Unit = { + if (hasProgress) { + val canClearPrompt = currentLineBytes.get.isEmpty + addBytes(terminal, bytes) + val toWrite = new ArrayBuffer[Byte] + terminal.prompt match { + case a: Prompt.AskUser if a.render.nonEmpty && canClearPrompt => toWrite ++= cleanPrompt + case _ => + } + val endsWithNewLine = bytes.endsWith(lineSeparatorBytes) + if (endsWithNewLine || bytes.containsSlice(lineSeparatorBytes)) { + val parts = new String(bytes, "UTF-8").split(System.lineSeparator) + def appendLine(l: String, appendNewline: Boolean): Unit = { + toWrite ++= l.getBytes("UTF-8") + toWrite ++= clearScreenBytes + if (appendNewline) toWrite ++= lineSeparatorBytes + } + parts.dropRight(1).foreach(appendLine(_, true)) + parts.lastOption match { + case Some(l) => appendLine(l, bytes.endsWith(lineSeparatorBytes)) + case None => toWrite ++= lineSeparatorBytes + } + } else toWrite ++= bytes + toWrite ++= clearScreenBytes + if (endsWithNewLine) { + if (progressLines.get.nonEmpty) { + val lastLine = terminal.prompt match { + case a: Prompt.AskUser => a.render() + case _ => currentLine.getOrElse("") + } + val lines = printProgress(terminal, lastLine) + toWrite ++= lines.getBytes("UTF-8") + } + toWrite ++= getPrompt(terminal) + } + printStream.write(toWrite.toArray) + printStream.flush() + } else printStream.write(bytes) + } + + private[util] def printProgress(terminal: Terminal, lastLine: String): String = { + val previousLines = progressLines.get + if (previousLines.nonEmpty) { + val currentLength = previousLines.foldLeft(0)(_ + terminal.lineCount(_)) + val (height, width) = terminal.getLineHeightAndWidth(lastLine) + val offset = width > 0 + val pad = math.max(padding.get - height, 0) + val start = (if (offset) s"\n$CursorLeft1000" else "") + val totalSize = currentLength + blankZone + pad + val blank = CursorLeft1000 + s"\n$DeleteLine" * (totalSize - currentLength) + val lines = previousLines.mkString(DeleteLine, s"\n$DeleteLine", s"\n$DeleteLine") + val resetCursorUp = cursorUp(totalSize + (if (offset) 1 else 0)) + val resetCursor = resetCursorUp + CursorLeft1000 + lastLine + start + blank + lines + resetCursor + } else { + ClearScreenAfterCursor + } + } +} + +private[sbt] object ProgressState { + private val SERVER_IS_RUNNING = "sbt server is running " + // the + 2 is for the quotation marks + private val SERVER_IS_RUNNING_LENGTH = SERVER_IS_RUNNING.length + 3 + + /** + * Receives a new task report and replaces the old one. In the event that the new + * report has fewer lines than the previous report, padding lines are added on top + * so that the console log lines remain contiguous. When a console line is printed + * at the info or greater level, we can decrement the padding because the console + * line will have filled in the blank line. + */ + private[sbt] def updateProgressState( + pe: ProgressEvent, + terminal: Terminal + ): Unit = { + val state = terminal.progressState + val isAskUser = terminal.prompt.isInstanceOf[Prompt.AskUser] + val isRunning = terminal.prompt == Prompt.Running + val isBatch = terminal.prompt == Prompt.Batch + val isWatch = terminal.prompt == Prompt.Watch + if (terminal.isSupershellEnabled) { + setShowProgress(true) // used by Zinc to not show "done compiling" + if (!pe.skipIfActive.getOrElse(false) || (!isRunning && !isBatch)) { + terminal.withPrintStream { ps => + val commandFromThisTerminal = pe.channelName.fold(true)(_ == terminal.name) + val info = if (commandFromThisTerminal) { + val base = pe.items.map { item => + val elapsed = item.elapsedMicros / 1000000L + s" | => ${item.name} ${elapsed}s" + } + val limit = state.maxItems + if (base.size > limit + 1) + s" | ... (${base.size - limit} other tasks)" +: base.takeRight(limit) + else base + } else { + pe.command.toSeq.flatMap { cmd => + val width = terminal.getWidth + val sanitized = if ((cmd.length + SERVER_IS_RUNNING_LENGTH) > width) { + cmd.take(width - 3 - SERVER_IS_RUNNING_LENGTH) + "..." + } else cmd + val tail = if (isWatch) Nil else "enter 'cancel' to stop evaluation" :: Nil + s"$SERVER_IS_RUNNING '$sanitized'" :: tail + } + } + + val currentLength = info.foldLeft(0)(_ + terminal.lineCount(_)) + val previousLines = state.progressLines.getAndSet(info) + val prevLength = previousLines.foldLeft(0)(_ + terminal.lineCount(_)) + val prevSize = prevLength + state.padding.get + + val lastLine = + if (isAskUser) terminal.prompt.render() else terminal.getLastLine.getOrElse("") + state.padding.set(math.max(0, prevSize - currentLength)) + val toWrite = + state.getPrompt(terminal) ++ state.printProgress(terminal, lastLine).getBytes("UTF-8") + ps.write(toWrite) + ps.flush() + } + } else if (state.progressLines.get.nonEmpty) { + state.progressLines.set(Nil) + terminal.withPrintStream { ps => + val lastLine = terminal.getLastLine.getOrElse("") + ps.print(lastLine + ClearScreenAfterCursor) + ps.flush() + } + } + } + } +} diff --git a/internal/util-logging/src/main/scala/sbt/internal/util/Prompt.scala b/internal/util-logging/src/main/scala/sbt/internal/util/Prompt.scala new file mode 100644 index 000000000..89c1872cb --- /dev/null +++ b/internal/util-logging/src/main/scala/sbt/internal/util/Prompt.scala @@ -0,0 +1,39 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util + +import java.util.concurrent.LinkedBlockingQueue +import scala.collection.JavaConverters._ + +private[sbt] sealed trait Prompt { + def mkPrompt: () => String + def render(): String + def reset(): Unit +} + +private[sbt] object Prompt { + private[sbt] case class AskUser(override val mkPrompt: () => String) extends Prompt { + private[this] val bytes = new LinkedBlockingQueue[Byte] + def write(b: Array[Byte]): Unit = b.foreach(bytes.put) + override def render(): String = { + val res = new String(bytes.asScala.toArray, "UTF-8") + if (res.endsWith(System.lineSeparator)) "" else res + } + override def reset(): Unit = bytes.clear() + } + private[sbt] trait NoPrompt extends Prompt { + override val mkPrompt: () => String = () => "" + override def render(): String = "" + override def reset(): Unit = {} + } + private[sbt] case object Running extends NoPrompt + private[sbt] case object Batch extends NoPrompt + private[sbt] case object Watch extends NoPrompt + private[sbt] case object Pending extends NoPrompt + private[sbt] case object NoPrompt extends NoPrompt +} diff --git a/internal/util-logging/src/main/scala/sbt/internal/util/StackTrace.scala b/internal/util-logging/src/main/scala/sbt/internal/util/StackTrace.scala new file mode 100644 index 000000000..f98c2d9ce --- /dev/null +++ b/internal/util-logging/src/main/scala/sbt/internal/util/StackTrace.scala @@ -0,0 +1,85 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util + +import sbt.io.IO +import scala.collection.mutable.ListBuffer + +object StackTrace { + def isSbtClass(name: String) = name.startsWith("sbt.") || name.startsWith("xsbt.") + + /** + * Return a printable representation of the stack trace associated + * with t. Information about t and its Throwable causes is included. + * The number of lines to be included for each Throwable is configured + * via d which should be greater than or equal to 0. + * + * - If d is 0, then all elements are included up to (but not including) + * the first element that comes from sbt. + * - If d is greater than 0, then up to that many lines are included, + * where the line for the Throwable is counted plus one line for each stack element. + * Less lines will be included if there are not enough stack elements. + * + * See also ConsoleAppender where d <= 2 is treated specially by + * printing a prepared statement. + */ + def trimmedLines(t: Throwable, d: Int): List[String] = { + require(d >= 0) + val b = new ListBuffer[String]() + + def appendStackTrace(t: Throwable, first: Boolean): Unit = { + + val include: StackTraceElement => Boolean = + if (d == 0) + element => !isSbtClass(element.getClassName) + else { + var count = d - 1 + (_ => { count -= 1; count >= 0 }) + } + + def appendElement(e: StackTraceElement): Unit = { + b.append("\tat " + e) + () + } + + if (!first) b.append("Caused by: " + t.toString) + else b.append(t.toString) + + val els = t.getStackTrace() + var i = 0 + while ((i < els.size) && include(els(i))) { + appendElement(els(i)) + i += 1 + } + + } + + appendStackTrace(t, true) + var c = t + while (c.getCause() != null) { + c = c.getCause() + appendStackTrace(c, false) + } + b.toList + } + + /** + * Return a printable representation of the stack trace associated + * with t. Information about t and its Throwable causes is included. + * The number of lines to be included for each Throwable is configured + * via d which should be greater than or equal to 0. + * + * - If d is 0, then all elements are included up to (but not including) + * the first element that comes from sbt. + * - If d is greater than 0, then up to that many lines are included, + * where the line for the Throwable is counted plus one line for each stack element. + * Less lines will be included if there are not enough stack elements. + */ + def trimmed(t: Throwable, d: Int): String = + trimmedLines(t, d).mkString(IO.Newline) +} diff --git a/internal/util-logging/src/main/scala/sbt/internal/util/StringTypeTag.scala b/internal/util-logging/src/main/scala/sbt/internal/util/StringTypeTag.scala new file mode 100644 index 000000000..e2b54232a --- /dev/null +++ b/internal/util-logging/src/main/scala/sbt/internal/util/StringTypeTag.scala @@ -0,0 +1,57 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util + +import scala.language.experimental.macros +import scala.reflect.runtime.universe._ + +/** This is used to carry type information in JSON. */ +final case class StringTypeTag[A](key: String) { + override def toString: String = key +} + +object StringTypeTag { + + /** Generates a StringTypeTag for any type at compile time. */ + implicit def fast[A]: StringTypeTag[A] = macro appmacro.StringTypeTag.impl[A] + @deprecated("Prefer macro generated StringTypeTag", "1.4.0") + def apply[A: TypeTag]: StringTypeTag[A] = + synchronized { + def doApply: StringTypeTag[A] = { + val tag = implicitly[TypeTag[A]] + val tpe = tag.tpe + val k = typeToString(tpe) + // println(tpe.getClass.toString + " " + k) + StringTypeTag[A](k) + } + def retry(n: Int): StringTypeTag[A] = + try { + doApply + } catch { + case e: NullPointerException => + if (n < 1) throw new RuntimeException("NPE in StringTypeTag", e) + else { + Thread.sleep(1) + retry(n - 1) + } + } + retry(3) + } + + @deprecated("Prefer macro generated StringTypeTag", "1.4.0") + def typeToString(tpe: Type): String = + tpe match { + case TypeRef(_, sym, args) => + if (args.nonEmpty) { + val typeCon = tpe.typeSymbol.fullName + val typeArgs = args map typeToString + s"""$typeCon[${typeArgs.mkString(",")}]""" + } else tpe.toString + case _ => tpe.toString + } +} diff --git a/internal/util-logging/src/main/scala/sbt/internal/util/Terminal.scala b/internal/util-logging/src/main/scala/sbt/internal/util/Terminal.scala new file mode 100644 index 000000000..dac378cdf --- /dev/null +++ b/internal/util-logging/src/main/scala/sbt/internal/util/Terminal.scala @@ -0,0 +1,1022 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util + +import java.io.{ InputStream, InterruptedIOException, IOException, OutputStream, PrintStream } +import java.nio.channels.ClosedChannelException +import java.util.{ Arrays, Locale } +import java.util.concurrent.atomic.{ AtomicBoolean, AtomicReference } +import java.util.concurrent.{ Executors, LinkedBlockingQueue, TimeUnit } + +import jline.console.ConsoleReader +import scala.annotation.tailrec +import scala.concurrent.duration._ +import scala.util.Try +import scala.util.control.NonFatal + +trait Terminal extends AutoCloseable { + + /** + * Gets the current width of the terminal. The implementation reads a property from the jline + * config which is updated if it has been more than a second since the last update. It is thus + * possible for this value to be stale. + * + * @return the terminal width. + */ + def getWidth: Int + + /** + * Gets the current height of the terminal. The implementation reads a property from the jline + * config which is updated if it has been more than a second since the last update. It is thus + * possible for this value to be stale. + * + * @return the terminal height. + */ + def getHeight: Int + + /** + * Returns the height and width of the current line that is displayed on the terminal. If the + * most recently flushed byte is a newline, this will be `(0, 0)`. + * + * @return the (height, width) pair + */ + def getLineHeightAndWidth(line: String): (Int, Int) + + /** + * Gets the input stream for this Terminal. This could be a wrapper around System.in for the + * process or it could be a remote input stream for a network channel. + * @return the input stream. + */ + def inputStream: InputStream + + /** + * Gets the output stream for this Terminal. + * @return the output stream. + */ + def outputStream: OutputStream + + /** + * Gets the error stream for this Terminal. + * @return the error stream. + */ + def errorStream: OutputStream + + /** + * Returns true if the terminal supports ansi characters. + * + * @return true if the terminal supports ansi escape codes. + */ + def isAnsiSupported: Boolean + + /** + * Returns true if color is enabled for this terminal. + * + * @return true if color is enabled for this terminal. + */ + def isColorEnabled: Boolean + + /** + * Returns true if the terminal has echo enabled. + * + * @return true if the terminal has echo enabled. + */ + def isEchoEnabled: Boolean + + /** + * Returns true if the terminal has success enabled, which it may not if it is for batch + * commands because the client will print the success results when received from the + * server. + * + * @return true if the terminal has success enabled + */ + def isSuccessEnabled: Boolean + + /** + * Returns true if the terminal has supershell enabled. + * + * @return true if the terminal has supershell enabled. + */ + def isSupershellEnabled: Boolean + + /** + * Toggles whether or not the terminal should echo characters back to stdout + * + * @return the previous value of the toggle + */ + def setEchoEnabled(toggle: Boolean): Unit + + /* + * The methods below this comment are implementation details that are in + * some cases specific to jline2. These methods may need to change or be + * removed if/when sbt upgrades to jline 3. + */ + + /** + * Returns the last line written to the terminal's output stream. + * @return the last line + */ + private[sbt] def getLastLine: Option[String] + + /** + * Returns the buffered lines that have been written to the terminal. The + * main use case is to display the system startup log lines when a client + * connects to a booting server. This could also be used to implement a more + * tmux like experience where multiple clients connect to the same console. + * + * @return the lines + */ + private[sbt] def getLines: Seq[String] + + private[sbt] def getBooleanCapability(capability: String): Boolean + private[sbt] def getNumericCapability(capability: String): Integer + private[sbt] def getStringCapability(capability: String): String + private[sbt] def getAttributes: Map[String, String] + private[sbt] def setAttributes(attributes: Map[String, String]): Unit + private[sbt] def setSize(width: Int, height: Int): Unit + + private[sbt] def name: String + private[sbt] final def withRawInput[T](f: => T): T = { + enterRawMode() + try f + catch { case e: InterruptedIOException => throw new InterruptedException } finally exitRawMode() + } + private[sbt] def enterRawMode(): Unit + private[sbt] def exitRawMode(): Unit + private[sbt] def write(bytes: Int*): Unit + private[sbt] def printStream: PrintStream + private[sbt] def withPrintStream[T](f: PrintStream => T): T + private[sbt] def withRawOutput[R](f: => R): R + private[sbt] def restore(): Unit = {} + private[sbt] def progressState: ProgressState + private[this] val promptHolder: AtomicReference[Prompt] = new AtomicReference(Prompt.Batch) + private[sbt] final def prompt: Prompt = promptHolder.get + private[sbt] final def setPrompt(newPrompt: Prompt): Unit = + if (prompt != Prompt.NoPrompt) promptHolder.set(newPrompt) + + /** + * Returns the number of lines that the input string will cover given the current width of the + * terminal. + * + * @param line the input line + * @return the number of lines that the line will cover on the terminal + */ + private[sbt] def lineCount(line: String): Int = { + val lines = EscHelpers.stripColorsAndMoves(line).split('\n') + val width = getWidth + def count(l: String): Int = { + val len = l.length + if (width > 0 && len > 0) (len - 1 + width) / width else 0 + } + if (lines.nonEmpty) lines.tail.foldLeft(lines.headOption.fold(0)(count))(_ + count(_)) + else 0 + } + private[sbt] def flush(): Unit = printStream.flush() +} + +object Terminal { + val NO_BOOT_CLIENTS_CONNECTED: Int = -2 + // Disable noisy jline log spam + if (System.getProperty("sbt.jline.verbose", "false") != "true") + jline.internal.Log.setOutput(new PrintStream(_ => {}, false)) + private[this] val isCI = System.getProperty("sbt.ci", "") == "true" || + sys.env.contains("BUILD_NUMBER") || sys.env.contains("CI") + def consoleLog(string: String): Unit = { + try Terminal.console.printStream.println(s"[info] $string") + catch { case _: IOException => } + } + private[sbt] def set(terminal: Terminal): Terminal = activeTerminal.getAndSet(terminal) + implicit class TerminalOps(private val term: Terminal) extends AnyVal { + def ansi(richString: => String, string: => String): String = + if (term.isAnsiSupported) richString else string + /* + * Whenever we are dealing with JLine, which is true in sbt's ConsoleReader + * as well as in the scala `console` task, we need to provide a jline.Terminal2 + * instance that can be consumed by the ConsoleReader. The ConsoleTerminal + * already wraps a jline terminal, so we can just return the wrapped jline + * terminal. + */ + private[sbt] def toJLine: jline.Terminal with jline.Terminal2 = term match { + case _ => + new jline.Terminal with jline.Terminal2 { + override def init(): Unit = {} + override def restore(): Unit = {} + override def reset(): Unit = {} + override def isSupported: Boolean = true + override def getWidth: Int = term.getWidth + override def getHeight: Int = term.getHeight + override def isAnsiSupported: Boolean = term.isAnsiSupported + override def wrapOutIfNeeded(out: OutputStream): OutputStream = out + override def wrapInIfNeeded(in: InputStream): InputStream = in + override def hasWeirdWrap: Boolean = false + override def isEchoEnabled: Boolean = term.isEchoEnabled + override def setEchoEnabled(enabled: Boolean): Unit = {} + override def disableInterruptCharacter(): Unit = {} + override def enableInterruptCharacter(): Unit = {} + override def getOutputEncoding: String = null + override def getBooleanCapability(capability: String): Boolean = + term.getBooleanCapability(capability) + override def getNumericCapability(capability: String): Integer = + term.getNumericCapability(capability) + override def getStringCapability(capability: String): String = + term.getStringCapability(capability) + } + } + } + + /* + * Closes the standard input and output streams for the process. This allows + * the sbt client to detach from the server it launches. + */ + def close(): Unit = { + if (System.console == null) { + originalOut.close() + originalIn.close() + originalErr.close() + } + } + + /** + * Returns true if System.in is attached. When sbt is run as a subprocess, like in scripted or + * as a server, System.in will not be attached and this method will return false. Otherwise + * it will return true. + * + * @return true if System.in is attached. + */ + def systemInIsAttached: Boolean = attached.get + + def read: Int = inputStream.get match { + case null => -1 + case is => is.read + } + + /** + * Returns an InputStream that will throw a [[ClosedChannelException]] if read returns -1. + * @return the wrapped InputStream. + */ + private[sbt] def throwOnClosedSystemIn(in: InputStream): InputStream = new InputStream { + override def available(): Int = in.available() + override def read(): Int = in.read() match { + case -1 => throw new ClosedChannelException + case r if r >= 0 => r + case _ => -1 + } + } + + /** + * Provides a wrapper around System.in. The wrapped stream in will check if the terminal is attached + * in available and read. If a read returns -1, it will mark System.in as unattached so that + * it can be detected by [[systemInIsAttached]]. + * + * @return the wrapped InputStream + */ + private[sbt] def wrappedSystemIn: InputStream = WrappedSystemIn + + /** + * Restore the terminal to its initial state. + */ + private[sbt] def restore(): Unit = console.toJLine.restore() + + private[this] val hasProgress: AtomicBoolean = new AtomicBoolean(false) + + private[sbt] def parseLogOption(s: String): Option[Boolean] = + s.toLowerCase match { + case "always" => Some(true) + case "auto" => None + case "never" => Some(false) + case "true" => Some(true) + case "false" => Some(false) + case _ => None + } + + /** + * Indicates whether formatting has been disabled in environment variables. + * 1. -Dsbt.log.noformat=true means no formatting. + * 2. -Dsbt.color=always/auto/never/true/false + * 3. -Dsbt.colour=always/auto/never/true/false + * 4. -Dsbt.log.format=always/auto/never/true/false + */ + private[this] lazy val logFormatEnabled: Option[Boolean] = { + sys.props.get("sbt.log.noformat") match { + case Some(_) => Some(!java.lang.Boolean.getBoolean("sbt.log.noformat")) + case _ => sys.props.get("sbt.log.format").flatMap(parseLogOption) + } + } + private[sbt] lazy val isAnsiSupported: Boolean = logFormatEnabled.getOrElse(useColorDefault) + + private[this] val isDumb = "dumb" == System.getenv("TERM") + private[this] def isDumbTerminal = isDumb || System.getProperty("jline.terminal", "") == "none" + private[this] val hasConsole = Option(java.lang.System.console).isDefined + private[this] def useColorDefault: Boolean = { + // This approximates that both stdin and stdio are connected, + // so by default color will be turned off for pipes and redirects. + props + .map(_.color) + .orElse(isColorEnabledProp) + .getOrElse( + logFormatEnabled + .getOrElse(true) && ((hasConsole && !isDumbTerminal) || isCI || Util.isEmacs) + ) + } + private[this] lazy val isColorEnabledProp: Option[Boolean] = + sys.props.get("sbt.color").orElse(sys.props.get("sbt.colour")).flatMap(parseLogOption) + private[sbt] lazy val isColorEnabled = useColorDefault + + private[sbt] def red(str: String, doRed: Boolean): String = + if (isColorEnabled && doRed) Console.RED + str + Console.RESET + else str + + private[this] def hasVirtualIO = System.getProperty("sbt.io.virtual", "") == "true" || !isCI + private[sbt] def canPollSystemIn: Boolean = hasConsole && !isDumbTerminal && hasVirtualIO + + /** + * + * @param isServer toggles whether or not this is a server of client process + * @param f the thunk to run + * @tparam T the result type of the thunk + * @return the result of the thunk + */ + private[sbt] def withStreams[T](isServer: Boolean, isSubProcess: Boolean)(f: => T): T = { + // In ci environments, don't touch the io streams unless run with -Dsbt.io.virtual=true + if ((hasConsole && !isDumbTerminal) || isSubProcess) + consoleTerminalHolder.set(newConsoleTerminal()) + if (hasVirtualIO) { + hasProgress.set(isServer && isAnsiSupported) + activeTerminal.set(consoleTerminalHolder.get) + try withOut(withIn(f)) + finally { + jline.TerminalFactory.reset() + if (isServer) { + console match { + case c: ConsoleTerminal if !isWindows => + /* + * Entering raw mode in this way causes the standard in InputStream + * to become non-blocking. After we set it to non-blocking, we spin + * up a thread that reads from the inputstream and the resets it + * back to blocking mode. We can then close the console. We do + * this on a background thread in case the read blocks indefinitely. + */ + c.system.enterRawMode() + val runnable: Runnable = () => { + try Util.ignoreResult(c.inputStream.read) + catch { case _: InterruptedException => } + } + val thread = new Thread(runnable, "sbt-console-background-close") + thread.setDaemon(true) + thread.start() + // The thread should exit almost instantly but give it 200ms to spin up + thread.join(200) + if (thread.isAlive) thread.interrupt() + c.close() + case c => c.close() + } + } else { + console.close() + } + } + } else f + } + + private[this] object ProxyTerminal extends Terminal { + private def t: Terminal = activeTerminal.get + override private[sbt] def progressState: ProgressState = t.progressState + override private[sbt] def enterRawMode(): Unit = t.enterRawMode() + override private[sbt] def exitRawMode(): Unit = t.exitRawMode() + override def getWidth: Int = t.getWidth + override def getHeight: Int = t.getHeight + override def getLineHeightAndWidth(line: String): (Int, Int) = t.getLineHeightAndWidth(line) + override def lineCount(line: String): Int = t.lineCount(line) + override def inputStream: InputStream = t.inputStream + override def outputStream: OutputStream = t.outputStream + override def errorStream: OutputStream = t.errorStream + override def isAnsiSupported: Boolean = t.isAnsiSupported + override def isColorEnabled: Boolean = t.isColorEnabled + override def isEchoEnabled: Boolean = t.isEchoEnabled + override def isSuccessEnabled: Boolean = t.isSuccessEnabled + override def isSupershellEnabled: Boolean = t.isSupershellEnabled + override def setEchoEnabled(toggle: Boolean): Unit = t.setEchoEnabled(toggle) + override def getBooleanCapability(capability: String): Boolean = + t.getBooleanCapability(capability) + override def getNumericCapability(capability: String): Integer = + t.getNumericCapability(capability) + override def getStringCapability(capability: String): String = + t.getStringCapability(capability) + override private[sbt] def getAttributes: Map[String, String] = t.getAttributes + override private[sbt] def setAttributes(attributes: Map[String, String]): Unit = + t.setAttributes(attributes) + override private[sbt] def setSize(width: Int, height: Int): Unit = t.setSize(width, height) + override def printStream: PrintStream = t.printStream + override def withPrintStream[T](f: PrintStream => T): T = t.withPrintStream(f) + override private[sbt] def withRawOutput[R](f: => R): R = t.withRawOutput(f) + override def restore(): Unit = t.restore() + override def close(): Unit = {} + override private[sbt] def write(bytes: Int*): Unit = t.write(bytes: _*) + override def getLastLine: Option[String] = t.getLastLine + override def getLines: Seq[String] = t.getLines + override private[sbt] def name: String = t.name + override def toString: String = s"ProxyTerminal(current = $t)" + } + private[sbt] def get: Terminal = ProxyTerminal + + private[sbt] def withIn[T](in: InputStream)(f: => T): T = { + val original = inputStream.get + try { + inputStream.set(in) + System.setIn(in) + scala.Console.withIn(in)(f) + } finally { + inputStream.set(original) + System.setIn(original) + } + } + + private[sbt] def withOut[T](out: PrintStream)(f: => T): T = { + val originalOut = System.out + val originalErr = System.err + val originalProxyOut = ConsoleOut.getGlobalProxy + try { + ConsoleOut.setGlobalProxy(ConsoleOut.printStreamOut(out)) + System.setOut(out) + System.setErr(out) + scala.Console.withErr(out)(scala.Console.withOut(out)(f)) + } finally { + ConsoleOut.setGlobalProxy(originalProxyOut) + System.setOut(originalOut) + System.setErr(originalErr) + } + } + + val sepBytes = System.lineSeparator.getBytes("UTF-8") + private class LinePrintStream(outputStream: OutputStream) + extends PrintStream(outputStream, true) { + override def println(s: String): Unit = synchronized { + out.write(s.getBytes("UTF-8") ++ sepBytes) + out.flush() + } + } + private[this] val originalOut = new LinePrintStream(System.out) + private[this] val originalErr = System.err + private[this] val originalIn = System.in + private[sbt] class WriteableInputStream(in: InputStream, name: String) + extends SimpleInputStream + with AutoCloseable { + private[this] val isRaw = new AtomicBoolean(false) + final def write(bytes: Int*): Unit = buffer.synchronized { + bytes.foreach(b => buffer.put(b)) + } + def setRawMode(toggle: Boolean): Unit = { + isRaw.set(toggle) + in match { + case win: WindowsInputStream => win.setRawMode(toggle) + case _ => + } + } + private[this] val executor = + Executors.newSingleThreadExecutor(r => new Thread(r, s"sbt-$name-input-reader")) + private[this] val buffer = new LinkedBlockingQueue[Integer] + private[this] val closed = new AtomicBoolean(false) + private[this] val readQueue = new LinkedBlockingQueue[Unit] + private[this] val readThread = new AtomicReference[Thread] + /* + * Starts a loop that fills a buffer with bytes from stdin. We only read from + * the underlying stream when the buffer is empty and there is an active reader. + * If the reader detaches without consuming any bytes, we just buffer the + * next byte that we read from the stream. One known issue with this approach + * is that if a remote client triggers a reboot, we cannot necessarily stop this + * loop from consuming the next byte from standard in even if sbt has fully + * rebooted and the byte will never be consumed. We try to fix this in withStreams + * by setting the terminal to raw mode, which the input stream makes it non blocking, + * but this approach only works on posix platforms. + */ + private[this] val runnable: Runnable = () => { + @tailrec def impl(): Unit = { + val _ = readQueue.take + val b = in.read + buffer.synchronized(buffer.put(b)) + if (Thread.interrupted() || (b == -1 && isRaw.get)) closed.set(true) + else impl() + } + try impl() + catch { case _: InterruptedException => closed.set(true) } + } + executor.submit(runnable) + def read(result: LinkedBlockingQueue[Integer]): Unit = + if (!closed.get) + readThread.synchronized { + readThread.set(Thread.currentThread) + try buffer.poll match { + case null => + readQueue.put(()) + result.put(buffer.take) + case b if b == -1 => throw new ClosedChannelException + case b => result.put(b) + } finally readThread.set(null) + } + override def read(): Int = { + val result = new LinkedBlockingQueue[Integer] + read(result) + result.poll match { + case null => -1 + case i => i.toInt + } + } + def cancel(): Unit = readThread.synchronized { + Option(readThread.getAndSet(null)).foreach(_.interrupt()) + readQueue.clear() + } + + override def available(): Int = { + buffer.size + } + override def close(): Unit = if (closed.compareAndSet(false, true)) { + executor.shutdownNow() + () + } + } + private[this] def nonBlockingIn(term: org.jline.terminal.Terminal): WriteableInputStream = { + val in = if (Util.isNonCygwinWindows) new WindowsInputStream(term, originalIn) else originalIn + new WriteableInputStream(in, "console") + } + + private[this] val inputStream = new AtomicReference[InputStream](System.in) + private[this] def withOut[T](f: => T): T = { + try { + System.setOut(proxyPrintStream) + System.setErr(proxyErrorStream) + scala.Console.withErr(proxyErrorStream)(scala.Console.withOut(proxyPrintStream)(f)) + } finally { + System.setOut(originalOut) + System.setErr(originalErr) + } + } + private[this] def withIn[T](f: => T): T = + try { + inputStream.set(proxyInputStream) + System.setIn(proxyInputStream) + scala.Console.withIn(proxyInputStream)(f) + } finally System.setIn(originalIn) + + private[sbt] def withPrintStream[T](f: PrintStream => T): T = console.withPrintStream(f) + private[this] val attached = new AtomicBoolean(true) + + /** + * A wrapped instance of a jline.Terminal2 instance. It should only ever be changed when the + * backgrounds sbt with ctrl+z and then foregrounds sbt which causes a call to reset. The + * Terminal.console method returns this terminal and the ConsoleChannel delegates its + * terminal method to it. + */ + private[this] val consoleTerminalHolder: AtomicReference[Terminal] = + new AtomicReference(SimpleTerminal) + + /** + * The terminal that is currently being used by the proxyInputStream and proxyOutputStream. + * It is set through the Terminal.set method which is called by the SetTerminal command, which + * is used to change the terminal during task evaluation. This allows us to route System.in and + * System.out through the terminal's input and output streams. + */ + private[this] val activeTerminal = new AtomicReference[Terminal](consoleTerminalHolder.get) + + /** + * The boot input stream allows a remote client to forward input to the sbt process while + * it is still loading. It works by updating proxyInputStream to read from the + * value of bootInputStreamHolder if it is non-null as well as from the normal process + * console io (assuming there is console io). + */ + private[this] val bootInputStreamHolder = new AtomicReference[InputStream] + + /** + * The boot output stream allows sbt to relay the bytes written to stdout to one or + * more remote clients while the sbt build is loading and hasn't yet loaded a server. + * The output stream of TerminalConsole is updated to write to value of + * bootOutputStreamHolder when it is non-null as well as the normal process console + * output stream. + */ + private[this] val bootOutputStreamHolder = new AtomicReference[OutputStream] + private[sbt] def setBootStreams( + bootInputStream: InputStream, + bootOutputStream: OutputStream + ): Unit = { + bootInputStreamHolder.set(bootInputStream) + bootOutputStreamHolder.set(bootOutputStream) + } + + private[sbt] trait SimpleInputStream extends InputStream { + override def read(b: Array[Byte]): Int = read(b, 0, b.length) + override def read(b: Array[Byte], off: Int, len: Int): Int = { + read() match { + case -1 => -1 + case byte => + b(off) = byte.toByte + 1 + } + } + } + private[this] object proxyInputStream extends SimpleInputStream { + private[this] val isScripted = System.getProperty("sbt.scripted", "false") == "true" + /* + * This is to handle the case when a remote client starts sbt and the build fails. + * We need to be able to consume input bytes from the remote client, but they + * haven't yet connected to the main server but may be connected to the + * BootServerSocket. Unfortunately there is no poll method on input stream that + * takes a duration so we have to manually implement that here. All of the input + * streams that we create in sbt are interruptible, so we can just poll each + * of the input streams and periodically interrupt the thread to switch between + * the two input streams. + */ + private class ReadThread extends Thread with AutoCloseable { + val result = new LinkedBlockingQueue[Integer] + val running = new AtomicBoolean(true) + setDaemon(true) + start() + override def run(): Unit = while (running.get) { + bootInputStreamHolder.get match { + case null => + case is => + def readFrom(inputStream: InputStream) = + try { + if (running.get) { + inputStream.read match { + case -1 => + case `NO_BOOT_CLIENTS_CONNECTED` => + if (System.console == null) { + result.put(-1) + running.set(false) + } + case i => + result.put(i) + running.set(false) + } + } + } catch { case _: InterruptedException => } + readFrom(is) + readFrom(activeTerminal.get().inputStream) + } + } + override def close(): Unit = if (running.compareAndSet(true, false)) this.interrupt() + } + def read(): Int = { + if (isScripted) -1 + else if (bootInputStreamHolder.get == null) activeTerminal.get().inputStream.read() + else { + val thread = new ReadThread + @tailrec def poll(): Int = thread.result.poll(10, TimeUnit.MILLISECONDS) match { + case null => + thread.interrupt() + poll() + case i => i + } + poll() + } + } + } + private[this] object proxyOutputStream extends OutputStream { + private[this] def os: OutputStream = activeTerminal.get().outputStream + def write(byte: Int): Unit = { + os.write(byte) + os.flush() + if (byte == 10) os.flush() + } + override def write(bytes: Array[Byte]): Unit = write(bytes, 0, bytes.length) + override def write(bytes: Array[Byte], offset: Int, len: Int): Unit = { + os.write(bytes, offset, len) + os.flush() + } + override def flush(): Unit = os.flush() + } + private[this] val proxyPrintStream = new LinePrintStream(proxyOutputStream) { + override def toString: String = s"proxyPrintStream($proxyOutputStream)" + } + private[this] object proxyErrorOutputStream extends OutputStream { + private[this] def os: OutputStream = activeTerminal.get().errorStream + def write(byte: Int): Unit = os.write(byte) + override def write(bytes: Array[Byte]): Unit = write(bytes, 0, bytes.length) + override def write(bytes: Array[Byte], offset: Int, len: Int): Unit = + os.write(bytes, offset, len) + override def flush(): Unit = os.flush() + } + private[this] object proxyErrorStream extends PrintStream(proxyErrorOutputStream, true) + private[this] lazy val isWindows = + System.getProperty("os.name", "").toLowerCase(Locale.ENGLISH).indexOf("windows") >= 0 + private[this] object WrappedSystemIn extends SimpleInputStream { + private[this] val in = proxyInputStream + override def available(): Int = if (attached.get) in.available() else 0 + override def read(): Int = synchronized { + if (attached.get) { + val res = in.read() + if (res == -1) attached.set(false) + res + } else -1 + } + } + + /* + * When the server is booted by a remote client, it may not be able to accurately + * calculate the terminal properties. To work around this, we can set the + * properties via an environment property. It was too difficult to get system + * properties working correctly with windows. + */ + private class Props( + val width: Int, + val height: Int, + val ansi: Boolean, + val color: Boolean, + val supershell: Boolean + ) + private[sbt] val TERMINAL_PROPS = "SBT_TERMINAL_PROPS" + private val props = System.getenv(TERMINAL_PROPS) match { + case null => None + case p => + p.split(",") match { + case Array(width, height, ansi, color, supershell) => + Try( + new Props( + width.toInt, + height.toInt, + ansi.toBoolean, + color.toBoolean, + supershell.toBoolean + ) + ).toOption + case _ => None + } + } + private[sbt] def startedByRemoteClient = props.isDefined + + private[this] def newConsoleTerminal(): Terminal = { + val system = JLine3.system + new ConsoleTerminal(nonBlockingIn(system), originalOut, system) + } + + private[sbt] def reset(): Unit = { + jline.TerminalFactory.reset() + console.close() + if (hasConsole && !isDumbTerminal) consoleTerminalHolder.set(newConsoleTerminal()) + } + + // translate explicit class names to type in order to support + // older Scala, since it shaded classes but not the system property + private[this] def fixTerminalProperty(): Unit = { + val terminalProperty = "jline.terminal" + val newValue = + if (!isAnsiSupported && System.getProperty("sbt.io.virtual", "") == "false") "none" + else + System.getProperty(terminalProperty) match { + case "jline.UnixTerminal" => "unix" + case null if System.getProperty("sbt.cygwin") != null => "unix" + case "jline.WindowsTerminal" => "windows" + case "jline.AnsiWindowsTerminal" => "windows" + case "jline.UnsupportedTerminal" => "none" + case null if isDumb => "none" + case x => x + } + if (newValue != null) { + System.setProperty(terminalProperty, newValue) + () + } + } + fixTerminalProperty() + + private[sbt] def createReader(term: Terminal, prompt: Prompt): ConsoleReader = { + new ConsoleReader(term.inputStream, term.outputStream, term.toJLine) { + override def readLine(prompt: String, mask: Character): String = + term.withRawInput(super.readLine(prompt, mask)) + override def readLine(prompt: String): String = term.withRawInput(super.readLine(prompt)) + } + } + + def console: Terminal = consoleTerminalHolder.get match { + case null => throw new IllegalStateException("Uninitialized terminal.") + case term => term + } + + private val capabilityMap = + org.jline.utils.InfoCmp.Capability.values().map(c => c.toString -> c).toMap + private val consoleProgressState = new AtomicReference[ProgressState](new ProgressState(1)) + private[sbt] def setConsoleProgressState(progressState: ProgressState): Unit = + consoleProgressState.set(progressState) + + @deprecated("For compatibility only", "1.4.0") + private[sbt] def deprecatedTeminal: jline.Terminal = console.toJLine + private[util] class ConsoleTerminal( + in: WriteableInputStream, + out: OutputStream, + private[util] val system: org.jline.terminal.Terminal, + ) extends TerminalImpl(in, out, originalErr, "console0") { + private[this] val rawMode = new AtomicBoolean(false) + if (Util.isWindows && hasConsole) { + // It is necessary to enter and exit raw mode in order to get the windows + // console to echo input. + enterRawMode() + exitRawMode() + } + override private[sbt] def getSizeImpl: (Int, Int) = { + val size = system.getSize + (size.getColumns, size.getRows) + } + override lazy val isAnsiSupported: Boolean = + !isDumbTerminal && Terminal.isAnsiSupported && !isCI + override private[sbt] def progressState: ProgressState = consoleProgressState.get + override def isEchoEnabled: Boolean = + try system.echo() + catch { case _: InterruptedIOException => false } + override def isSuccessEnabled: Boolean = true + override def setEchoEnabled(toggle: Boolean): Unit = + try Util.ignoreResult(system.echo(toggle)) + catch { case _: InterruptedIOException => } + override def getBooleanCapability(capability: String): Boolean = + capabilityMap.get(capability).fold(false)(system.getBooleanCapability) + override def getNumericCapability(capability: String): Integer = + capabilityMap.get(capability).fold(null: Integer)(system.getNumericCapability) + override def getStringCapability(capability: String): String = { + val res = capabilityMap.get(capability).fold(null: String)(system.getStringCapability) + res + } + override private[sbt] def restore(): Unit = exitRawMode() + + override private[sbt] def getAttributes: Map[String, String] = + Try(JLine3.toMap(system.getAttributes)).getOrElse(Map.empty) + override private[sbt] def setAttributes(attributes: Map[String, String]): Unit = { + system.setAttributes(JLine3.attributesFromMap(attributes)) + JLine3.setEnableProcessInput() + } + override private[sbt] def setSize(width: Int, height: Int): Unit = + system.setSize(new org.jline.terminal.Size(width, height)) + + override def inputStream: InputStream = in + + override private[sbt] def enterRawMode(): Unit = + if (rawMode.compareAndSet(false, true) && hasConsole) { + in.setRawMode(true) + try JLine3.enterRawMode(system) + catch { case _: java.io.IOError => } + } + override private[sbt] def exitRawMode(): Unit = + if (rawMode.compareAndSet(true, false) && hasConsole) { + in.setRawMode(false) + try JLine3.exitRawMode(system) + catch { case _: java.io.IOError => } + } + override def isColorEnabled: Boolean = + props + .map(_.color) + .getOrElse(isColorEnabledProp.getOrElse(Terminal.isColorEnabled)) + + override def isSupershellEnabled: Boolean = + props + .map(_.supershell) + .getOrElse(System.getProperty("sbt.supershell") match { + case null => + !(sys.env.contains("BUILD_NUMBER") || sys.env + .contains("CI")) && isColorEnabled && !Util.isEmacs + case "true" => true + case _ => false + }) + override def close(): Unit = { + try { + system.setAttributes(JLine3.initialAttributes.get) + system.close() + in.close() + } catch { case NonFatal(_) => } + super.close() + } + } + private[sbt] abstract class TerminalImpl private[sbt] ( + val in: WriteableInputStream, + val out: OutputStream, + override val errorStream: OutputStream, + override private[sbt] val name: String + ) extends Terminal { + private[sbt] def getSizeImpl: (Int, Int) + private[this] val sizeRefreshPeriod = 1.second + private[this] val size = + new AtomicReference[((Int, Int), Deadline)](((1, 1), Deadline.now - 1.day)) + private[this] def setSize() = size.set((Try(getSizeImpl).getOrElse((1, 1)), Deadline.now)) + private[this] def getSize = size.get match { + case (s, d) if (d + sizeRefreshPeriod).isOverdue => + setSize() + size.get._1 + case (s, _) => s + } + override def getWidth: Int = getSize._1 + override def getHeight: Int = getSize._2 + private[this] val rawMode = new AtomicBoolean(false) + private[this] val writeLock = new AnyRef + def throwIfClosed[R](f: => R): R = if (isStopped.get) throw new ClosedChannelException else f + override def getLastLine: Option[String] = progressState.currentLine + override def getLines: Seq[String] = progressState.getLines + + private val combinedOutputStream = new OutputStream { + override def write(b: Int): Unit = { + Option(bootOutputStreamHolder.get).foreach(_.write(b)) + out.write(b) + } + override def write(b: Array[Byte]): Unit = { + write(b, 0, b.length) + } + override def write(b: Array[Byte], offset: Int, len: Int): Unit = { + Option(bootOutputStreamHolder.get).foreach(_.write(b, offset, len)) + out.write(b, offset, len) + } + override def flush(): Unit = { + Option(bootOutputStreamHolder.get).foreach(_.flush()) + out.flush() + } + } + + override val outputStream = new OutputStream { + override def write(b: Int): Unit = throwIfClosed { + write(Array((b & 0xFF).toByte)) + } + override def write(b: Array[Byte]): Unit = throwIfClosed { + writeLock.synchronized(doWrite(b)) + } + override def write(b: Array[Byte], offset: Int, length: Int): Unit = throwIfClosed { + write(Arrays.copyOfRange(b, offset, offset + length)) + } + override def flush(): Unit = combinedOutputStream.flush() + } + private def doWrite(rawBytes: Array[Byte]): Unit = withPrintStream { ps => + val (toWrite, len) = + if (rawBytes.contains(27.toByte)) { + if (!isAnsiSupported || !isColorEnabled) + EscHelpers.strip(rawBytes, stripAnsi = !isAnsiSupported, stripColor = !isColorEnabled) + else (rawBytes, rawBytes.length) + } else (rawBytes, rawBytes.length) + val bytes = if (len < toWrite.length) toWrite.take(len) else toWrite + progressState.write(TerminalImpl.this, bytes, ps, hasProgress.get && !rawMode.get) + } + override private[sbt] val printStream: PrintStream = new LinePrintStream(outputStream) + override def inputStream: InputStream = in + + private[sbt] def write(bytes: Int*): Unit = in.write(bytes: _*) + private[this] val isStopped = new AtomicBoolean(false) + + override def getLineHeightAndWidth(line: String): (Int, Int) = getWidth match { + case width if width > 0 => + val position = EscHelpers.cursorPosition(line) + val count = (position + width - 1) / width + (count, position - (math.max((count - 1), 0) * width)) + case _ => (0, 0) + } + + private[sbt] def withRawOutput[R](f: => R): R = { + rawMode.set(true) + try f + finally rawMode.set(false) + } + private[this] val rawPrintStream: PrintStream = new LinePrintStream(combinedOutputStream) + override def withPrintStream[T](f: PrintStream => T): T = + writeLock.synchronized(f(rawPrintStream)) + + override def close(): Unit = if (isStopped.compareAndSet(false, true)) { + in.close() + } + } + private lazy val nullInputStream: InputStream = () => { + try this.synchronized(this.wait) + catch { case _: InterruptedException => } + -1 + } + private[sbt] class DefaultTerminal extends Terminal { + override def close(): Unit = {} + override private[sbt] def progressState: ProgressState = new ProgressState(1) + override private[sbt] def enterRawMode(): Unit = {} + override private[sbt] def exitRawMode(): Unit = {} + override def getBooleanCapability(capability: String): Boolean = false + override def getHeight: Int = 0 + override def getLastLine: Option[String] = None + override def getLines: Seq[String] = Nil + override def getLineHeightAndWidth(line: String): (Int, Int) = (0, 0) + override def getNumericCapability(capability: String): Integer = null + override def getStringCapability(capability: String): String = null + override def getWidth: Int = 0 + override def inputStream: InputStream = nullInputStream + override def isAnsiSupported: Boolean = Terminal.isAnsiSupported + override def isColorEnabled: Boolean = isColorEnabledProp.getOrElse(Terminal.isColorEnabled) + override def isEchoEnabled: Boolean = false + override def isSuccessEnabled: Boolean = true + override def isSupershellEnabled: Boolean = false + override def setEchoEnabled(toggle: Boolean): Unit = {} + override def outputStream: OutputStream = _ => {} + override def errorStream: OutputStream = _ => {} + override private[sbt] def getAttributes: Map[String, String] = Map.empty + override private[sbt] def setAttributes(attributes: Map[String, String]): Unit = {} + override private[sbt] def setSize(width: Int, height: Int): Unit = {} + override private[sbt] def name: String = "NullTerminal" + override private[sbt] val printStream: PrintStream = + new PrintStream(outputStream, false) + override private[sbt] def withPrintStream[T](f: PrintStream => T): T = f(printStream) + override private[sbt] def write(bytes: Int*): Unit = {} + override private[sbt] def withRawOutput[R](f: => R): R = f + } + private[sbt] object NullTerminal extends DefaultTerminal + private[sbt] object SimpleTerminal extends DefaultTerminal { + override lazy val inputStream: InputStream = originalIn + override lazy val outputStream: OutputStream = originalOut + override lazy val errorStream: OutputStream = originalErr + } +} diff --git a/internal/util-logging/src/main/scala/sbt/internal/util/WindowsInputStream.scala b/internal/util-logging/src/main/scala/sbt/internal/util/WindowsInputStream.scala new file mode 100644 index 000000000..486759cbb --- /dev/null +++ b/internal/util-logging/src/main/scala/sbt/internal/util/WindowsInputStream.scala @@ -0,0 +1,154 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util + +import java.io.InputStream +import java.util.concurrent.LinkedBlockingQueue +import java.util.concurrent.atomic.AtomicBoolean +import org.fusesource.jansi.internal.Kernel32 +import org.jline.utils.InfoCmp.Capability +import scala.annotation.tailrec +import Terminal.SimpleInputStream + +private object WindowsSupport { + def getConsoleMode = { + val console = Kernel32.GetStdHandle(Kernel32.STD_INPUT_HANDLE); + val mode = new Array[Int](1); + if (Kernel32.GetConsoleMode(console, mode) == 0) -1 else mode.head + } + def setConsoleMode(mode: Int): Unit = { + val console = Kernel32.GetStdHandle(Kernel32.STD_INPUT_HANDLE); + Kernel32.SetConsoleMode(console, mode) + () + } + def readConsoleInput(count: Int) = { + val console = Kernel32.GetStdHandle(Kernel32.STD_INPUT_HANDLE); + Kernel32.readConsoleInputHelper(console, 1, false) + } +} +/* + * We need a special input stream for windows because special key events + * like arrow keys are not reported by System.in. What makes this extra + * tricky is that in canonical mode, ReadConsoleInput does not echo + * characters, even when echo is set. The ReadConsole api does, but that + * api isn't exposed by jansi so we can't use it without rolling our own + * jni library or using the jna. That is more trouble than it's worth. To + * work around the canonical mode issue, we can switch between reading using + * ReadConsoleInput and just reading directly from the original System.in, + * which does echo characters in canonical mode, when we enter and exit + * raw mode. + */ +private[util] class WindowsInputStream(term: org.jline.terminal.Terminal, in: InputStream) + extends SimpleInputStream { + private val SHIFT_FLAG = 0x01; + private val ALT_FLAG = 0x02; + private val CTRL_FLAG = 0x04; + + private val RIGHT_ALT_PRESSED = 0x0001; + private val LEFT_ALT_PRESSED = 0x0002; + private val RIGHT_CTRL_PRESSED = 0x0004; + private val LEFT_CTRL_PRESSED = 0x0008; + private val SHIFT_PRESSED = 0x0010; + private def getCapability(cap: Capability): String = term.getStringCapability(cap) match { + case null => null + case c => c.replaceAllLiterally("\\E", "\u001B") + } + /* + * This function is a hybrid of jline 2 WindowsTerminal.readConsoleInput + * and jline3 AbstractTerminal.getEscapeSequence. + */ + private def readConsoleInput(): Array[Byte] = { + WindowsSupport.readConsoleInput(1) match { + case null => Array.empty + case events => + val sb = new StringBuilder(); + events.foreach { event => + val keyEvent = event.keyEvent + val controlKeyState = keyEvent.controlKeyState + val isCtrl = (controlKeyState & (RIGHT_CTRL_PRESSED | LEFT_CTRL_PRESSED)) > 0; + val isAlt = (controlKeyState & (RIGHT_ALT_PRESSED | LEFT_ALT_PRESSED)) > 0; + val isShift = (controlKeyState & SHIFT_PRESSED) > 0; + if (keyEvent.keyDown) { + if (keyEvent.uchar > 0) { + if (((keyEvent.uchar >= '@' && keyEvent.uchar <= '_') || (keyEvent.uchar >= 'a' && keyEvent.uchar <= 'z')) + && isAlt && !isCtrl) { + sb.append('\u001B') // ESC + } + if (isShift && keyEvent.keyCode == 9) { + getCapability(Capability.key_btab) match { + case null => sb.append(keyEvent.uchar) + case cap => sb.append(cap) + } + } else { + sb.append(keyEvent.uchar) + } + } else { + // virtual keycodes: http://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx + // just add support for basic editing keys (no control state, no numpad keys) + val escapeSequence = keyEvent.keyCode match { + case 0x21 /* VK_PRIOR PageUp*/ => getCapability(Capability.key_ppage); + case 0x22 /* VK_NEXT PageDown*/ => getCapability(Capability.key_npage); + case 0x24 /* VK_HOME */ => getCapability(Capability.key_home) + case 0x25 /* VK_LEFT */ => getCapability(Capability.key_left) + case 0x26 /* VK_UP */ => getCapability(Capability.key_up) + case 0x27 /* VK_RIGHT */ => getCapability(Capability.key_right) + case 0x28 /* VK_DOWN */ => getCapability(Capability.key_down) + case 0x70 /* VK_F1 */ => getCapability(Capability.key_f1) + case 0x71 /* VK_F2 */ => getCapability(Capability.key_f2) + case 0x72 /* VK_F3 */ => getCapability(Capability.key_f3) + case 0x73 /* VK_F4 */ => getCapability(Capability.key_f4) + case 0x74 /* VK_F5 */ => getCapability(Capability.key_f5) + case 0x75 /* VK_F6 */ => getCapability(Capability.key_f6) + case 0x76 /* VK_F7 */ => getCapability(Capability.key_f7) + case 0x77 /* VK_F8 */ => getCapability(Capability.key_f8) + case 0x78 /* VK_F9 */ => getCapability(Capability.key_f9) + case 0x79 /* VK_F10 */ => getCapability(Capability.key_f10) + case 0x7A /* VK_F11 */ => getCapability(Capability.key_f11) + case 0x7B /* VK_F12 */ => getCapability(Capability.key_f12) + // VK_END, VK_INSERT and VK_DELETE are not in the ansi key bindings so we + // have to manually apply the the sequences here and in JLine3.wrap + case 0x23 /* VK_END */ => + Option(getCapability(Capability.key_end)).getOrElse("\u001B[4~") + case 0x2D /* VK_INSERT */ => + Option(getCapability(Capability.key_ic)).getOrElse("\u001B[2~") + case 0x2E /* VK_DELETE */ => + Option(getCapability(Capability.key_dc)).getOrElse("\u001B[3~") + case _ => null + } + escapeSequence match { + case null => + case es => (0 until keyEvent.repeatCount.toInt).foreach(_ => sb.append(es)) + } + } + } else { + // key up event + // support ALT+NumPad input method + if (keyEvent.keyCode == 0x12 /*VK_MENU ALT key*/ && keyEvent.uchar > 0) { + sb.append(keyEvent.uchar); + } + } + } + sb.toString().getBytes() + } + } + private[this] val raw: InputStream = new SimpleInputStream { + val buffer = new LinkedBlockingQueue[Integer] + @tailrec + override def read(): Int = { + buffer.poll match { + case null => + readConsoleInput().foreach(b => buffer.put(b & 0xFF)) + if (!Thread.interrupted) read() else throw new InterruptedException + case b => b + } + } + } + private[this] val isRaw = new AtomicBoolean(true) + private[sbt] def setRawMode(toggle: Boolean): Unit = isRaw.set(toggle) + override def read(): Int = if (isRaw.get) raw.read() else in.read() +} diff --git a/internal/util-logging/src/main/scala/sbt/internal/util/codec/JValueFormats.scala b/internal/util-logging/src/main/scala/sbt/internal/util/codec/JValueFormats.scala new file mode 100644 index 000000000..1b67fa425 --- /dev/null +++ b/internal/util-logging/src/main/scala/sbt/internal/util/codec/JValueFormats.scala @@ -0,0 +1,59 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt +package internal +package util.codec + +import sjsonnew.{ JsonWriter => JW, JsonReader => JR, JsonFormat => JF, _ } +import sjsonnew.shaded.scalajson.ast.unsafe._ + +trait JValueFormats { self: sjsonnew.BasicJsonProtocol => + implicit val JNullFormat: JF[JNull.type] = new JF[JNull.type] { + def write[J](x: JNull.type, b: Builder[J]) = b.writeNull() + def read[J](j: Option[J], u: Unbuilder[J]) = JNull + } + + implicit val JBooleanFormat: JF[JBoolean] = projectFormat(_.get, (x: Boolean) => JBoolean(x)) + implicit val JStringFormat: JF[JString] = projectFormat(_.value, (x: String) => JString(x)) + + implicit val JNumberFormat: JF[JNumber] = + projectFormat(x => BigDecimal(x.value), (x: BigDecimal) => JNumber(x.toString)) + + implicit val JArrayFormat: JF[JArray] = projectFormat[JArray, Array[JValue]](_.value, JArray(_)) + + implicit lazy val JObjectJsonWriter: JW[JObject] = new JW[JObject] { + def write[J](x: JObject, b: Builder[J]) = { + b.beginObject() + x.value foreach (jsonField => JValueFormat.addField(jsonField.field, jsonField.value, b)) + b.endObject() + } + } + + implicit lazy val JValueJsonWriter: JW[JValue] = new JW[JValue] { + def write[J](x: JValue, b: Builder[J]) = x match { + case x: JNull.type => JNullFormat.write(x, b) + case x: JBoolean => JBooleanFormat.write(x, b) + case x: JString => JStringFormat.write(x, b) + case x: JNumber => JNumberFormat.write(x, b) + case x: JArray => JArrayFormat.write(x, b) + case x: JObject => JObjectJsonWriter.write(x, b) + } + } + + // This passes through JValue, or returns JNull instead of blowing up with unimplemented. + implicit lazy val JValueJsonReader: JR[JValue] = new JR[JValue] { + def read[J](j: Option[J], u: Unbuilder[J]) = j match { + case Some(x: JValue) => x + case Some(x) => sys.error(s"Uknown AST $x") + case _ => JNull + } + } + + implicit lazy val JValueFormat: JF[JValue] = + jsonFormat[JValue](JValueJsonReader, JValueJsonWriter) +} diff --git a/internal/util-logging/src/main/scala/sbt/internal/util/codec/PositionFormats.scala b/internal/util-logging/src/main/scala/sbt/internal/util/codec/PositionFormats.scala new file mode 100644 index 000000000..9b77889df --- /dev/null +++ b/internal/util-logging/src/main/scala/sbt/internal/util/codec/PositionFormats.scala @@ -0,0 +1,73 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util.codec +import _root_.sjsonnew.{ deserializationError, Builder, JsonFormat, Unbuilder } +import xsbti.Position +import java.util.Optional + +trait PositionFormats { self: sjsonnew.BasicJsonProtocol => + implicit lazy val PositionFormat: JsonFormat[Position] = new JsonFormat[Position] { + override def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): Position = { + jsOpt match { + case Some(js) => + unbuilder.beginObject(js) + val line0 = unbuilder.readField[Optional[java.lang.Integer]]("line") + val lineContent0 = unbuilder.readField[String]("lineContent") + val offset0 = unbuilder.readField[Optional[java.lang.Integer]]("offset") + val pointer0 = unbuilder.readField[Optional[java.lang.Integer]]("pointer") + val pointerSpace0 = unbuilder.readField[Optional[String]]("pointerSpace") + val sourcePath0 = unbuilder.readField[Optional[String]]("sourcePath") + val sourceFile0 = unbuilder.readField[Optional[java.io.File]]("sourceFile") + val startOffset0 = unbuilder.readField[Optional[java.lang.Integer]]("startOffset") + val endOffset0 = unbuilder.readField[Optional[java.lang.Integer]]("endOffset") + val startLine0 = unbuilder.readField[Optional[java.lang.Integer]]("startLine") + val startColumn0 = unbuilder.readField[Optional[java.lang.Integer]]("startColumn") + val endLine0 = unbuilder.readField[Optional[java.lang.Integer]]("endLine") + val endColumn0 = unbuilder.readField[Optional[java.lang.Integer]]("endColumn") + + unbuilder.endObject() + new Position() { + override val line = line0 + override val lineContent = lineContent0 + override val offset = offset0 + override val pointer = pointer0 + override val pointerSpace = pointerSpace0 + override val sourcePath = sourcePath0 + override val sourceFile = sourceFile0 + override val startOffset = startOffset0 + override val endOffset = endOffset0 + override val startLine = startLine0 + override val startColumn = startColumn0 + override val endLine = endLine0 + override val endColumn = endColumn0 + + } + case None => + deserializationError("Expected JsObject but found None") + } + } + override def write[J](obj: Position, builder: Builder[J]): Unit = { + builder.beginObject() + builder.addField("line", obj.line) + builder.addField("lineContent", obj.lineContent) + builder.addField("offset", obj.offset) + builder.addField("pointer", obj.pointer) + builder.addField("pointerSpace", obj.pointerSpace) + builder.addField("sourcePath", obj.sourcePath) + builder.addField("sourceFile", obj.sourceFile) + builder.addField("startOffset", obj.startOffset) + builder.addField("endOffset", obj.endOffset) + builder.addField("startLine", obj.startLine) + builder.addField("startColumn", obj.startColumn) + builder.addField("endLine", obj.endLine) + builder.addField("endColumn", obj.endColumn) + + builder.endObject() + } + } +} diff --git a/internal/util-logging/src/main/scala/sbt/internal/util/codec/ProblemFormats.scala b/internal/util-logging/src/main/scala/sbt/internal/util/codec/ProblemFormats.scala new file mode 100644 index 000000000..2ae0d7512 --- /dev/null +++ b/internal/util-logging/src/main/scala/sbt/internal/util/codec/ProblemFormats.scala @@ -0,0 +1,48 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util.codec + +import xsbti.{ Problem, Severity, Position } +import _root_.sjsonnew.{ deserializationError, Builder, JsonFormat, Unbuilder } +import java.util.Optional + +trait ProblemFormats { self: SeverityFormats with PositionFormats with sjsonnew.BasicJsonProtocol => + implicit lazy val ProblemFormat: JsonFormat[Problem] = new JsonFormat[Problem] { + override def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): Problem = { + jsOpt match { + case Some(js) => + unbuilder.beginObject(js) + val category0 = unbuilder.readField[String]("category") + val severity0 = unbuilder.readField[Severity]("severity") + val message0 = unbuilder.readField[String]("message") + val position0 = unbuilder.readField[Position]("position") + val rendered0 = unbuilder.readField[Optional[String]]("rendered") + + unbuilder.endObject() + new Problem { + override val category = category0 + override val position = position0 + override val message = message0 + override val severity = severity0 + override val rendered = rendered0 + } + case None => + deserializationError("Expected JsObject but found None") + } + } + override def write[J](obj: Problem, builder: Builder[J]): Unit = { + builder.beginObject() + builder.addField("category", obj.category) + builder.addField("severity", obj.severity) + builder.addField("message", obj.message) + builder.addField("position", obj.position) + builder.addField("rendered", obj.rendered) + builder.endObject() + } + } +} diff --git a/internal/util-logging/src/main/scala/sbt/internal/util/codec/SeverityFormats.scala b/internal/util-logging/src/main/scala/sbt/internal/util/codec/SeverityFormats.scala new file mode 100644 index 000000000..982d248d0 --- /dev/null +++ b/internal/util-logging/src/main/scala/sbt/internal/util/codec/SeverityFormats.scala @@ -0,0 +1,36 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util.codec + +import _root_.sjsonnew.{ deserializationError, Builder, JsonFormat, Unbuilder } +import xsbti.Severity; + +trait SeverityFormats { self: sjsonnew.BasicJsonProtocol => + implicit lazy val SeverityFormat: JsonFormat[Severity] = new JsonFormat[Severity] { + override def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): Severity = { + jsOpt match { + case Some(js) => + unbuilder.readString(js) match { + case "Info" => Severity.Info + case "Warn" => Severity.Warn + case "Error" => Severity.Error + } + case None => + deserializationError("Expected JsString but found None") + } + } + override def write[J](obj: Severity, builder: Builder[J]): Unit = { + val str = obj match { + case Severity.Info => "Info" + case Severity.Warn => "Warn" + case Severity.Error => "Error" + } + builder.writeString(str) + } + } +} diff --git a/internal/util-logging/src/main/scala/sbt/internal/util/codec/SuccessEventShowLines.scala b/internal/util-logging/src/main/scala/sbt/internal/util/codec/SuccessEventShowLines.scala new file mode 100644 index 000000000..d0bb12ea3 --- /dev/null +++ b/internal/util-logging/src/main/scala/sbt/internal/util/codec/SuccessEventShowLines.scala @@ -0,0 +1,21 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt +package internal.util.codec + +import sbt.util.ShowLines +import sbt.internal.util.SuccessEvent + +trait SuccessEventShowLines { + implicit val sbtSuccessEventShowLines: ShowLines[SuccessEvent] = + ShowLines[SuccessEvent]((e: SuccessEvent) => { + Vector(e.message) + }) +} + +object SuccessEventShowLines extends SuccessEventShowLines diff --git a/internal/util-logging/src/main/scala/sbt/internal/util/codec/ThrowableShowLines.scala b/internal/util-logging/src/main/scala/sbt/internal/util/codec/ThrowableShowLines.scala new file mode 100644 index 000000000..21f400ee4 --- /dev/null +++ b/internal/util-logging/src/main/scala/sbt/internal/util/codec/ThrowableShowLines.scala @@ -0,0 +1,32 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt +package internal.util.codec + +import sbt.util.ShowLines +import sbt.internal.util.{ StackTrace, TraceEvent } + +trait ThrowableShowLines { + implicit val sbtThrowableShowLines: ShowLines[Throwable] = + ShowLines[Throwable]((t: Throwable) => { + // 0 means enabled with default behavior. See StackTrace.scala. + val traceLevel = 0 + List(StackTrace.trimmed(t, traceLevel)) + }) +} + +object ThrowableShowLines extends ThrowableShowLines + +trait TraceEventShowLines { + implicit val sbtTraceEventShowLines: ShowLines[TraceEvent] = + ShowLines[TraceEvent]((t: TraceEvent) => { + ThrowableShowLines.sbtThrowableShowLines.showLines(t.message) + }) +} + +object TraceEventShowLines extends TraceEventShowLines diff --git a/internal/util-logging/src/main/scala/sbt/util/AbstractLogger.scala b/internal/util-logging/src/main/scala/sbt/util/AbstractLogger.scala new file mode 100644 index 000000000..1337aaf97 --- /dev/null +++ b/internal/util-logging/src/main/scala/sbt/util/AbstractLogger.scala @@ -0,0 +1,36 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.util + +abstract class AbstractLogger extends Logger { + def getLevel: Level.Value + def setLevel(newLevel: Level.Value): Unit + def setTrace(flag: Int): Unit + def getTrace: Int + final def traceEnabled: Boolean = getTrace >= 0 + def successEnabled: Boolean + def setSuccessEnabled(flag: Boolean): Unit + + def atLevel(level: Level.Value): Boolean = level.id >= getLevel.id + def control(event: ControlEvent.Value, message: => String): Unit + + def logAll(events: Seq[LogEvent]): Unit + + /** Defined in terms of other methods in Logger and should not be called from them. */ + final def log(event: LogEvent): Unit = { + event match { + case s: Success => success(s.msg) + case l: Log => log(l.level, l.msg) + case t: Trace => trace(t.exception) + case setL: SetLevel => setLevel(setL.newLevel) + case setT: SetTrace => setTrace(setT.level) + case setS: SetSuccess => setSuccessEnabled(setS.enabled) + case c: ControlEvent => control(c.event, c.msg) + } + } +} diff --git a/internal/util-logging/src/main/scala/sbt/util/InterfaceUtil.scala b/internal/util-logging/src/main/scala/sbt/util/InterfaceUtil.scala new file mode 100644 index 000000000..40fecbae1 --- /dev/null +++ b/internal/util-logging/src/main/scala/sbt/util/InterfaceUtil.scala @@ -0,0 +1,178 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.util + +import xsbti.{ Position, Problem, Severity, T2 } +import java.io.File +import java.util.Optional +import java.util.function.Supplier + +object InterfaceUtil { + def toSupplier[A](a: => A): Supplier[A] = new Supplier[A] { + override def get: A = a + } + + import java.util.function.{ Function => JavaFunction } + def toJavaFunction[A1, R](f: A1 => R): JavaFunction[A1, R] = new JavaFunction[A1, R] { + override def apply(t: A1): R = f(t) + } + + def t2[A1, A2](x: (A1, A2)): T2[A1, A2] = new ConcreteT2(x._1, x._2) + + def toOption[A](m: Optional[A]): Option[A] = + if (m.isPresent) Some(m.get) else None + + def toOptional[A](o: Option[A]): Optional[A] = + o match { + case Some(v) => Optional.of(v) + case None => Optional.empty() + } + + def jo2o[A](o: Optional[A]): Option[A] = + if (o.isPresent) Some(o.get) + else None + + def o2jo[A](o: Option[A]): Optional[A] = + o match { + case Some(v) => Optional.ofNullable(v) + case None => Optional.empty[A]() + } + + @deprecated("Use the overload of this method with more arguments", "1.2.2") + def position( + line0: Option[Integer], + content: String, + offset0: Option[Integer], + pointer0: Option[Integer], + pointerSpace0: Option[String], + sourcePath0: Option[String], + sourceFile0: Option[File] + ): Position = + position( + line0, + content, + offset0, + pointer0, + pointerSpace0, + sourcePath0, + sourceFile0, + None, + None, + None, + None, + None, + None + ) + + def position( + line0: Option[Integer], + content: String, + offset0: Option[Integer], + pointer0: Option[Integer], + pointerSpace0: Option[String], + sourcePath0: Option[String], + sourceFile0: Option[File], + startOffset0: Option[Integer], + endOffset0: Option[Integer], + startLine0: Option[Integer], + startColumn0: Option[Integer], + endLine0: Option[Integer], + endColumn0: Option[Integer] + ): Position = + new ConcretePosition( + line0, + content, + offset0, + pointer0, + pointerSpace0, + sourcePath0, + sourceFile0, + startOffset0, + endOffset0, + startLine0, + startColumn0, + endLine0, + endColumn0 + ) + + @deprecated("Use the overload of this method with more arguments", "1.2.2") + def problem(cat: String, pos: Position, msg: String, sev: Severity): Problem = + problem(cat, pos, msg, sev, None) + + def problem( + cat: String, + pos: Position, + msg: String, + sev: Severity, + rendered: Option[String] + ): Problem = + new ConcreteProblem(cat, pos, msg, sev, rendered) + + private final class ConcreteT2[A1, A2](a1: A1, a2: A2) extends T2[A1, A2] { + val get1: A1 = a1 + val get2: A2 = a2 + override def toString: String = s"ConcreteT2($a1, $a2)" + override def equals(o: Any): Boolean = o match { + case o: ConcreteT2[A1, A2] => + this.get1 == o.get1 && + this.get2 == o.get2 + case _ => false + } + override def hashCode: Int = { + var hash = 1 + hash = hash * 31 + this.get1.## + hash = hash * 31 + this.get2.## + hash + } + } + + private final class ConcretePosition( + line0: Option[Integer], + content: String, + offset0: Option[Integer], + pointer0: Option[Integer], + pointerSpace0: Option[String], + sourcePath0: Option[String], + sourceFile0: Option[File], + startOffset0: Option[Integer], + endOffset0: Option[Integer], + startLine0: Option[Integer], + startColumn0: Option[Integer], + endLine0: Option[Integer], + endColumn0: Option[Integer] + ) extends Position { + val line = o2jo(line0) + val lineContent = content + val offset = o2jo(offset0) + val pointer = o2jo(pointer0) + val pointerSpace = o2jo(pointerSpace0) + val sourcePath = o2jo(sourcePath0) + val sourceFile = o2jo(sourceFile0) + override val startOffset = o2jo(startOffset0) + override val endOffset = o2jo(endOffset0) + override val startLine = o2jo(startLine0) + override val startColumn = o2jo(startColumn0) + override val endLine = o2jo(endLine0) + override val endColumn = o2jo(endColumn0) + } + + private final class ConcreteProblem( + cat: String, + pos: Position, + msg: String, + sev: Severity, + rendered0: Option[String] + ) extends Problem { + val category = cat + val position = pos + val message = msg + val severity = sev + override val rendered = o2jo(rendered0) + override def toString = s"[$severity] $pos: $message" + } +} diff --git a/internal/util-logging/src/main/scala/sbt/util/Level.scala b/internal/util-logging/src/main/scala/sbt/util/Level.scala new file mode 100644 index 000000000..b9d30dca7 --- /dev/null +++ b/internal/util-logging/src/main/scala/sbt/util/Level.scala @@ -0,0 +1,34 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.util + +/** + * An enumeration defining the levels available for logging. A level includes all of the levels + * with id larger than its own id. For example, Warn (id=3) includes Error (id=4). + */ +object Level extends Enumeration { + val Debug = Value(1, "debug") + val Info = Value(2, "info") + val Warn = Value(3, "warn") + val Error = Value(4, "error") + + /** + * Defines the label to use for success messages. + * Because the label for levels is defined in this module, the success label is also defined here. + */ + val SuccessLabel = "success" + + def union(a: Value, b: Value) = if (a.id < b.id) a else b + def unionAll(vs: Seq[Value]) = vs reduceLeft union + + /** Returns the level with the given name wrapped in Some, or None if no level exists for that name. */ + def apply(s: String) = values.find(s == _.toString) + + /** Same as apply, defined for use in pattern matching. */ + private[sbt] def unapply(s: String) = apply(s) +} diff --git a/internal/util-logging/src/main/scala/sbt/util/LogEvent.scala b/internal/util-logging/src/main/scala/sbt/util/LogEvent.scala new file mode 100644 index 000000000..28798feef --- /dev/null +++ b/internal/util-logging/src/main/scala/sbt/util/LogEvent.scala @@ -0,0 +1,21 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.util + +sealed trait LogEvent +final class Success(val msg: String) extends LogEvent +final class Log(val level: Level.Value, val msg: String) extends LogEvent +final class Trace(val exception: Throwable) extends LogEvent +final class SetLevel(val newLevel: Level.Value) extends LogEvent +final class SetTrace(val level: Int) extends LogEvent +final class SetSuccess(val enabled: Boolean) extends LogEvent +final class ControlEvent(val event: ControlEvent.Value, val msg: String) extends LogEvent + +object ControlEvent extends Enumeration { + val Start, Header, Finish = Value +} diff --git a/internal/util-logging/src/main/scala/sbt/util/LogExchange.scala b/internal/util-logging/src/main/scala/sbt/util/LogExchange.scala new file mode 100644 index 000000000..99ee76192 --- /dev/null +++ b/internal/util-logging/src/main/scala/sbt/util/LogExchange.scala @@ -0,0 +1,156 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.util + +import java.util.concurrent.ConcurrentHashMap +import org.apache.logging.log4j.{ LogManager => XLogManager, Level => XLevel } +import org.apache.logging.log4j.core.{ Appender => XAppender, LoggerContext => XLoggerContext } +import org.apache.logging.log4j.core.config.{ AppenderRef, LoggerConfig } +import org.apache.logging.log4j.core.layout.PatternLayout +import sbt.internal.util._ +import scala.collection.concurrent +import sjsonnew.JsonFormat +import org.apache.logging.log4j.core.appender.AsyncAppender + +// http://logging.apache.org/log4j/2.x/manual/customconfig.html +// https://logging.apache.org/log4j/2.x/log4j-core/apidocs/index.html + +sealed abstract class LogExchange { + private[sbt] lazy val context: XLoggerContext = init() + private[sbt] val stringCodecs: concurrent.Map[String, ShowLines[_]] = concurrent.TrieMap() + private[sbt] val builtInStringCodecs: Unit = initStringCodecs() + private[util] val configs = new ConcurrentHashMap[String, LoggerConfig] + private[util] def addConfig(name: String, config: LoggerConfig): Unit = + Util.ignoreResult(configs.putIfAbsent(name, config)) + private[util] def removeConfig(name: String): Option[LoggerConfig] = Option(configs.remove(name)) + + @deprecated("Use LoggerContext to create loggers", "1.4.0") + def logger(name: String): ManagedLogger = logger(name, None, None) + @deprecated("Use LoggerContext to create loggers", "1.4.0") + def logger(name: String, channelName: Option[String], execId: Option[String]): ManagedLogger = + LoggerContext.globalContext.logger(name, channelName, execId) + @deprecated("Use LoggerContext to unbind appenders", "1.4.0") + def unbindLoggerAppenders(loggerName: String): Unit = { + LoggerContext.globalContext.clearAppenders(loggerName) + } + @deprecated("Use LoggerContext to bind appenders", "1.4.0") + def bindLoggerAppenders( + loggerName: String, + appenders: List[(XAppender, Level.Value)] + ): Unit = { + appenders.foreach { + case (a, l) => + LoggerContext.globalContext + .addAppender(loggerName, new ConsoleAppenderFromLog4J(loggerName, a) -> l) + } + } + @deprecated("Use LoggerContext to bind appenders", "1.4.0") + def bindLoggerAppenders( + loggerName: String, + appenders: Seq[(Appender, Level.Value)] + ): Unit = bindLoggerAppenders(loggerName, appenders.map { case (a, l) => a.toLog4J -> l }.toList) + @deprecated("unused", "1.4.0") + def loggerConfig(loggerName: String): LoggerConfig = configs.get(loggerName) + + @deprecated("unused", "1.4.0") + lazy val asyncStdout = buildAsyncStdout + @deprecated("unused", "1.4.0") + private[sbt] def buildAsyncStdout: AsyncAppender = { + val ctx = XLogManager.getContext(false) match { case x: XLoggerContext => x } + val config = ctx.getConfiguration + val appender = ConsoleAppender("Stdout").toLog4J + // CustomConsoleAppender.createAppender("Stdout", layout, null, null) + appender.start + config.addAppender(appender) + val asyncAppender: AsyncAppender = AsyncAppender + .newBuilder() + .setName("AsyncStdout") + .setAppenderRefs(Array(AppenderRef.createAppenderRef("Stdout", XLevel.DEBUG, null))) + .setBlocking(false) + .setConfiguration(config) + .build + asyncAppender.start + config.addAppender(asyncAppender) + asyncAppender + } + + // Construct these StringTypeTags manually, because they're used at the very startup of sbt + // and we'll try not to initialize the universe by using the StringTypeTag.apply that requires a TypeTag + // A better long-term solution could be to make StringTypeTag.apply a macro. + lazy val stringTypeTagThrowable = StringTypeTag[Throwable]("scala.Throwable") + lazy val stringTypeTagTraceEvent = StringTypeTag[TraceEvent]("sbt.internal.util.TraceEvent") + lazy val stringTypeTagSuccessEvent = StringTypeTag[SuccessEvent]("sbt.internal.util.SuccessEvent") + + private[sbt] def initStringCodecs(): Unit = { + import sbt.internal.util.codec.ThrowableShowLines._ + import sbt.internal.util.codec.TraceEventShowLines._ + import sbt.internal.util.codec.SuccessEventShowLines._ + + registerStringCodecByStringTypeTag(stringTypeTagThrowable) + registerStringCodecByStringTypeTag(stringTypeTagTraceEvent) + registerStringCodecByStringTypeTag(stringTypeTagSuccessEvent) + } + + // This is a dummy layout to avoid casting error during PatternLayout.createDefaultLayout() + // that was originally used for ConsoleAppender. + // The stacktrace shows it's having issue initializing default DefaultConfiguration. + // Since we currently do not use Layout inside ConsoleAppender, the actual pattern is not relevant. + private[sbt] lazy val dummyLayout: PatternLayout = { + val _ = context + val ctx = XLogManager.getContext(false) match { case x: XLoggerContext => x } + val config = ctx.getConfiguration + val lo = PatternLayout.newBuilder + .withConfiguration(config) + .withPattern(PatternLayout.SIMPLE_CONVERSION_PATTERN) + .build + lo + } + + @deprecated("It is now necessary to provide a json format instance", "1.4.0") + def jsonCodec[A](tag: String): Option[JsonFormat[A]] = None + @deprecated("Always returns false", "1.4.0") + def hasJsonCodec(tag: String): Boolean = false + @deprecated("This is a no-op", "1.4.0") + def getOrElseUpdateJsonCodec[A](tag: String, v: JsonFormat[A]): JsonFormat[A] = v + @deprecated("The log manager no longer caches jsonCodecs", "1.4.0") + def jsonCodecs(): concurrent.Map[String, JsonFormat[_]] = concurrent.TrieMap.empty + + def stringCodec[A](tag: String): Option[ShowLines[A]] = + stringCodecs.get(tag) map { _.asInstanceOf[ShowLines[A]] } + def hasStringCodec(tag: String): Boolean = + stringCodecs.contains(tag) + def getOrElseUpdateStringCodec[A](tag: String, v: ShowLines[A]): ShowLines[A] = + stringCodecs.getOrElseUpdate(tag, v).asInstanceOf[ShowLines[A]] + + @deprecated("Prefer macro based registerStringCodec", "1.4.0") + def registerStringCodec[A]( + st: ShowLines[A], + tt: scala.reflect.runtime.universe.TypeTag[A] + ): Unit = { + registerStringCodecByStringTypeTag(StringTypeTag.apply[A](tt))(st) + } + private[sbt] def registerStringCodec[A: ShowLines: StringTypeTag]: Unit = { + registerStringCodecByStringTypeTag(implicitly[StringTypeTag[A]]) + } + + private[sbt] def registerStringCodecByStringTypeTag[A: ShowLines](tag: StringTypeTag[A]): Unit = { + val ev = implicitly[ShowLines[A]] + val _ = getOrElseUpdateStringCodec(tag.key, ev) + } + + private[sbt] def init(): XLoggerContext = { + import org.apache.logging.log4j.core.config.builder.api.ConfigurationBuilderFactory + import org.apache.logging.log4j.core.config.Configurator + val builder = ConfigurationBuilderFactory.newConfigurationBuilder + builder.setConfigurationName("sbt.util.logging") + val ctx = Configurator.initialize(builder.build()) + ctx match { case x: XLoggerContext => x } + } + private[sbt] def init(name: String): XLoggerContext = new XLoggerContext(name) +} +object LogExchange extends LogExchange diff --git a/internal/util-logging/src/main/scala/sbt/util/Logger.scala b/internal/util-logging/src/main/scala/sbt/util/Logger.scala new file mode 100644 index 000000000..135c92da4 --- /dev/null +++ b/internal/util-logging/src/main/scala/sbt/util/Logger.scala @@ -0,0 +1,127 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.util + +import xsbti.{ Logger => xLogger } +import xsbti.{ Position, Problem, Severity } + +import sys.process.ProcessLogger +import sbt.internal.util.{ BufferedLogger, FullLogger } +import java.io.File +import java.util.Optional +import java.util.function.Supplier + +/** + * This is intended to be the simplest logging interface for use by code that wants to log. + * It does not include configuring the logger. + */ +abstract class Logger extends xLogger { + final def verbose(message: => String): Unit = debug(message) + final def debug(message: => String): Unit = log(Level.Debug, message) + final def info(message: => String): Unit = log(Level.Info, message) + final def warn(message: => String): Unit = log(Level.Warn, message) + final def error(message: => String): Unit = log(Level.Error, message) + + // Added by sys.process.ProcessLogger + final def err(message: => String): Unit = log(Level.Error, message) + // sys.process.ProcessLogger + final def out(message: => String): Unit = log(Level.Info, message) + + @deprecated("No longer used.", "1.0.0") + def ansiCodesSupported: Boolean = false + + def trace(t: => Throwable): Unit + def success(message: => String): Unit + def log(level: Level.Value, message: => String): Unit + + def debug(msg: Supplier[String]): Unit = log(Level.Debug, msg) + def warn(msg: Supplier[String]): Unit = log(Level.Warn, msg) + def info(msg: Supplier[String]): Unit = log(Level.Info, msg) + def error(msg: Supplier[String]): Unit = log(Level.Error, msg) + def trace(msg: Supplier[Throwable]): Unit = trace(msg.get()) + def log(level: Level.Value, msg: Supplier[String]): Unit = log(level, msg.get) +} + +object Logger { + def transferLevels(oldLog: AbstractLogger, newLog: AbstractLogger): Unit = { + newLog.setLevel(oldLog.getLevel) + newLog.setTrace(oldLog.getTrace) + } + + val Null: AbstractLogger = new AbstractLogger { + def getLevel: Level.Value = Level.Error + def setLevel(newLevel: Level.Value): Unit = () + def getTrace: Int = 0 + def setTrace(flag: Int): Unit = () + def successEnabled: Boolean = false + def setSuccessEnabled(flag: Boolean): Unit = () + def control(event: ControlEvent.Value, message: => String): Unit = () + def logAll(events: Seq[LogEvent]): Unit = () + def trace(t: => Throwable): Unit = () + def success(message: => String): Unit = () + def log(level: Level.Value, message: => String): Unit = () + } + + implicit def absLog2PLog(log: AbstractLogger): ProcessLogger = + new BufferedLogger(log) with ProcessLogger + + implicit def log2PLog(log: Logger): ProcessLogger = absLog2PLog(new FullLogger(log)) + + implicit def xlog2Log(lg: xLogger): Logger = lg match { + case l: Logger => l + case _ => wrapXLogger(lg) + } + + private[this] def wrapXLogger(lg: xLogger): Logger = new Logger { + import InterfaceUtil.toSupplier + override def debug(msg: Supplier[String]): Unit = lg.debug(msg) + override def warn(msg: Supplier[String]): Unit = lg.warn(msg) + override def info(msg: Supplier[String]): Unit = lg.info(msg) + override def error(msg: Supplier[String]): Unit = lg.error(msg) + override def trace(msg: Supplier[Throwable]): Unit = lg.trace(msg) + override def log(level: Level.Value, msg: Supplier[String]): Unit = lg.log(level, msg) + def trace(t: => Throwable): Unit = trace(toSupplier(t)) + def success(s: => String): Unit = info(toSupplier(s)) + def log(level: Level.Value, msg: => String): Unit = { + val fmsg = toSupplier(msg) + level match { + case Level.Debug => lg.debug(fmsg) + case Level.Info => lg.info(fmsg) + case Level.Warn => lg.warn(fmsg) + case Level.Error => lg.error(fmsg) + } + } + } + + def jo2o[A](o: Optional[A]): Option[A] = InterfaceUtil.jo2o(o) + def o2jo[A](o: Option[A]): Optional[A] = InterfaceUtil.o2jo(o) + + @deprecated("Use InterfaceUtil.position", "1.2.2") + def position( + line0: Option[Integer], + content: String, + offset0: Option[Integer], + pointer0: Option[Integer], + pointerSpace0: Option[String], + sourcePath0: Option[String], + sourceFile0: Option[File] + ): Position = + InterfaceUtil.position( + line0, + content, + offset0, + pointer0, + pointerSpace0, + sourcePath0, + sourceFile0 + ) + + @deprecated("Use InterfaceUtil.problem", "1.2.2") + def problem(cat: String, pos: Position, msg: String, sev: Severity): Problem = + InterfaceUtil.problem(cat, pos, msg, sev) +} diff --git a/internal/util-logging/src/main/scala/sbt/util/LoggerContext.scala b/internal/util-logging/src/main/scala/sbt/util/LoggerContext.scala new file mode 100644 index 000000000..ee1069061 --- /dev/null +++ b/internal/util-logging/src/main/scala/sbt/util/LoggerContext.scala @@ -0,0 +1,191 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.util + +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.atomic.AtomicBoolean +import org.apache.logging.log4j.{ Level => XLevel } +import org.apache.logging.log4j.core.{ Appender => XAppender, LoggerContext => XLoggerContext } +import org.apache.logging.log4j.core.config.{ AppenderRef, LoggerConfig } +import sbt.internal.util._ +import scala.collection.JavaConverters._ +import org.apache.logging.log4j.core.config.AbstractConfiguration +import org.apache.logging.log4j.message.ObjectMessage + +/** + * Provides a context for generating loggers during task evaluation. The logger context + * can be initialized for a single command evaluation run and all of the resources + * created (such as cached logger appenders) can be cleaned up after task evaluation. + * This trait evolved out of LogExchange when it became clear that it was very difficult + * to manage the loggers and appenders without introducing memory leaks. + */ +sealed trait LoggerContext extends AutoCloseable { + def logger(name: String, channelName: Option[String], execId: Option[String]): ManagedLogger + def clearAppenders(loggerName: String): Unit + def addAppender( + loggerName: String, + appender: (Appender, Level.Value) + ): Unit + def appenders(loggerName: String): Seq[Appender] + def remove(name: String): Unit +} +object LoggerContext { + private[this] val useLog4J = System.getProperty("sbt.log.uselog4j", "false") == "true" + private[this] lazy val global = new LoggerContext.LoggerContextImpl + private[this] lazy val globalLog4J = new LoggerContext.Log4JLoggerContext(LogExchange.context) + private[sbt] lazy val globalContext = if (useLog4J) globalLog4J else global + private[util] class Log4JLoggerContext(val xlc: XLoggerContext) extends LoggerContext { + private val config = xlc.getConfiguration match { + case a: AbstractConfiguration => a + case _ => throw new IllegalStateException("") + } + val loggers = new java.util.Vector[String] + private[this] val closed = new AtomicBoolean(false) + override def logger( + name: String, + channelName: Option[String], + execId: Option[String] + ): ManagedLogger = { + if (closed.get) { + throw new IllegalStateException("Tried to create logger for closed LoggerContext") + } + val loggerConfig = LoggerConfig.createLogger( + false, + XLevel.DEBUG, + name, + // disable the calculation of caller location as it is very expensive + // https://issues.apache.org/jira/browse/LOG4J2-153 + "false", + Array[AppenderRef](), + null, + config, + null + ) + config.addLogger(name, loggerConfig) + val logger = xlc.getLogger(name) + LogExchange.addConfig(name, loggerConfig) + loggers.add(name) + val xlogger = new MiniLogger { + def log(level: Level.Value, message: => String): Unit = + logger.log( + ConsoleAppender.toXLevel(level), + new ObjectMessage(StringEvent(level.toString, message, channelName, execId)) + ) + def log[T](level: Level.Value, message: ObjectEvent[T]): Unit = + logger.log(ConsoleAppender.toXLevel(level), new ObjectMessage(message)) + } + new ManagedLogger(name, channelName, execId, xlogger, Some(Terminal.get), this) + } + override def clearAppenders(loggerName: String): Unit = { + val lc = config.getLoggerConfig(loggerName) + lc.getAppenders.asScala foreach { + case (name, a) => + a.stop() + lc.removeAppender(name) + } + } + override def addAppender( + loggerName: String, + appender: (Appender, Level.Value) + ): Unit = { + val lc = config.getLoggerConfig(loggerName) + appender match { + case (x: XAppender, lv) => lc.addAppender(x, ConsoleAppender.toXLevel(lv), null) + case (x, lv) => lc.addAppender(x.toLog4J, ConsoleAppender.toXLevel(lv), null) + } + } + override def appenders(loggerName: String): Seq[Appender] = { + val lc = config.getLoggerConfig(loggerName) + lc.getAppenders.asScala.collect { case (name, ca: ConsoleAppender) => ca }.toVector + } + override def remove(name: String): Unit = { + val lc = config.getLoggerConfig(name) + config.removeLogger(name) + } + def close(): Unit = if (closed.compareAndSet(false, true)) { + loggers.forEach(l => remove(l)) + loggers.clear() + } + } + private[util] class LoggerContextImpl extends LoggerContext { + private class Log extends MiniLogger { + private val consoleAppenders: java.util.Vector[(Appender, Level.Value)] = + new java.util.Vector + def log(level: Level.Value, message: => String): Unit = { + val toAppend = consoleAppenders.asScala.filter { case (a, l) => level.compare(l) >= 0 } + if (toAppend.nonEmpty) { + val m = message + toAppend.foreach { case (a, l) => a.appendLog(level, m) } + } + } + def log[T](level: Level.Value, message: ObjectEvent[T]): Unit = { + consoleAppenders.forEach { + case (a, l) => + if (level.compare(l) >= 0) a.appendObjectEvent(level, message) + } + } + def addAppender(newAppender: (Appender, Level.Value)): Unit = + Util.ignoreResult(consoleAppenders.add(newAppender)) + def clearAppenders(): Unit = { + consoleAppenders.forEach { case (a, _) => a.close() } + consoleAppenders.clear() + } + def appenders: Seq[Appender] = consoleAppenders.asScala.map(_._1).toVector + } + private[this] val loggers = new ConcurrentHashMap[String, Log] + private[this] val closed = new AtomicBoolean(false) + override def logger( + name: String, + channelName: Option[String], + execId: Option[String] + ): ManagedLogger = { + if (closed.get) { + throw new IllegalStateException("Tried to create logger for closed LoggerContext") + } + val xlogger = new Log + loggers.put(name, xlogger) + new ManagedLogger(name, channelName, execId, xlogger, Some(Terminal.get), this) + } + override def clearAppenders(loggerName: String): Unit = { + loggers.get(loggerName) match { + case null => + case l => l.clearAppenders() + } + } + override def addAppender( + loggerName: String, + appender: (Appender, Level.Value) + ): Unit = { + if (closed.get) { + throw new IllegalStateException("Tried to add appender for closed LoggerContext") + } + loggers.get(loggerName) match { + case null => + case l => l.addAppender(appender) + } + } + override def appenders(loggerName: String): Seq[Appender] = { + loggers.get(loggerName) match { + case null => Nil + case l => l.appenders + } + } + override def remove(name: String): Unit = { + loggers.remove(name) match { + case null => + case l => l.clearAppenders() + } + } + def close(): Unit = { + loggers.forEach((name, l) => l.clearAppenders()) + loggers.clear() + } + } + private[sbt] def apply(useLog4J: Boolean) = + if (useLog4J) new Log4JLoggerContext(LogExchange.context) else new LoggerContextImpl +} diff --git a/internal/util-logging/src/main/scala/sbt/util/ShowLines.scala b/internal/util-logging/src/main/scala/sbt/util/ShowLines.scala new file mode 100644 index 000000000..2f428086f --- /dev/null +++ b/internal/util-logging/src/main/scala/sbt/util/ShowLines.scala @@ -0,0 +1,22 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.util + +trait ShowLines[A] { + def showLines(a: A): Seq[String] +} +object ShowLines { + def apply[A](f: A => Seq[String]): ShowLines[A] = + new ShowLines[A] { + def showLines(a: A): Seq[String] = f(a) + } + + implicit class ShowLinesOp[A: ShowLines](a: A) { + def lines: Seq[String] = implicitly[ShowLines[A]].showLines(a) + } +} diff --git a/internal/util-logging/src/test/resources/log4j2.component.properties b/internal/util-logging/src/test/resources/log4j2.component.properties new file mode 100644 index 000000000..ee7c90784 --- /dev/null +++ b/internal/util-logging/src/test/resources/log4j2.component.properties @@ -0,0 +1 @@ +Log4jContextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector diff --git a/internal/util-logging/src/test/scala/Escapes.scala b/internal/util-logging/src/test/scala/Escapes.scala new file mode 100644 index 000000000..82d144d99 --- /dev/null +++ b/internal/util-logging/src/test/scala/Escapes.scala @@ -0,0 +1,150 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util + +import org.scalacheck._ +import Prop._ +import Gen.{ listOf, oneOf } + +import EscHelpers.{ ESC, hasEscapeSequence, isEscapeTerminator, removeEscapeSequences } + +object Escapes extends Properties("Escapes") { + property("genTerminator only generates terminators") = + forAllNoShrink(genTerminator)((c: Char) => isEscapeTerminator(c)) + + property("genWithoutTerminator only generates terminators") = + forAllNoShrink(genWithoutTerminator) { (s: String) => + s.forall(c => !isEscapeTerminator(c)) + } + + property("hasEscapeSequence is false when no escape character is present") = + forAllNoShrink(genWithoutEscape)((s: String) => !hasEscapeSequence(s)) + + property("hasEscapeSequence is true when escape character is present") = + forAllNoShrink(genWithRandomEscapes)((s: String) => hasEscapeSequence(s)) + + property("removeEscapeSequences is the identity when no escape character is present") = + forAllNoShrink(genWithoutEscape) { (s: String) => + val removed: String = removeEscapeSequences(s) + ("Escape sequence removed: '" + removed + "'") |: + (removed == s) + } + + property("No escape characters remain after removeEscapeSequences") = forAll { (s: String) => + val removed: String = removeEscapeSequences(s) + ("Escape sequence removed: '" + removed + "'") |: + !hasEscapeSequence(removed) + } + + private[this] final val ecs = ESC.toString + private val partialEscapeSequences = + Gen.oneOf(Gen const ecs, Gen const ecs ++ "[", Gen.choose('@', '_').map(ecs :+ _)) + + property("removeEscapeSequences handles partial escape sequences") = + forAll(partialEscapeSequences) { s => + val removed: String = removeEscapeSequences(s) + s"Escape sequence removed: '$removed'" |: !hasEscapeSequence(removed) + } + + property("removeEscapeSequences returns string without escape sequences") = + forAllNoShrink(genWithoutEscape, genEscapePairs) { + (start: String, escapes: List[EscapeAndNot]) => + val withEscapes: String = + start + escapes.map(ean => ean.escape.makeString + ean.notEscape).mkString("") + val removed: String = removeEscapeSequences(withEscapes) + val original = start + escapes.map(_.notEscape).mkString("") + val diffCharString = diffIndex(original, removed) + ("Input string : '" + withEscapes + "'") |: + ("Expected : '" + original + "'") |: + ("Escapes removed : '" + removed + "'") |: + (diffCharString) |: + (original == removed) + } + + def diffIndex(expect: String, original: String): String = { + var i = 0; + while (i < expect.length && i < original.length) { + if (expect.charAt(i) != original.charAt(i)) + return ("Differing character, idx: " + i + ", char: " + original.charAt(i) + + ", expected: " + expect.charAt(i)) + i += 1 + } + if (expect.length != original.length) return s"Strings are different lengths!" + "No differences found" + } + + final case class EscapeAndNot(escape: EscapeSequence, notEscape: String) { + override def toString = + s"EscapeAntNot(escape = [$escape], notEscape = [${notEscape.map(_.toInt)}])" + } + + // 2.10.5 warns on "implicit numeric widening" but it looks like a bug: https://issues.scala-lang.org/browse/SI-8450 + final case class EscapeSequence(content: String, terminator: Char) { + if (!content.isEmpty) { + assert( + content.tail.forall(c => !isEscapeTerminator(c)), + "Escape sequence content contains an escape terminator: '" + content + "'" + ) + assert( + (content.head == '[') || !isEscapeTerminator(content.head), + "Escape sequence content contains an escape terminator: '" + content.headOption + "'" + ) + } + assert(isEscapeTerminator(terminator)) + def makeString: String = ESC + content + terminator + + override def toString = + if (content.isEmpty) s"ESC (${terminator.toInt})" + else s"ESC ($content) (${terminator.toInt})" + } + + private[this] def noEscape(s: String): String = s.replace(ESC, ' ') + + lazy val genEscapeSequence: Gen[EscapeSequence] = + oneOf(genKnownSequence, genTwoCharacterSequence, genArbitraryEscapeSequence) + + lazy val genEscapePair: Gen[EscapeAndNot] = + for (esc <- genEscapeSequence; not <- genWithoutEscape) yield EscapeAndNot(esc, not) + + lazy val genEscapePairs: Gen[List[EscapeAndNot]] = listOf(genEscapePair) + + lazy val genArbitraryEscapeSequence: Gen[EscapeSequence] = + for (content <- genWithoutTerminator if !content.isEmpty; term <- genTerminator) + yield new EscapeSequence("[" + content, term) + + lazy val genKnownSequence: Gen[EscapeSequence] = + oneOf((misc ++ setGraphicsMode ++ setMode ++ resetMode).map(toEscapeSequence)) + + def toEscapeSequence(s: String): EscapeSequence = EscapeSequence(s.init, s.last) + + lazy val misc = Seq("14;23H", "5;3f", "2A", "94B", "19C", "85D", "s", "u", "2J", "K") + + lazy val setGraphicsMode: Seq[String] = + for (txt <- 0 to 8; fg <- 30 to 37; bg <- 40 to 47) + yield txt.toString + ";" + fg.toString + ";" + bg.toString + "m" + + lazy val resetMode = setModeLike('I') + lazy val setMode = setModeLike('h') + def setModeLike(term: Char): Seq[String] = (0 to 19).map(i => "=" + i.toString + term) + + lazy val genWithoutTerminator = + genRawString.map(_.filter(c => !isEscapeTerminator(c) && (c != '['))) + + lazy val genTwoCharacterSequence = + // 91 == [ which is the CSI escape sequence. + oneOf((64 to 95)) filter (_ != 91) map (c => new EscapeSequence("", c.toChar)) + + lazy val genTerminator: Gen[Char] = Gen.choose('@', '~') + lazy val genWithoutEscape: Gen[String] = genRawString.map(noEscape) + + def genWithRandomEscapes: Gen[String] = + for (ls <- listOf(genRawString); end <- genRawString) + yield ls.mkString("", ESC.toString, ESC.toString + end) + + private def genRawString = Arbitrary.arbString.arbitrary +} diff --git a/internal/util-logging/src/test/scala/LogExchangeSpec.scala b/internal/util-logging/src/test/scala/LogExchangeSpec.scala new file mode 100644 index 000000000..3a26b44ab --- /dev/null +++ b/internal/util-logging/src/test/scala/LogExchangeSpec.scala @@ -0,0 +1,42 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.util + +import sbt.internal.util._ + +import org.scalatest._ + +class LogExchangeSpec extends FlatSpec with Matchers { + import LogExchange._ + + checkTypeTag("stringTypeTagThrowable", stringTypeTagThrowable, StringTypeTag.fast[Throwable]) + checkTypeTag( + "stringTypeTagTraceEvent", + stringTypeTagTraceEvent, + StringTypeTag.fast[TraceEvent] + ) + checkTypeTag( + "stringTypeTagSuccessEvent", + stringTypeTagSuccessEvent, + StringTypeTag.fast[SuccessEvent] + ) + + private def checkTypeTag[A](name: String, inc: StringTypeTag[A], exp: StringTypeTag[A]): Unit = + s"LogExchange.$name" should s"match real StringTypeTag[$exp]" in { + val StringTypeTag(incomingString) = inc + val StringTypeTag(expectedString) = exp + if ((incomingString startsWith "scala.") || (expectedString startsWith "scala.")) { + // > historically [Scala] has been inconsistent whether `scala.` is included, or not + // > would it be hard to make the test accept either result? + // https://github.com/scala/community-builds/pull/758#issuecomment-409760633 + assert((incomingString stripPrefix "scala.") == (expectedString stripPrefix "scala.")) + } else { + assert(incomingString == expectedString) + } + } +} diff --git a/internal/util-logging/src/test/scala/LogWriterTest.scala b/internal/util-logging/src/test/scala/LogWriterTest.scala new file mode 100644 index 000000000..0fda82264 --- /dev/null +++ b/internal/util-logging/src/test/scala/LogWriterTest.scala @@ -0,0 +1,168 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util + +import sbt.util._ +import org.scalacheck._ +import Arbitrary._ +import Gen.{ listOfN, oneOf } +import Prop._ + +import java.io.Writer + +object LogWriterTest extends Properties("Log Writer") { + final val MaxLines = 100 + final val MaxSegments = 10 + + /* Tests that content written through a LoggerWriter is properly passed to the underlying Logger. + * Each line, determined by the specified newline separator, must be logged at the correct logging level. */ + property("properly logged") = forAll { (output: Output, newLine: NewLine) => + import output.{ lines, level } + val log = new RecordingLogger + val writer = new LoggerWriter(log, Some(level), newLine.str) + logLines(writer, lines, newLine.str) + val events = log.getEvents + ("Recorded:\n" + events.map(show).mkString("\n")) |: + check(toLines(lines), events, level) + } + + /** + * Displays a LogEvent in a useful format for debugging. In particular, we are only interested in `Log` types + * and non-printable characters should be escaped + */ + def show(event: LogEvent): String = + event match { + case l: Log => "Log('" + Escape(l.msg) + "', " + l.level + ")" + case _ => "Not Log" + } + + /** + * Writes the given lines to the Writer. `lines` is taken to be a list of lines, which are + * represented as separately written segments (ToLog instances). ToLog.`byCharacter` + * indicates whether to write the segment by character (true) or all at once (false) + */ + def logLines(writer: Writer, lines: List[List[ToLog]], newLine: String): Unit = { + for (line <- lines; section <- line) { + val content = section.content + val normalized = Escape.newline(content, newLine) + if (section.byCharacter) + normalized.foreach(c => writer.write(c.toInt)) + else + writer.write(normalized) + } + writer.flush() + } + + /** Converts the given lines in segments to lines as Strings for checking the results of the test.*/ + def toLines(lines: List[List[ToLog]]): List[String] = + lines.map(_.map(_.contentOnly).mkString) + + /** Checks that the expected `lines` were recorded as `events` at level `Lvl`.*/ + def check(lines: List[String], events: List[LogEvent], Lvl: Level.Value): Boolean = + (lines zip events) forall { + case (line, log: Log) => log.level == Lvl && line == log.msg + case _ => false + } + + /* The following are implicit generators to build up a write sequence. + * ToLog represents a written segment. NewLine represents one of the possible + * newline separators. A List[ToLog] represents a full line and always includes a + * final ToLog with a trailing '\n'. Newline characters are otherwise not present in + * the `content` of a ToLog instance.*/ + + implicit lazy val arbOut: Arbitrary[Output] = Arbitrary(genOutput) + implicit lazy val arbLog: Arbitrary[ToLog] = Arbitrary(genLog) + implicit lazy val arbLine: Arbitrary[List[ToLog]] = Arbitrary(genLine) + implicit lazy val arbNewLine: Arbitrary[NewLine] = Arbitrary(genNewLine) + implicit lazy val arbLevel: Arbitrary[Level.Value] = Arbitrary(genLevel) + + implicit def genLine(implicit logG: Gen[ToLog]): Gen[List[ToLog]] = + for (l <- listOf[ToLog](MaxSegments); last <- logG) + yield (addNewline(last) :: l.filter(!_.content.isEmpty)).reverse + + implicit def genLog(implicit content: Arbitrary[String], byChar: Arbitrary[Boolean]): Gen[ToLog] = + for (c <- content.arbitrary; by <- byChar.arbitrary) yield { + assert(c != null) + new ToLog(removeNewlines(c), by) + } + + implicit lazy val genNewLine: Gen[NewLine] = + for (str <- oneOf("\n", "\r", "\r\n")) yield new NewLine(str) + + implicit lazy val genLevel: Gen[Level.Value] = + oneOf(Level.values.toSeq) + + implicit lazy val genOutput: Gen[Output] = + for (ls <- listOf[List[ToLog]](MaxLines); lv <- genLevel) yield new Output(ls, lv) + + def removeNewlines(s: String) = s.replaceAll("""[\n\r]+""", "") + def addNewline(l: ToLog): ToLog = + new ToLog(l.content + "\n", l.byCharacter) // \n will be replaced by a random line terminator for all lines + + def listOf[T](max: Int)(implicit content: Arbitrary[T]): Gen[List[T]] = + Gen.choose(0, max) flatMap (sz => listOfN(sz, content.arbitrary)) +} + +/* Helper classes*/ + +final class Output(val lines: List[List[ToLog]], val level: Level.Value) { + override def toString = + "Level: " + level + "\n" + lines.map(_.mkString).mkString("\n") +} + +final class NewLine(val str: String) { + override def toString = Escape(str) +} + +final class ToLog(val content: String, val byCharacter: Boolean) { + def contentOnly = Escape.newline(content, "") + + override def toString = + if (content.isEmpty) "" else "ToLog('" + Escape(contentOnly) + "', " + byCharacter + ")" +} + +/** Defines some utility methods for escaping unprintable characters.*/ +object Escape { + + /** Escapes characters with code less than 20 by printing them as unicode escapes.*/ + def apply(s: String): String = { + val builder = new StringBuilder(s.length) + for (c <- s) { + val char = c.toInt + def escaped = pad(char.toHexString.toUpperCase, 4, '0') + if (c < 20) builder.append("\\u").append(escaped) else builder.append(c) + } + builder.toString + } + + def pad(s: String, minLength: Int, extra: Char) = { + val diff = minLength - s.length + if (diff <= 0) s else List.fill(diff)(extra).mkString("", "", s) + } + + /** Replaces a \n character at the end of a string `s` with `nl`.*/ + def newline(s: String, nl: String): String = + if (s.endsWith("\n")) s.substring(0, s.length - 1) + nl else s + +} + +/** Records logging events for later retrieval.*/ +final class RecordingLogger extends BasicLogger { + private var events: List[LogEvent] = Nil + + def getEvents = events.reverse + + override def ansiCodesSupported = true + def trace(t: => Throwable): Unit = { events ::= new Trace(t) } + def log(level: Level.Value, message: => String): Unit = { events ::= new Log(level, message) } + def success(message: => String): Unit = { events ::= new Success(message) } + def logAll(es: Seq[LogEvent]): Unit = { events :::= es.toList } + + def control(event: ControlEvent.Value, message: => String): Unit = + events ::= new ControlEvent(event, message) +} diff --git a/internal/util-logging/src/test/scala/ManagedLoggerSpec.scala b/internal/util-logging/src/test/scala/ManagedLoggerSpec.scala new file mode 100644 index 000000000..9d617662f --- /dev/null +++ b/internal/util-logging/src/test/scala/ManagedLoggerSpec.scala @@ -0,0 +1,146 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util + +import org.scalatest._ +import sbt.util._ +import java.io.{ File, PrintWriter } +import sbt.io.Using +import scala.annotation.nowarn + +class ManagedLoggerSpec extends FlatSpec with Matchers { + val context = LoggerContext(useLog4J = true) + @nowarn + val asyncStdout = new ConsoleAppenderFromLog4J("asyncStdout", LogExchange.asyncStdout) + def newLogger(name: String): ManagedLogger = context.logger(name, None, None) + "ManagedLogger" should "log to console" in { + val log = newLogger("foo") + context.addAppender("foo", asyncStdout -> Level.Info) + log.info("test") + log.debug("test") + } + + it should "support event logging" in { + import sjsonnew.BasicJsonProtocol._ + val log = newLogger("foo") + context.addAppender("foo", asyncStdout -> Level.Info) + log.infoEvent(1) + } + + it should "validate performance improvement of disabling location calculation for async loggers" in { + val log = newLogger("foo") + context.addAppender("foo", asyncStdout -> Level.Info) + val before = System.currentTimeMillis() + 1 to 10000 foreach { _ => + log.debug("test") + } + val after = System.currentTimeMillis() + + log.info(s"Peformance test took: ${after - before}ms") + } + + it should "support logging Throwable out of the box" in { + import sbt.internal.util.codec.JsonProtocol._ + val log = newLogger("foo") + context.addAppender("foo", asyncStdout -> Level.Info) + log.infoEvent(SuccessEvent("yes")) + } + + it should "allow registering Show[Int]" in { + import sjsonnew.BasicJsonProtocol._ + val log = newLogger("foo") + context.addAppender("foo", asyncStdout -> Level.Info) + implicit val intShow: ShowLines[Int] = + ShowLines((x: Int) => Vector(s"String representation of $x")) + log.registerStringCodec[Int] + log.infoEvent(1) + } + + it should "allow registering Show[Array[Int]]" in { + import sjsonnew.BasicJsonProtocol._ + val log = newLogger("foo") + context.addAppender("foo", asyncStdout -> Level.Info) + implicit val intArrayShow: ShowLines[Array[Int]] = + ShowLines((x: Array[Int]) => Vector(s"String representation of ${x.mkString}")) + log.registerStringCodec[Array[Int]] + log.infoEvent(Array(1, 2, 3)) + } + + it should "allow registering Show[Vector[Vector[Int]]]" in { + import sjsonnew.BasicJsonProtocol._ + val log = newLogger("foo") + context.addAppender("foo", asyncStdout -> Level.Info) + implicit val intVectorShow: ShowLines[Vector[Vector[Int]]] = + ShowLines((xss: Vector[Vector[Int]]) => Vector(s"String representation of $xss")) + log.registerStringCodec[Vector[Vector[Int]]] + log.infoEvent(Vector(Vector(1, 2, 3))) + } + + it should "be thread safe" in { + import java.util.concurrent.{ Executors, TimeUnit } + val pool = Executors.newFixedThreadPool(100) + for { + i <- 1 to 10000 + } { + pool.submit(new Runnable { + def run(): Unit = { + val stringTypeTag = StringTypeTag.fast[List[Int]] + val log = newLogger(s"foo$i") + context.addAppender(s"foo$i", asyncStdout -> Level.Info) + if (i % 100 == 0) { + log.info(s"foo$i test $stringTypeTag") + } + Thread.sleep(1) + } + }) + } + pool.shutdown + pool.awaitTermination(30, TimeUnit.SECONDS) + } + + "global logging" should "log immediately after initialization" in { + // this is passed into State normally + val global0 = initialGlobalLogging + val full = global0.full + (1 to 3).toList foreach { x => + full.info(s"test$x") + } + } + + // This is done in Mainloop.scala + it should "create a new backing with newAppender" in { + val global0 = initialGlobalLogging + val logBacking0 = global0.backing + val global1 = Using.fileWriter(append = true)(logBacking0.file) { writer => + val out = new PrintWriter(writer) + val g = global0.newAppender(global0.full, out, logBacking0, context) + val full = g.full + (1 to 3).toList foreach (x => full.info(s"newAppender $x")) + assert(logBacking0.file.exists) + g + } + val logBacking1 = global1.backing + Using.fileWriter(append = true)(logBacking1.file) { writer => + val out = new PrintWriter(writer) + val g = global1.newAppender(global1.full, out, logBacking1, context) + val full = g.full + (1 to 3).toList foreach (x => full.info(s"newAppender $x")) + // println(logBacking.file) + // print("Press enter to continue. ") + // System.console.readLine + assert(logBacking1.file.exists) + } + } + + val console = ConsoleOut.systemOut + def initialGlobalLogging: GlobalLogging = GlobalLogging.initial( + MainAppender.globalDefault(console), + File.createTempFile("sbt", ".log"), + console + ) +} diff --git a/internal/util-logging/src/test/scala/TestLogger.scala b/internal/util-logging/src/test/scala/TestLogger.scala new file mode 100644 index 000000000..45034ab10 --- /dev/null +++ b/internal/util-logging/src/test/scala/TestLogger.scala @@ -0,0 +1,18 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util + +import sbt.util._ + +object TestLogger { + def apply[T](f: Logger => T): T = { + val log = new BufferedLogger(ConsoleLogger()) + log.setLevel(Level.Debug) + log.bufferQuietly(f(log)) + } +} diff --git a/internal/util-logging/src/test/scala/sbt/internal/util/CleanStringSpec.scala b/internal/util-logging/src/test/scala/sbt/internal/util/CleanStringSpec.scala new file mode 100644 index 000000000..f734923a6 --- /dev/null +++ b/internal/util-logging/src/test/scala/sbt/internal/util/CleanStringSpec.scala @@ -0,0 +1,99 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util + +import org.scalatest.FlatSpec + +class CleanStringSpec extends FlatSpec { + "EscHelpers" should "not modify normal strings" in { + val cleanString = s"1234" + assert(EscHelpers.stripColorsAndMoves(cleanString) == cleanString) + } + it should "remove delete lines" in { + val clean = "1234" + val string = s"${ConsoleAppender.DeleteLine}$clean" + assert(EscHelpers.stripColorsAndMoves(string) == clean) + } + it should "remove cursor left" in { + val clean = "1234" + val backspaced = s"1235${ConsoleAppender.cursorLeft(1)}${ConsoleAppender.clearLine(0)}4" + assert(EscHelpers.stripColorsAndMoves(backspaced) == clean) + } + it should "remove colors" in { + val clean = "1234" + val colored = s"${scala.Console.RED}$clean${scala.Console.RESET}" + assert(EscHelpers.stripColorsAndMoves(colored) == clean) + } + it should "remove backspaces" in { + // Taken from an actual failure case. In the scala client, type 'clean', then type backspace + // five times to clear 'clean' and then retype 'clean'. + val bytes = Array[Byte](27, 91, 50, 75, 27, 91, 48, 74, 27, 91, 50, 75, 27, 91, 49, 48, 48, 48, + 68, 115, 98, 116, 58, 115, 99, 97, 108, 97, 45, 99, 111, 109, 112, 105, 108, 101, 27, 91, 51, + 54, 109, 62, 32, 27, 91, 48, 109, 99, 108, 101, 97, 110, 8, 27, 91, 75, 110) + val str = new String(bytes) + assert(EscHelpers.stripColorsAndMoves(str) == "sbt:scala-compile> clean") + } + it should "handle cursor left overwrite" in { + val clean = "1234" + val backspaced = s"1235${8.toChar}4${8.toChar}" + assert(EscHelpers.stripColorsAndMoves(backspaced) == clean) + } + it should "remove moves in string with only moves" in { + val original = + Array[Byte](27, 91, 50, 75, 27, 91, 51, 65, 27, 91, 49, 48, 48, 48, 68) + val (bytes, len) = EscHelpers.strip(original, stripAnsi = true, stripColor = true) + assert(len == 0) + } + it should "remove moves in string with moves and letters" in { + val original = + Array[Byte](27, 91, 50, 75, 27, 91, 51, 65) ++ "foo".getBytes ++ Array[Byte](27, 91, 49, 48, + 48, 48, 68) + val (bytes, len) = EscHelpers.strip(original, stripAnsi = true, stripColor = true) + assert(new String(bytes, 0, len) == "foo") + } + it should "preserve colors" in { + val original = + Array[Byte](27, 91, 49, 48, 48, 48, 68, 27, 91, 48, 74, 102, 111, 111, 27, 91, 51, 54, 109, + 62, 32, 27, 91, 48, 109) + // this is taken from an sbt prompt that looks like "foo> " with the > rendered blue + val colorArrow = new String(Array[Byte](27, 91, 51, 54, 109, 62)) + val (bytes, len) = EscHelpers.strip(original, stripAnsi = true, stripColor = false) + assert(new String(bytes, 0, len) == "foo" + colorArrow + " " + scala.Console.RESET) + } + it should "remove unusual escape characters" in { + val original = new String( + Array[Byte](27, 91, 63, 49, 108, 27, 62, 27, 91, 63, 49, 48, 48, 48, 108, 27, 91, 63, 50, 48, + 48, 52, 108) + ) + assert(EscHelpers.stripColorsAndMoves(original).isEmpty) + } + it should "remove bracketed paste csi" in { + // taken from a test project prompt + val original = + Array[Byte](27, 91, 63, 50, 48, 48, 52, 104, 115, 98, 116, 58, 114, 101, 112, 114, 111, 62, + 32) + val (bytes, len) = EscHelpers.strip(original, stripAnsi = true, stripColor = false) + assert(new String(bytes, 0, len) == "sbt:repro> ") + } + it should "strip colors" in { + // taken from utest output + val original = + Array[Byte](91, 105, 110, 102, 111, 93, 32, 27, 91, 51, 50, 109, 43, 27, 91, 51, 57, 109, 32, + 99, 111, 109, 46, 97, 99, 109, 101, 46, 67, 111, 121, 111, 116, 101, 84, 101, 115, 116, 46, + 109, 97, 107, 101, 84, 114, 97, 112, 32, 27, 91, 50, 109, 57, 109, 115, 27, 91, 48, 109, 32, + 32, 27, 91, 48, 74, 10) + val (bytes, len) = EscHelpers.strip(original, stripAnsi = false, stripColor = true) + val expected = "[info] + com.acme.CoyoteTest.makeTrap 9ms " + + new String(Array[Byte](27, 91, 48, 74, 10)) + assert(new String(bytes, 0, len) == expected) + + val (bytes2, len2) = EscHelpers.strip(original, stripAnsi = true, stripColor = true) + val expected2 = "[info] + com.acme.CoyoteTest.makeTrap 9ms \n" + assert(new String(bytes2, 0, len2) == expected2) + } +} diff --git a/internal/util-logging/src/test/scala/sbt/internal/util/UTF8DecoderSpec.scala b/internal/util-logging/src/test/scala/sbt/internal/util/UTF8DecoderSpec.scala new file mode 100644 index 000000000..957a95b4e --- /dev/null +++ b/internal/util-logging/src/test/scala/sbt/internal/util/UTF8DecoderSpec.scala @@ -0,0 +1,41 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util + +import java.io.InputStream +import java.nio.charset.Charset +import org.scalatest.FlatSpec +import java.util.concurrent.LinkedBlockingQueue + +class UTF8DecoderSpec extends FlatSpec { + val decoder = Charset.forName("UTF-8").newDecoder + "ascii characters" should "not be modified" in { + val inputStream = new InputStream { + override def read(): Int = 'c'.toInt + } + assert(JLine3.decodeInput(decoder, inputStream) == 'c'.toInt) + } + "swedish characters" should "be handled" in { + val bytes = new LinkedBlockingQueue[Int] + // these are the utf-8 codes for an umlauted a in swedish + Seq(195, 164).foreach(b => bytes.put(b)) + val inputStream = new InputStream { + override def read(): Int = Option(bytes.poll).getOrElse(-1) + } + assert(JLine3.decodeInput(decoder, inputStream) == 228) + } + "emoji" should "be handled" in { + val bytes = new LinkedBlockingQueue[Int] + // laughing and crying emoji in utf8 + Seq(0xF0, 0x9F, 0x98, 0x82).foreach(b => bytes.put(b)) + val inputStream = new InputStream { + override def read(): Int = Option(bytes.poll).getOrElse(-1) + } + assert(JLine3.decodeInput(decoder, inputStream) == 128514) + } +} diff --git a/internal/util-logic/src/main/scala/sbt/internal/util/logic/Logic.scala b/internal/util-logic/src/main/scala/sbt/internal/util/logic/Logic.scala new file mode 100644 index 000000000..e508022bb --- /dev/null +++ b/internal/util-logic/src/main/scala/sbt/internal/util/logic/Logic.scala @@ -0,0 +1,408 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util +package logic + +import scala.annotation.{ nowarn, tailrec } +import Formula.{ And, True } + +/* +Defines a propositional logic with negation as failure and only allows stratified rule sets +(negation must be acyclic) in order to have a unique minimal model. + +For example, this is not allowed: + + p :- not q + + q :- not p +but this is: + + p :- q + + q :- p +as is this: + + p :- q + + q := not r + + + Some useful links: + + https://en.wikipedia.org/wiki/Nonmonotonic_logic + + https://en.wikipedia.org/wiki/Negation_as_failure + + https://en.wikipedia.org/wiki/Propositional_logic + + https://en.wikipedia.org/wiki/Stable_model_semantics + + http://www.w3.org/2005/rules/wg/wiki/negation + */ + +/** Disjunction (or) of the list of clauses. */ +final case class Clauses(clauses: List[Clause]) { + assert(clauses.nonEmpty, "At least one clause is required.") + override def toString: String = + s"Clauses(${clauses.mkString("\n")})" +} + +/** When the `body` Formula succeeds, atoms in `head` are true. */ +final case class Clause(body: Formula, head: Set[Atom]) + +/** A literal is an [[Atom]] or its negation ([[Negated]]). */ +sealed abstract class Literal extends Formula { + + /** The underlying (positive) atom. */ + def atom: Atom + + /** Negates this literal.*/ + def unary_! : Literal + +} + +/** A variable with name `label`. */ +final case class Atom(label: String) extends Literal { + def atom = this + def unary_! : Negated = Negated(this) +} + +/** + * A negated atom, in the sense of negation as failure, not logical negation. + * That is, it is true if `atom` is not known/defined. + */ +final case class Negated(atom: Atom) extends Literal { + def unary_! : Atom = atom +} + +/** + * A formula consists of variables, negation, and conjunction (and). + * (Disjunction is not currently included- it is modeled at the level of a sequence of clauses. + * This is less convenient when defining clauses, but is not less powerful.) + */ +sealed abstract class Formula { + + /** Constructs a clause that proves `atoms` when this formula is true. */ + def proves(atom: Atom, atoms: Atom*): Clause = Clause(this, (atom +: atoms).toSet) + + /** Constructs a formula that is true iff this formula and `f` are both true.*/ + def &&(f: Formula): Formula = (this, f) match { + case (True, x) => x + case (x, True) => x + case (And(as), And(bs)) => And(as ++ bs) + case (And(as), b: Literal) => And(as + b) + case (a: Literal, And(bs)) => And(bs + a) + case (a: Literal, b: Literal) => And(Set(a, b)) + } + +} + +object Formula { + + /** A conjunction of literals. */ + final case class And(literals: Set[Literal]) extends Formula { + assert(literals.nonEmpty, "'And' requires at least one literal.") + } + + final case object True extends Formula + +} + +object Logic { + def reduceAll( + clauses: List[Clause], + initialFacts: Set[Literal] + ): Either[LogicException, Matched] = + reduce(Clauses(clauses), initialFacts) + + /** + * Computes the variables in the unique stable model for the program represented by `clauses` and + * `initialFacts`. `clause` may not have any negative feedback (that is, negation is acyclic) + * and `initialFacts` cannot be in the head of any clauses in `clause`. + * These restrictions ensure that the logic program has a unique minimal model. + */ + def reduce(clauses: Clauses, initialFacts: Set[Literal]): Either[LogicException, Matched] = { + val (posSeq, negSeq) = separate(initialFacts.toSeq) + val (pos, neg) = (posSeq.toSet, negSeq.toSet) + + val problem = + (checkContradictions(pos, neg): Option[LogicException]) orElse + (checkOverlap(clauses, pos): Option[LogicException]) + // orElse + // (checkAcyclic(clauses): Option[LogicException]) + problem.toLeft( + reduce0(clauses, initialFacts, Matched.empty) + ) + } + + /** + * Verifies `initialFacts` are not in the head of any `clauses`. + * This avoids the situation where an atom is proved but no clauses prove it. + * This isn't necessarily a problem, but the main sbt use cases expects + * a proven atom to have at least one clause satisfied. + */ + private[this] def checkOverlap( + clauses: Clauses, + initialFacts: Set[Atom] + ): Option[InitialOverlap] = { + val as = atoms(clauses) + val initialOverlap = initialFacts.filter(as.inHead) + if (initialOverlap.nonEmpty) Some(new InitialOverlap(initialOverlap)) else None + } + + private[this] def checkContradictions( + pos: Set[Atom], + neg: Set[Atom] + ): Option[InitialContradictions] = { + val contradictions = pos intersect neg + if (contradictions.nonEmpty) Some(new InitialContradictions(contradictions)) else None + } + + @nowarn + private[this] def checkAcyclic(clauses: Clauses): Option[CyclicNegation] = { + val deps = dependencyMap(clauses) + // println(s"deps = $deps") + // println(s"graph(deps) = ${graph(deps)}") + val cycle = Dag.findNegativeCycle(graph(deps)) + if (cycle.nonEmpty) Some(new CyclicNegation(cycle)) else None + } + + private[this] def graph(deps: Map[Atom, Set[Literal]]) = new Dag.DirectedSignedGraph[Atom] { + type Arrow = Literal + def nodes = deps.keys.toList + def dependencies(a: Atom) = deps.getOrElse(a, Set.empty).toList + + def isNegative(b: Literal) = b match { + case Negated(_) => true + case Atom(_) => false + } + + def head(b: Literal) = b.atom + override def toString(): String = + nodes + .flatMap(n => List(n) ++ dependencies(n).map(d => s"$n -> $d")) + .mkString("{\n", "\n", "\n}") + } + + private[this] def dependencyMap(clauses: Clauses): Map[Atom, Set[Literal]] = + clauses.clauses.foldLeft(Map.empty[Atom, Set[Literal]]) { + case (m, Clause(formula, heads)) => + val deps = literals(formula) + heads.foldLeft(m) { (n, head) => + n.updated(head, n.getOrElse(head, Set.empty) ++ deps) + } + } + + sealed abstract class LogicException(override val toString: String) + + final class InitialContradictions(val literals: Set[Atom]) + extends LogicException( + "Initial facts cannot be both true and false:\n\t" + literals.mkString("\n\t") + ) + + final class InitialOverlap(val literals: Set[Atom]) + extends LogicException( + "Initial positive facts cannot be implied by any clauses:\n\t" + literals.mkString("\n\t") + ) + + final class CyclicNegation(val cycle: List[Literal]) + extends LogicException( + "Negation may not be involved in a cycle:\n\t" + cycle.mkString("\n\t") + ) + + /** Tracks proven atoms in the reverse order they were proved. */ + final class Matched private (val provenSet: Set[Atom], reverseOrdered: List[Atom]) { + def add(atoms: Set[Atom]): Matched = add(atoms.toList) + + def add(atoms: List[Atom]): Matched = { + val newOnly = atoms.filterNot(provenSet) + new Matched(provenSet ++ newOnly.toSet, newOnly ::: reverseOrdered) + } + + def ordered: List[Atom] = reverseOrdered.reverse + override def toString = ordered.map(_.label).mkString("Matched(", ",", ")") + } + + object Matched { + val empty = new Matched(Set.empty, Nil) + } + + /** Separates a sequence of literals into `(pos, neg)` atom sequences. */ + private[this] def separate(lits: Seq[Literal]): (Seq[Atom], Seq[Atom]) = + Util.separate(lits) { + case a: Atom => Left(a) + case Negated(n) => Right(n) + } + + /** + * Finds clauses that have no body and thus prove their head. + * Returns `(, )`. + */ + private[this] def findProven(c: Clauses): (Set[Atom], List[Clause]) = { + val (proven, unproven) = c.clauses.partition(_.body == True) + (proven.flatMap(_.head).toSet, unproven) + } + + private[this] def keepPositive(lits: Set[Literal]): Set[Atom] = + lits.collect { case a: Atom => a }.toSet + + // precondition: factsToProcess contains no contradictions + @tailrec private[this] def reduce0( + clauses: Clauses, + factsToProcess: Set[Literal], + state: Matched + ): Matched = + applyAll(clauses, factsToProcess) match { + case None => state // all of the remaining clauses failed on the new facts + case Some(applied) => + val (proven, unprovenClauses) = findProven(applied) + val processedFacts = state add keepPositive(factsToProcess) + val newlyProven = proven -- processedFacts.provenSet + val newState = processedFacts add newlyProven + if (unprovenClauses.isEmpty) + newState // no remaining clauses, done. + else { + val unproven = Clauses(unprovenClauses) + val nextFacts: Set[Literal] = + if (newlyProven.nonEmpty) newlyProven.toSet[Literal] + else inferFailure(unproven) + reduce0(unproven, nextFacts, newState) + } + } + + /** + * Finds negated atoms under the negation as failure rule and returns them. + * This should be called only after there are no more known atoms to be substituted. + */ + private[this] def inferFailure(clauses: Clauses): Set[Literal] = { + /* At this point, there is at least one clause and one of the following is the case as the + result of the acyclic negation rule: + i. there is at least one variable that occurs in a clause body but not in the head of a + clause + ii. there is at least one variable that occurs in the head of a clause and does not + transitively depend on a negated variable + + In either case, each such variable x cannot be proven true and therefore proves 'not x' + (negation as failure, !x in the code). + */ + val allAtoms = atoms(clauses) + val newFacts: Set[Literal] = negated(allAtoms.triviallyFalse) + if (newFacts.nonEmpty) + newFacts + else { + val possiblyTrue = hasNegatedDependency(clauses.clauses, Relation.empty, Relation.empty) + val newlyFalse: Set[Literal] = negated(allAtoms.inHead -- possiblyTrue) + if (newlyFalse.nonEmpty) + newlyFalse + else // should never happen due to the acyclic negation rule + sys.error(s"No progress:\n\tclauses: $clauses\n\tpossibly true: $possiblyTrue") + } + } + + private[this] def negated(atoms: Set[Atom]): Set[Literal] = atoms.map(a => (Negated(a): Literal)) + + /** + * Computes the set of atoms in `clauses` that directly or transitively take a negated atom as input. + * For example, for the following clauses, this method would return `List(a, d)` : + * a :- b, not c + * d :- a + */ + @tailrec + def hasNegatedDependency( + clauses: Seq[Clause], + posDeps: Relation[Atom, Atom], + negDeps: Relation[Atom, Atom] + ): List[Atom] = + clauses match { + case Seq() => + // because cycles between positive literals are allowed, this isn't strictly a topological sort + Dag.topologicalSortUnchecked(negDeps._1s)(posDeps.reverse) + + case Clause(formula, head) +: tail => + // collect direct positive and negative literals and track them in separate graphs + val (pos, neg) = directDeps(formula) + val (newPos, newNeg) = head.foldLeft((posDeps, negDeps)) { + case ((pdeps, ndeps), d) => + (pdeps.+(d, pos), ndeps.+(d, neg)) + } + hasNegatedDependency(tail, newPos, newNeg) + } + + /** Computes the `(positive, negative)` literals in `formula`. */ + private[this] def directDeps(formula: Formula): (Seq[Atom], Seq[Atom]) = + Util.separate(literals(formula).toSeq) { + case Negated(a) => Right(a) + case a: Atom => Left(a) + } + + private[this] def literals(formula: Formula): Set[Literal] = formula match { + case And(lits) => lits + case l: Literal => Set(l) + case True => Set.empty + } + + /** Computes the atoms in the heads and bodies of the clauses in `clause`. */ + def atoms(cs: Clauses): Atoms = cs.clauses.map(c => Atoms(c.head, atoms(c.body))).reduce(_ ++ _) + + /** Computes the set of all atoms in `formula`. */ + def atoms(formula: Formula): Set[Atom] = formula match { + case And(lits) => lits.map(_.atom) + case Negated(lit) => Set(lit) + case a: Atom => Set(a) + case True => Set() + } + + /** Represents the set of atoms in the heads of clauses and in the bodies (formulas) of clauses. */ + final case class Atoms(inHead: Set[Atom], inFormula: Set[Atom]) { + + /** Concatenates this with `as`. */ + def ++(as: Atoms): Atoms = Atoms(inHead ++ as.inHead, inFormula ++ as.inFormula) + + /** Atoms that cannot be true because they do not occur in a head. */ + def triviallyFalse: Set[Atom] = inFormula -- inHead + + } + + /** + * Applies known facts to `clause`s, deriving a new, possibly empty list of clauses. + * 1. If a fact is in the body of a clause, the derived clause has that fact removed from the body. + * 2. If the negation of a fact is in a body of a clause, that clause fails and is removed. + * 3. If a fact or its negation is in the head of a clause, the derived clause has that fact (or its negation) removed from the head. + * 4. If a head is empty, the clause proves nothing and is removed. + * + * NOTE: empty bodies do not cause a clause to succeed yet. + * All known facts must be applied before this can be done in order to avoid inconsistencies. + * Precondition: no contradictions in `facts` + * Postcondition: no atom in `facts` is present in the result + * Postcondition: No clauses have an empty head + */ + def applyAll(cs: Clauses, facts: Set[Literal]): Option[Clauses] = { + val newClauses = + if (facts.isEmpty) + cs.clauses.filter(_.head.nonEmpty) // still need to drop clauses with an empty head + else + cs.clauses.map(c => applyAll(c, facts)).flatMap(_.toList) + if (newClauses.isEmpty) None else Some(Clauses(newClauses)) + } + + def applyAll(c: Clause, facts: Set[Literal]): Option[Clause] = { + val atoms = facts.map(_.atom) + val newHead = c.head -- atoms // 3. + if (newHead.isEmpty) // 4. empty head + None + else + substitute(c.body, facts).map(f => Clause(f, newHead)) // 1, 2 + } + + /** Derives the formula that results from substituting `facts` into `formula`. */ + @tailrec def substitute(formula: Formula, facts: Set[Literal]): Option[Formula] = formula match { + case And(lits) => + def negated(lits: Set[Literal]): Set[Literal] = lits.map(a => !a) + if (lits.exists(negated(facts))) // 2. + None + else { + val newLits = lits -- facts + val newF = + if (newLits.isEmpty) (True: Formula) + else (And(newLits): Formula) + Some(newF) // 1. + } + case True => Some(True) + case lit: Literal => // define in terms of And + substitute(And(Set(lit)), facts) + } +} diff --git a/internal/util-logic/src/test/scala/sbt/logic/Test.scala b/internal/util-logic/src/test/scala/sbt/logic/Test.scala new file mode 100644 index 000000000..1a2046381 --- /dev/null +++ b/internal/util-logic/src/test/scala/sbt/logic/Test.scala @@ -0,0 +1,128 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util +package logic + +import org.scalacheck._ +import Prop.secure +import Logic.{ LogicException, Matched } + +object LogicTest extends Properties("Logic") { + import TestClauses._ + + property("Handles trivial resolution.") = secure(expect(trivial, Set(A))) + property("Handles less trivial resolution.") = secure(expect(lessTrivial, Set(B, A, D))) + property("Handles cycles without negation") = secure(expect(cycles, Set(F, A, B))) + property("Handles basic exclusion.") = secure(expect(excludedPos, Set())) + property("Handles exclusion of head proved by negation.") = secure(expect(excludedNeg, Set())) + // TODO: actually check ordering, probably as part of a check that dependencies are satisfied + property("Properly orders results.") = secure(expect(ordering, Set(B, A, C, E, F))) + + /* + property("Detects cyclic negation") = secure( + Logic.reduceAll(badClauses, Set()) match { + case Right(_) => false + case Left(_: Logic.CyclicNegation) => true + case Left(err) => sys.error(s"Expected cyclic error, got: $err") + } + ) + */ + + def expect(result: Either[LogicException, Matched], expected: Set[Atom]) = result match { + case Left(_) => false + case Right(res) => + val actual = res.provenSet + if (actual != expected) + sys.error(s"Expected to prove $expected, but actually proved $actual") + else + true + } +} + +object TestClauses { + + val A = Atom("A") + val B = Atom("B") + val C = Atom("C") + val D = Atom("D") + val E = Atom("E") + val F = Atom("F") + val G = Atom("G") + + val clauses = + A.proves(B) :: + A.proves(F) :: + B.proves(F) :: + F.proves(A) :: + (!C).proves(F) :: + D.proves(C) :: + C.proves(D) :: + Nil + + val cycles = Logic.reduceAll(clauses, Set()) + + val badClauses = + A.proves(D) :: + clauses + + val excludedNeg = { + val cs = + (!A).proves(B) :: + Nil + val init = + (!A) :: + (!B) :: + Nil + Logic.reduceAll(cs, init.toSet) + } + + val excludedPos = { + val cs = + A.proves(B) :: + Nil + val init = + A :: + (!B) :: + Nil + Logic.reduceAll(cs, init.toSet) + } + + val trivial = { + val cs = + Formula.True.proves(A) :: + Nil + Logic.reduceAll(cs, Set.empty) + } + + val lessTrivial = { + val cs = + Formula.True.proves(A) :: + Formula.True.proves(B) :: + (A && B && (!C)).proves(D) :: + Nil + Logic.reduceAll(cs, Set()) + } + + val ordering = { + val cs = + E.proves(F) :: + (C && !D).proves(E) :: + (A && B).proves(C) :: + Nil + Logic.reduceAll(cs, Set(A, B)) + } + + def all(): Unit = { + println(s"Cycles: $cycles") + println(s"xNeg: $excludedNeg") + println(s"xPos: $excludedPos") + println(s"trivial: $trivial") + println(s"lessTrivial: $lessTrivial") + println(s"ordering: $ordering") + } +} diff --git a/internal/util-position/src/main/scala/sbt/internal/util/Positions.scala b/internal/util-position/src/main/scala/sbt/internal/util/Positions.scala new file mode 100644 index 000000000..c3895a4bd --- /dev/null +++ b/internal/util-position/src/main/scala/sbt/internal/util/Positions.scala @@ -0,0 +1,74 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util + +import scala.language.experimental.macros + +sealed trait SourcePosition + +sealed trait FilePosition extends SourcePosition { + def path: String + def startLine: Int +} + +case object NoPosition extends SourcePosition + +final case class LinePosition(path: String, startLine: Int) extends FilePosition + +final case class LineRange(start: Int, end: Int) { + def shift(n: Int) = new LineRange(start + n, end + n) +} + +final case class RangePosition(path: String, range: LineRange) extends FilePosition { + def startLine = range.start +} + +object SourcePosition { + + /** Creates a SourcePosition by using the enclosing position of the invocation of this method. + * @return SourcePosition + */ + def fromEnclosing(): SourcePosition = macro SourcePositionMacro.fromEnclosingImpl + +} + +import scala.annotation.tailrec +import scala.reflect.macros.blackbox +import scala.reflect.internal.util.UndefinedPosition + +final class SourcePositionMacro(val c: blackbox.Context) { + import c.universe.{ NoPosition => _, _ } + + def fromEnclosingImpl(): Expr[SourcePosition] = { + val pos = c.enclosingPosition + if (!pos.isInstanceOf[UndefinedPosition] && pos.line >= 0 && pos.source != null) { + val f = pos.source.file + val name = constant[String](ownerSource(f.path, f.name)) + val line = constant[Int](pos.line) + reify { LinePosition(name.splice, line.splice) } + } else + reify { NoPosition } + } + + private[this] def ownerSource(path: String, name: String): String = { + @tailrec def inEmptyPackage(s: Symbol): Boolean = + s != NoSymbol && ( + s.owner == c.mirror.EmptyPackage + || s.owner == c.mirror.EmptyPackageClass + || inEmptyPackage(s.owner) + ) + + c.internal.enclosingOwner match { + case ec if !ec.isStatic => name + case ec if inEmptyPackage(ec) => path + case ec => s"(${ec.fullName}) $name" + } + } + + private[this] def constant[T: WeakTypeTag](t: T): Expr[T] = c.Expr[T](Literal(Constant(t))) +} diff --git a/internal/util-position/src/test/scala/sbt/internal/util/SourcePositionSpec.scala b/internal/util-position/src/test/scala/sbt/internal/util/SourcePositionSpec.scala new file mode 100644 index 000000000..93ed9fb96 --- /dev/null +++ b/internal/util-position/src/test/scala/sbt/internal/util/SourcePositionSpec.scala @@ -0,0 +1,25 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util + +import org.scalatest._ + +class SourcePositionSpec extends FlatSpec { + "SourcePosition()" should "return a sane SourcePosition" in { + val filename = "SourcePositionSpec.scala" + val lineNumber = 16 + SourcePosition.fromEnclosing() match { + case LinePosition(path, startLine) => assert(path === filename && startLine === lineNumber) + case RangePosition(path, range) => assert(path === filename && inRange(range, lineNumber)) + case NoPosition => fail("No source position found") + } + } + + private def inRange(range: LineRange, lineNo: Int) = + range.start until range.end contains lineNo +} diff --git a/internal/util-relation/src/main/scala/sbt/internal/util/Relation.scala b/internal/util-relation/src/main/scala/sbt/internal/util/Relation.scala new file mode 100644 index 000000000..befdba41c --- /dev/null +++ b/internal/util-relation/src/main/scala/sbt/internal/util/Relation.scala @@ -0,0 +1,206 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util + +import Relation._ + +object Relation { + + /** Constructs a new immutable, finite relation that is initially empty. */ + def empty[A, B]: Relation[A, B] = make(Map.empty, Map.empty) + + /** + * Constructs a [[Relation]] from underlying `forward` and `reverse` representations, without checking that they are consistent. + * This is a low-level constructor and the alternatives [[empty]] and [[reconstruct]] should be preferred. + */ + def make[A, B](forward: Map[A, Set[B]], reverse: Map[B, Set[A]]): Relation[A, B] = + new MRelation(forward, reverse) + + /** Constructs a relation such that for every entry `_1 -> _2s` in `forward` and every `_2` in `_2s`, `(_1, _2)` is in the relation. */ + def reconstruct[A, B](forward: Map[A, Set[B]]): Relation[A, B] = { + val reversePairs = for ((a, bs) <- forward.view; b <- bs.view) yield (b, a) + val reverse = reversePairs.foldLeft(Map.empty[B, Set[A]]) { + case (m, (b, a)) => add(m, b, a :: Nil) + } + make(forward filter { case (a, bs) => bs.nonEmpty }, reverse) + } + + def merge[A, B](rels: Traversable[Relation[A, B]]): Relation[A, B] = + rels.foldLeft(Relation.empty[A, B])(_ ++ _) + + private[sbt] def remove[X, Y](map: M[X, Y], from: X, to: Y): M[X, Y] = + map.get(from) match { + case Some(tos) => + val newSet = tos - to + if (newSet.isEmpty) map - from else map.updated(from, newSet) + case None => map + } + + private[sbt] def combine[X, Y](a: M[X, Y], b: M[X, Y]): M[X, Y] = + b.foldLeft(a)((map, mapping) => add(map, mapping._1, mapping._2)) + + private[sbt] def add[X, Y](map: M[X, Y], from: X, to: Traversable[Y]): M[X, Y] = + map.updated(from, get(map, from) ++ to) + + private[sbt] def get[X, Y](map: M[X, Y], t: X): Set[Y] = map.getOrElse(t, Set.empty[Y]) + + private[sbt] type M[X, Y] = Map[X, Set[Y]] +} + +/** Binary relation between A and B. It is a set of pairs (_1, _2) for _1 in A, _2 in B. */ +trait Relation[A, B] { + + /** Returns the set of all `_2`s such that `(_1, _2)` is in this relation. */ + def forward(_1: A): Set[B] + + /** Returns the set of all `_1`s such that `(_1, _2)` is in this relation. */ + def reverse(_2: B): Set[A] + + /** Includes `pair` in the relation. */ + def +(pair: (A, B)): Relation[A, B] + + /** Includes `(a, b)` in the relation. */ + def +(a: A, b: B): Relation[A, B] + + /** Includes in the relation `(a, b)` for all `b` in `bs`. */ + def +(a: A, bs: Traversable[B]): Relation[A, B] + + /** Returns the union of the relation `r` with this relation. */ + def ++(r: Relation[A, B]): Relation[A, B] + + /** Includes the given pairs in this relation. */ + def ++(rs: Traversable[(A, B)]): Relation[A, B] + + /** Removes all elements `(_1, _2)` for all `_1` in `_1s` from this relation. */ + def --(_1s: Traversable[A]): Relation[A, B] + + /** Removes all `pairs` from this relation. */ + def --(pairs: TraversableOnce[(A, B)]): Relation[A, B] + + /** Removes all `relations` from this relation. */ + def --(relations: Relation[A, B]): Relation[A, B] + + /** Removes all pairs `(_1, _2)` from this relation. */ + def -(_1: A): Relation[A, B] + + /** Removes `pair` from this relation. */ + def -(pair: (A, B)): Relation[A, B] + + /** Returns the set of all `_1`s such that `(_1, _2)` is in this relation. */ + def _1s: collection.Set[A] + + /** Returns the set of all `_2`s such that `(_1, _2)` is in this relation. */ + def _2s: collection.Set[B] + + /** Returns the number of pairs in this relation */ + def size: Int + + /** Returns true iff `(a,b)` is in this relation*/ + def contains(a: A, b: B): Boolean + + /** Returns a relation with only pairs `(a,b)` for which `f(a,b)` is true.*/ + def filter(f: (A, B) => Boolean): Relation[A, B] + + /** + * Returns a pair of relations: the first contains only pairs `(a,b)` for which `f(a,b)` is true and + * the other only pairs `(a,b)` for which `f(a,b)` is false. + */ + def partition(f: (A, B) => Boolean): (Relation[A, B], Relation[A, B]) + + /** Partitions this relation into a map of relations according to some discriminator function. */ + def groupBy[K](discriminator: ((A, B)) => K): Map[K, Relation[A, B]] + + /** Returns all pairs in this relation.*/ + def all: Traversable[(A, B)] + + /** + * Represents this relation as a `Map` from a `_1` to the set of `_2`s such that `(_1, _2)` is in this relation. + * + * Specifically, there is one entry for each `_1` such that `(_1, _2)` is in this relation for some `_2`. + * The value associated with a given `_1` is the set of all `_2`s such that `(_1, _2)` is in this relation. + */ + def forwardMap: Map[A, Set[B]] + + /** + * Represents this relation as a `Map` from a `_2` to the set of `_1`s such that `(_1, _2)` is in this relation. + * + * Specifically, there is one entry for each `_2` such that `(_1, _2)` is in this relation for some `_1`. + * The value associated with a given `_2` is the set of all `_1`s such that `(_1, _2)` is in this relation. + */ + def reverseMap: Map[B, Set[A]] + +} + +// Note that we assume without checking that fwd and rev are consistent. +private final class MRelation[A, B](fwd: Map[A, Set[B]], rev: Map[B, Set[A]]) + extends Relation[A, B] { + def forwardMap = fwd + def reverseMap = rev + + def forward(t: A) = get(fwd, t) + def reverse(t: B) = get(rev, t) + + def _1s = fwd.keySet + def _2s = rev.keySet + + def size = (fwd.valuesIterator map (_.size)).sum + + def all: Traversable[(A, B)] = + fwd.iterator.flatMap { case (a, bs) => bs.iterator.map(b => (a, b)) }.toTraversable + + def +(pair: (A, B)) = this + (pair._1, Set(pair._2)) + def +(from: A, to: B) = this + (from, to :: Nil) + def +(from: A, to: Traversable[B]) = + if (to.isEmpty) this + else new MRelation(add(fwd, from, to), to.foldLeft(rev)((map, t) => add(map, t, from :: Nil))) + + def ++(rs: Traversable[(A, B)]) = rs.foldLeft(this: Relation[A, B]) { _ + _ } + def ++(other: Relation[A, B]) = + new MRelation[A, B](combine(fwd, other.forwardMap), combine(rev, other.reverseMap)) + + def --(ts: Traversable[A]): Relation[A, B] = ts.foldLeft(this: Relation[A, B]) { _ - _ } + def --(pairs: TraversableOnce[(A, B)]): Relation[A, B] = + pairs.foldLeft(this: Relation[A, B])(_ - _) + def --(relations: Relation[A, B]): Relation[A, B] = --(relations.all) + + def -(pair: (A, B)): Relation[A, B] = + new MRelation(remove(fwd, pair._1, pair._2), remove(rev, pair._2, pair._1)) + + def -(t: A): Relation[A, B] = + fwd.get(t) match { + case Some(rs) => + val upRev = rs.foldLeft(rev)((map, r) => remove(map, r, t)) + new MRelation(fwd - t, upRev) + case None => this + } + + def filter(f: (A, B) => Boolean): Relation[A, B] = Relation.empty[A, B] ++ all.filter(f.tupled) + + def partition(f: (A, B) => Boolean): (Relation[A, B], Relation[A, B]) = { + val (y, n) = all.partition(f.tupled) + (Relation.empty[A, B] ++ y, Relation.empty[A, B] ++ n) + } + + def groupBy[K](discriminator: ((A, B)) => K): Map[K, Relation[A, B]] = + (all.groupBy(discriminator) mapValues { Relation.empty[A, B] ++ _ }).toMap + + def contains(a: A, b: B): Boolean = forward(a)(b) + + override def equals(other: Any) = other match { + // We assume that the forward and reverse maps are consistent, so we only use the forward map + // for equality. Note that key -> Empty is semantically the same as key not existing. + case o: MRelation[A, B] => + forwardMap.filterNot(_._2.isEmpty) == o.forwardMap.filterNot(_._2.isEmpty) + case _ => false + } + + override def hashCode = fwd.filterNot(_._2.isEmpty).hashCode() + + override def toString = + all.map { case (a, b) => a + " -> " + b }.mkString("Relation [", ", ", "]") +} diff --git a/internal/util-relation/src/test/scala/RelationTest.scala b/internal/util-relation/src/test/scala/RelationTest.scala new file mode 100644 index 000000000..1ee742d73 --- /dev/null +++ b/internal/util-relation/src/test/scala/RelationTest.scala @@ -0,0 +1,87 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util + +import org.scalacheck._ +import Prop._ + +object RelationTest extends Properties("Relation") { + property("Added entry check") = forAll { (pairs: List[(Int, Double)]) => + val r = Relation.empty[Int, Double] ++ pairs + check(r, pairs) + } + def check(r: Relation[Int, Double], pairs: Seq[(Int, Double)]) = { + val _1s = pairs.map(_._1).toSet + val _2s = pairs.map(_._2).toSet + + r._1s == _1s && r.forwardMap.keySet == _1s && + r._2s == _2s && r.reverseMap.keySet == _2s && + pairs.forall { + case (a, b) => + (r.forward(a) contains b) && + (r.reverse(b) contains a) && + (r.forwardMap(a) contains b) && + (r.reverseMap(b) contains a) + } + } + + property("Does not contain removed entries") = forAll { (pairs: List[(Int, Double, Boolean)]) => + val add = pairs.map { case (a, b, c) => (a, b) } + val added = Relation.empty[Int, Double] ++ add + + val removeFine = pairs.collect { case (a, b, true) => (a, b) } + val removeCoarse = removeFine.map(_._1) + val r = added -- removeCoarse + + def notIn[X, Y](map: Map[X, Set[Y]], a: X, b: Y) = map.get(a).forall(set => !(set contains b)) + + all(removeCoarse) { rem => + ("_1s does not contain removed" |: (!r._1s.contains(rem))) && + ("Forward does not contain removed" |: r.forward(rem).isEmpty) && + ("Forward map does not contain removed" |: !r.forwardMap.contains(rem)) && + ("Removed is not a value in reverse map" |: !r.reverseMap.values.toSet.contains(rem)) + } && + all(removeFine) { + case (a, b) => + ("Forward does not contain removed" |: (!r.forward(a).contains(b))) && + ("Reverse does not contain removed" |: (!r.reverse(b).contains(a))) && + ("Forward map does not contain removed" |: (notIn(r.forwardMap, a, b))) && + ("Reverse map does not contain removed" |: (notIn(r.reverseMap, b, a))) + } + } + + property("Groups correctly") = forAll { (entries: List[(Int, Double)], randomInt: Int) => + val splitInto = math.abs(randomInt) % 10 + 1 // Split into 1-10 groups. + val rel = Relation.empty[Int, Double] ++ entries + val grouped = rel groupBy (_._1 % splitInto) + all(grouped.toSeq) { + case (k, rel_k) => rel_k._1s forall { _ % splitInto == k } + } + } + + property("Computes size correctly") = forAll { (entries: List[(Int, Double)]) => + val rel = Relation.empty[Int, Double] ++ entries + val expected = rel.all.size // Note: not entries.length, as entries may have duplicates. + val computed = rel.size + "Expected size: %d. Computed size: %d.".format(expected, computed) |: expected == computed + } + + def all[T](s: Seq[T])(p: T => Prop): Prop = + if (s.isEmpty) true else s.map(p).reduceLeft(_ && _) +} + +object EmptyRelationTest extends Properties("Empty relation") { + lazy val e = Relation.empty[Int, Double] + + property("Forward empty") = forAll((i: Int) => e.forward(i).isEmpty) + property("Reverse empty") = forAll((i: Double) => e.reverse(i).isEmpty) + property("Forward map empty") = e.forwardMap.isEmpty + property("Reverse map empty") = e.reverseMap.isEmpty + property("_1 empty") = e._1s.isEmpty + property("_2 empty") = e._2s.isEmpty +} diff --git a/internal/util-scripted/src/main/java/sbt/internal/scripted/ScriptConfig.java b/internal/util-scripted/src/main/java/sbt/internal/scripted/ScriptConfig.java new file mode 100644 index 000000000..39216d3ae --- /dev/null +++ b/internal/util-scripted/src/main/java/sbt/internal/scripted/ScriptConfig.java @@ -0,0 +1,37 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.scripted; + +import java.io.File; + +import xsbti.Logger; + +public class ScriptConfig { + + private String label; + private File testDirectory; + private Logger logger; + + public ScriptConfig(String label, File testDirectory, Logger logger) { + this.label = label; + this.testDirectory = testDirectory; + this.logger = logger; + } + + public String label() { + return this.label; + } + + public File testDirectory() { + return this.testDirectory; + } + + public Logger logger() { + return this.logger; + } +} diff --git a/internal/util-scripted/src/main/scala/sbt/internal/scripted/CommentHandler.scala b/internal/util-scripted/src/main/scala/sbt/internal/scripted/CommentHandler.scala new file mode 100644 index 000000000..7b78f82e3 --- /dev/null +++ b/internal/util-scripted/src/main/scala/sbt/internal/scripted/CommentHandler.scala @@ -0,0 +1,14 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt +package internal +package scripted + +object CommentHandler extends BasicStatementHandler { + def apply(command: String, args: List[String]) = () +} diff --git a/internal/util-scripted/src/main/scala/sbt/internal/scripted/FileCommands.scala b/internal/util-scripted/src/main/scala/sbt/internal/scripted/FileCommands.scala new file mode 100644 index 000000000..d9e7a1d42 --- /dev/null +++ b/internal/util-scripted/src/main/scala/sbt/internal/scripted/FileCommands.scala @@ -0,0 +1,150 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt +package internal +package scripted + +import java.io.File +import sbt.io.{ IO, Path } +import sbt.io.syntax._ +import Path._ +import sbt.io.IO + +class FileCommands(baseDirectory: File) extends BasicStatementHandler { + lazy val commands = commandMap + def commandMap = + Map( + "touch" nonEmpty touch _, + "delete" nonEmpty delete _, + "exists" nonEmpty exists _, + "mkdir" nonEmpty makeDirectories _, + "absent" nonEmpty absent _, + // "sync" twoArg("Two directory paths", sync _), + "newer" twoArg ("Two paths", newer _), + "pause" noArg { + println("Pausing in " + baseDirectory) + /*readLine("Press enter to continue. ") */ + print("Press enter to continue. ") + System.console.readLine + println() + }, + "sleep" oneArg ("Time in milliseconds", time => Thread.sleep(time.toLong)), + "exec" nonEmpty (execute _), + "copy" copy (to => rebase(baseDirectory, to)), + "copy-file" twoArg ("Two paths", copyFile _), + "must-mirror" twoArg ("Two paths", diffFiles _), + "copy-flat" copy flat + ) + + def apply(command: String, arguments: List[String]): Unit = + commands.get(command).map(_(arguments)) match { + case Some(_) => () + case _ => scriptError("Unknown command " + command); () + } + + def scriptError(message: String): Unit = sys.error("Test script error: " + message) + def spaced[T](l: Seq[T]) = l.mkString(" ") + def fromStrings(paths: List[String]) = paths.map(fromString) + def fromString(path: String) = new File(baseDirectory, path) + def touch(paths: List[String]): Unit = IO.touch(fromStrings(paths)) + def delete(paths: List[String]): Unit = IO.delete(fromStrings(paths)) + /*def sync(from: String, to: String) = + IO.sync(fromString(from), fromString(to), log)*/ + def copyFile(from: String, to: String): Unit = + IO.copyFile(fromString(from), fromString(to)) + def makeDirectories(paths: List[String]) = + IO.createDirectories(fromStrings(paths)) + def diffFiles(file1: String, file2: String): Unit = { + val lines1 = IO.readLines(fromString(file1)) + val lines2 = IO.readLines(fromString(file2)) + if (lines1 != lines2) + scriptError( + "File contents are different:\n" + lines1.mkString("\n") + + "\nAnd:\n" + lines2.mkString("\n") + ) + } + + def newer(a: String, b: String): Unit = { + val pathA = fromString(a) + val pathB = fromString(b) + val isNewer = pathA.exists && + (!pathB.exists || IO.getModifiedTimeOrZero(pathA) > IO.getModifiedTimeOrZero(pathB)) + if (!isNewer) { + scriptError(s"$pathA is not newer than $pathB") + } + } + def exists(paths: List[String]): Unit = { + val notPresent = fromStrings(paths).filter(!_.exists) + if (notPresent.nonEmpty) + scriptError("File(s) did not exist: " + notPresent.mkString("[ ", " , ", " ]")) + } + def absent(paths: List[String]): Unit = { + val present = fromStrings(paths).filter(_.exists) + if (present.nonEmpty) + scriptError("File(s) existed: " + present.mkString("[ ", " , ", " ]")) + } + def execute(command: List[String]): Unit = execute0(command.head, command.tail) + def execute0(command: String, args: List[String]): Unit = { + if (command.trim.isEmpty) + scriptError("Command was empty.") + else { + val exitValue = sys.process.Process(command :: args, baseDirectory).! + if (exitValue != 0) + sys.error("Nonzero exit value (" + exitValue + ")") + } + } + + // these are for readability of the command list + implicit def commandBuilder(s: String): CommandBuilder = new CommandBuilder(s) + final class CommandBuilder(commandName: String) { + type NamedCommand = (String, List[String] => Unit) + def nonEmpty(action: List[String] => Unit): NamedCommand = + commandName -> { paths => + if (paths.isEmpty) + scriptError("No arguments specified for " + commandName + " command.") + else + action(paths) + } + def twoArg(requiredArgs: String, action: (String, String) => Unit): NamedCommand = + commandName -> { + case List(from, to) => action(from, to) + case other => wrongArguments(requiredArgs, other) + } + def noArg(action: => Unit): NamedCommand = + commandName -> { + case Nil => action + case other => wrongArguments(other) + } + def oneArg(requiredArgs: String, action: String => Unit): NamedCommand = + commandName -> { + case List(single) => action(single) + case other => wrongArguments(requiredArgs, other) + } + def copy(mapper: File => FileMap): NamedCommand = + commandName -> { + case Nil => scriptError("No paths specified for " + commandName + " command.") + case path :: Nil => scriptError("No destination specified for " + commandName + " command.") + case paths => + val mapped = fromStrings(paths) + val map = mapper(mapped.last) + IO.copy(mapped.init pair map) + () + } + + def wrongArguments(args: List[String]): Unit = + scriptError( + "Command '" + commandName + "' does not accept arguments (found '" + spaced(args) + "')." + ) + + def wrongArguments(requiredArgs: String, args: List[String]): Unit = + scriptError( + "Wrong number of arguments to " + commandName + " command. " + + requiredArgs + " required, found: '" + spaced(args) + "'." + ) + } +} diff --git a/internal/util-scripted/src/main/scala/sbt/internal/scripted/FilteredLoader.scala b/internal/util-scripted/src/main/scala/sbt/internal/scripted/FilteredLoader.scala new file mode 100644 index 000000000..8c980eb97 --- /dev/null +++ b/internal/util-scripted/src/main/scala/sbt/internal/scripted/FilteredLoader.scala @@ -0,0 +1,22 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt +package internal +package scripted + +final class FilteredLoader(parent: ClassLoader) extends ClassLoader(parent) { + @throws(classOf[ClassNotFoundException]) + override final def loadClass(className: String, resolve: Boolean): Class[_] = { + if (className.startsWith("java.") || className.startsWith("javax.")) + super.loadClass(className, resolve) + else + throw new ClassNotFoundException(className) + } + override def getResources(name: String) = null + override def getResource(name: String) = null +} diff --git a/internal/util-scripted/src/main/scala/sbt/internal/scripted/HandlersProvider.scala b/internal/util-scripted/src/main/scala/sbt/internal/scripted/HandlersProvider.scala new file mode 100644 index 000000000..f6bfe1c20 --- /dev/null +++ b/internal/util-scripted/src/main/scala/sbt/internal/scripted/HandlersProvider.scala @@ -0,0 +1,12 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.scripted + +trait HandlersProvider { + def getHandlers(config: ScriptConfig): Map[Char, StatementHandler] +} diff --git a/internal/util-scripted/src/main/scala/sbt/internal/scripted/ScriptRunner.scala b/internal/util-scripted/src/main/scala/sbt/internal/scripted/ScriptRunner.scala new file mode 100644 index 000000000..9bcbe25ee --- /dev/null +++ b/internal/util-scripted/src/main/scala/sbt/internal/scripted/ScriptRunner.scala @@ -0,0 +1,57 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt +package internal +package scripted + +final class TestException(statement: Statement, msg: String, exception: Throwable) + extends RuntimeException(statement.linePrefix + " " + msg, exception) + +class ScriptRunner { + import scala.collection.mutable.HashMap + def apply(statements: List[(StatementHandler, Statement)]): Unit = { + val states = new HashMap[StatementHandler, Any] + def processStatement(handler: StatementHandler, statement: Statement): Unit = { + val state = states(handler).asInstanceOf[handler.State] + val nextState = + try { + Right(handler(statement.command, statement.arguments, state)) + } catch { + case e: Exception => Left(e) + } + nextState match { + case Left(err) => + if (statement.successExpected) { + err match { + case t: TestFailed => + throw new TestException(statement, "Command failed: " + t.getMessage, null) + case _ => throw new TestException(statement, "Command failed", err) + } + } else + () + case Right(s) => + if (statement.successExpected) + states(handler) = s + else + throw new TestException(statement, "Command succeeded but failure was expected", null) + } + } + val handlers = Set() ++ statements.map(_._1) + + try { + handlers.foreach(handler => states(handler) = handler.initialState) + statements foreach (Function.tupled(processStatement)) + } finally { + for (handler <- handlers; state <- states.get(handler)) { + try { + handler.finish(state.asInstanceOf[handler.State]) + } catch { case e: Exception => () } + } + } + } +} diff --git a/internal/util-scripted/src/main/scala/sbt/internal/scripted/ScriptedTests.scala b/internal/util-scripted/src/main/scala/sbt/internal/scripted/ScriptedTests.scala new file mode 100644 index 000000000..ffdd4e685 --- /dev/null +++ b/internal/util-scripted/src/main/scala/sbt/internal/scripted/ScriptedTests.scala @@ -0,0 +1,235 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt +package internal +package scripted + +import java.io.File +import sbt.util.{ Logger, LoggerContext, Level } +import sbt.internal.util.{ Appender, ManagedLogger, ConsoleAppender, BufferedAppender } +import sbt.io.IO.wrapNull +import sbt.io.{ DirectoryFilter, HiddenFileFilter } +import sbt.io.syntax._ +import sbt.internal.io.Resources +import java.util.concurrent.atomic.AtomicInteger + +object ScriptedRunnerImpl { + def run( + resourceBaseDirectory: File, + bufferLog: Boolean, + tests: Array[String], + handlersProvider: HandlersProvider + ): Unit = { + val context = + LoggerContext(useLog4J = System.getProperty("sbt.log.uselog4j", "false") == "true") + val runner = new ScriptedTests(resourceBaseDirectory, bufferLog, handlersProvider) + val logger = newLogger(context) + val allTests = get(tests, resourceBaseDirectory, logger) flatMap { + case ScriptedTest(group, name) => + runner.scriptedTest(group, name, logger, context) + } + runAll(allTests) + } + def runAll(tests: Seq[() => Option[String]]): Unit = { + val errors = for (test <- tests; err <- test()) yield err + if (errors.nonEmpty) + sys.error(errors.mkString("Failed tests:\n\t", "\n\t", "\n")) + } + def get(tests: Seq[String], baseDirectory: File, log: ManagedLogger): Seq[ScriptedTest] = + if (tests.isEmpty) listTests(baseDirectory, log) else parseTests(tests) + def listTests(baseDirectory: File, log: ManagedLogger): Seq[ScriptedTest] = + (new ListTests(baseDirectory, _ => true, log)).listTests + def parseTests(in: Seq[String]): Seq[ScriptedTest] = + for (testString <- in) yield { + val Array(group, name) = testString.split("/").map(_.trim) + ScriptedTest(group, name) + } + private[sbt] val generateId: AtomicInteger = new AtomicInteger + private[sbt] def newLogger(context: LoggerContext): ManagedLogger = { + val loggerName = "scripted-" + generateId.incrementAndGet + context.logger(loggerName, None, None) + } +} + +final class ScriptedTests( + resourceBaseDirectory: File, + bufferLog: Boolean, + handlersProvider: HandlersProvider, + stripQuotes: Boolean +) { + def this(resourceBaseDirectory: File, bufferLog: Boolean, handlersProvider: HandlersProvider) = + this(resourceBaseDirectory, bufferLog, handlersProvider, true) + private val testResources = new Resources(resourceBaseDirectory) + private val appender: Appender = ConsoleAppender() + + val ScriptFilename = "test" + val PendingScriptFilename = "pending" + + def scriptedTest(group: String, name: String, log: xsbti.Logger): Seq[() => Option[String]] = + scriptedTest(group, name, Logger.xlog2Log(log)) + + @deprecated("Use scriptedTest that takes a LoggerContext", "1.4.0") + def scriptedTest( + group: String, + name: String, + log: ManagedLogger, + ): Seq[() => Option[String]] = + scriptedTest(group, name, (_ => ()), log, LoggerContext.globalContext) + def scriptedTest( + group: String, + name: String, + log: ManagedLogger, + context: LoggerContext + ): Seq[() => Option[String]] = + scriptedTest(group, name, (_ => ()), log, context) + + @deprecated("Use scriptedTest that provides LoggerContext", "1.4.0") + def scriptedTest( + group: String, + name: String, + prescripted: File => Unit, + log: ManagedLogger, + ): Seq[() => Option[String]] = + scriptedTest(group, name, prescripted, log, LoggerContext.globalContext) + def scriptedTest( + group: String, + name: String, + prescripted: File => Unit, + log: ManagedLogger, + context: LoggerContext, + ): Seq[() => Option[String]] = { + for (groupDir <- (resourceBaseDirectory * group).get; nme <- (groupDir * name).get) yield { + val g = groupDir.getName + val n = nme.getName + val str = s"$g / $n" + () => { + println("Running " + str) + testResources.readWriteResourceDirectory(g, n) { testDirectory => + val disabled = new File(testDirectory, "disabled").isFile + if (disabled) { + log.info("D " + str + " [DISABLED]") + None + } else { + try { + scriptedTest(str, testDirectory, prescripted, log, context); None + } catch { + case _: TestException | _: PendingTestSuccessException => Some(str) + } + } + } + } + } + } + + private def scriptedTest( + label: String, + testDirectory: File, + prescripted: File => Unit, + log: ManagedLogger, + context: LoggerContext, + ): Unit = { + val buffered = BufferedAppender(appender) + context.clearAppenders(log.name) + context.addAppender(log.name, (buffered -> Level.Debug)) + if (bufferLog) { + buffered.record() + } + def createParser() = { + // val fileHandler = new FileCommands(testDirectory) + // // val sbtHandler = new SbtHandler(testDirectory, launcher, buffered, launchOpts) + // new TestScriptParser(Map('$' -> fileHandler, /* '>' -> sbtHandler, */ '#' -> CommentHandler)) + val scriptConfig = new ScriptConfig(label, testDirectory, log) + new TestScriptParser(handlersProvider getHandlers scriptConfig) + } + val (file, pending) = { + val normal = new File(testDirectory, ScriptFilename) + val pending = new File(testDirectory, PendingScriptFilename) + if (pending.isFile) (pending, true) else (normal, false) + } + val pendingString = if (pending) " [PENDING]" else "" + + def runTest(): Unit = { + val run = new ScriptRunner + val parser = createParser() + run(parser.parse(file, stripQuotes)) + } + def testFailed(): Unit = { + if (pending) buffered.clearBuffer() else buffered.stopBuffer() + log.error("x " + label + pendingString) + } + + try { + prescripted(testDirectory) + runTest() + log.info("+ " + label + pendingString) + if (pending) throw new PendingTestSuccessException(label) + } catch { + case e: TestException => + testFailed() + e.getCause match { + case null | _: java.net.SocketException => log.error(" " + e.getMessage) + case _ => if (!pending) e.printStackTrace + } + if (!pending) throw e + case e: PendingTestSuccessException => + testFailed() + log.error(" Mark as passing to remove this failure.") + throw e + case e: Exception => + testFailed() + if (!pending) throw e + } finally { + buffered.clearBuffer() + } + } +} + +// object ScriptedTests extends ScriptedRunner { +// val emptyCallback: File => Unit = { _ => () } +// } + +final case class ScriptedTest(group: String, name: String) { + override def toString = group + "/" + name +} + +object ListTests { + def list(directory: File, filter: java.io.FileFilter) = wrapNull(directory.listFiles(filter)) +} +import ListTests._ +final class ListTests(baseDirectory: File, accept: ScriptedTest => Boolean, log: Logger) { + def filter = DirectoryFilter -- HiddenFileFilter + def listTests: Seq[ScriptedTest] = { + list(baseDirectory, filter) flatMap { group => + val groupName = group.getName + listTests(group).map(ScriptedTest(groupName, _)) + } + } + private[this] def listTests(group: File): Seq[String] = { + val groupName = group.getName + val allTests = list(group, filter).sortBy(_.getName) + if (allTests.isEmpty) { + log.warn("No tests in test group " + groupName) + Seq.empty + } else { + val (included, skipped) = + allTests.toList.partition(test => accept(ScriptedTest(groupName, test.getName))) + if (included.isEmpty) + log.warn("Test group " + groupName + " skipped.") + else if (skipped.nonEmpty) { + log.warn("Tests skipped in group " + group.getName + ":") + skipped.foreach(testName => log.warn(" " + testName.getName)) + } + Seq(included.map(_.getName): _*) + } + } +} + +class PendingTestSuccessException(label: String) extends Exception { + override def getMessage: String = + s"The pending test $label succeeded. Mark this test as passing to remove this failure." +} diff --git a/internal/util-scripted/src/main/scala/sbt/internal/scripted/StatementHandler.scala b/internal/util-scripted/src/main/scala/sbt/internal/scripted/StatementHandler.scala new file mode 100644 index 000000000..56a2bfc44 --- /dev/null +++ b/internal/util-scripted/src/main/scala/sbt/internal/scripted/StatementHandler.scala @@ -0,0 +1,33 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt +package internal +package scripted + +trait StatementHandler { + type State + def initialState: State + def apply(command: String, arguments: List[String], state: State): State + def finish(state: State): Unit +} + +trait BasicStatementHandler extends StatementHandler { + final type State = Unit + final def initialState = () + + final def apply(command: String, arguments: List[String], state: Unit): Unit = + apply(command, arguments) + + def apply(command: String, arguments: List[String]): Unit + def finish(state: Unit) = () +} + +/** Use when a stack trace is not useful */ +final class TestFailed(msg: String) extends RuntimeException(msg) { + override def fillInStackTrace = this +} diff --git a/internal/util-scripted/src/main/scala/sbt/internal/scripted/TestScriptParser.scala b/internal/util-scripted/src/main/scala/sbt/internal/scripted/TestScriptParser.scala new file mode 100644 index 000000000..9d2548059 --- /dev/null +++ b/internal/util-scripted/src/main/scala/sbt/internal/scripted/TestScriptParser.scala @@ -0,0 +1,118 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt +package internal +package scripted + +import java.io.File +import scala.util.parsing.combinator._ +import scala.util.parsing.input.Positional +import Character.isWhitespace +import sbt.io.IO + +/* +statement* +statement ::= startChar successChar word+ nl +startChar ::= +successChar ::= '+' | '-' +word ::= [^ \[\]]+ +comment ::= '#' \S* nl +nl ::= '\r' \'n' | '\n' | '\r' | eof + */ +final case class Statement( + command: String, + arguments: List[String], + successExpected: Boolean, + line: Int +) { + def linePrefix = "{line " + line + "} " +} + +private object TestScriptParser { + val SuccessLiteral = "success" + val FailureLiteral = "failure" + val WordRegex = """[^ \[\]\s'\"][^ \[\]\s]*""".r +} + +import TestScriptParser._ +class TestScriptParser(handlers: Map[Char, StatementHandler]) extends RegexParsers { + require(handlers.nonEmpty) + override def skipWhitespace = false + + import IO.read + if (handlers.keys.exists(isWhitespace)) + sys.error("Start characters cannot be whitespace") + if (handlers.keys.exists(key => key == '+' || key == '-')) + sys.error("Start characters cannot be '+' or '-'") + + @deprecated("Use variant that specifies whether to strip quotes or not", "1.4.0") + def parse(scriptFile: File): List[(StatementHandler, Statement)] = + parse(scriptFile, stripQuotes = true) + def parse(scriptFile: File, stripQuotes: Boolean): List[(StatementHandler, Statement)] = + parse(read(scriptFile), Some(scriptFile.getAbsolutePath), stripQuotes) + @deprecated("Use variant that specifies whether to strip quotes or not", "1.4.0") + def parse(script: String): List[(StatementHandler, Statement)] = + parse(script, None, stripQuotes = true) + def parse(script: String, stripQuotes: Boolean): List[(StatementHandler, Statement)] = + parse(script, None, stripQuotes) + private def parse( + script: String, + label: Option[String], + stripQuotes: Boolean + ): List[(StatementHandler, Statement)] = { + parseAll(statements(stripQuotes), script) match { + case Success(result, next) => result + case err: NoSuccess => { + val labelString = label.map("'" + _ + "' ").getOrElse("") + sys.error("Could not parse test script, " + labelString + err.toString) + } + } + } + + @deprecated("Use variant that specifies whether to strip quotes or not", "1.4.0") + lazy val statements = rep1(space ~> statement <~ newline) + def statements(stripQuotes: Boolean): Parser[List[(StatementHandler, Statement)]] = + rep1(space ~> statement(stripQuotes) <~ newline) + + @deprecated("Use variant that specifies whether to strip quotes or not", "1.4.0") + def statement: Parser[(StatementHandler, Statement)] = statement(stripQuotes = true) + def statement(stripQuotes: Boolean): Parser[(StatementHandler, Statement)] = { + trait PositionalStatement extends Positional { + def tuple: (StatementHandler, Statement) + } + positioned { + val w = if (stripQuotes) word else rawWord + val command = w | err("expected command") + val arguments = rep(space ~> w | failure("expected argument")) + (successParser ~ (space ~> startCharacterParser <~ space) ~! command ~! arguments) ^^ { + case successExpected ~ start ~ command ~ arguments => + new PositionalStatement { + def tuple = + (handlers(start), new Statement(command, arguments, successExpected, pos.line)) + } + } + } ^^ (_.tuple) + } + + def successParser: Parser[Boolean] = ('+' ^^^ true) | ('-' ^^^ false) | success(true) + def space: Parser[String] = """[ \t]*""".r + + lazy val word: Parser[String] = + ("\'" ~> "[^'\n\r]*".r <~ "\'") | "\"" ~> "[^\"\n\r]*".r <~ "\'" | WordRegex + private lazy val rawWord: Parser[String] = + ("\'" ~> "[^'\n\r]*".r <~ "\'") | "\"[^\"\n\r]*\"".r | WordRegex + + def startCharacterParser: Parser[Char] = + elem("start character", handlers.contains _) | + ( + (newline | err("expected start character " + handlers.keys.mkString("(", "", ")"))) + ~> failure("end of input") + ) + + def newline = """\s*([\n\r]|$)""".r +} diff --git a/launch/NOTICE b/launch/NOTICE index d0158f707..8abf4ed53 100644 --- a/launch/NOTICE +++ b/launch/NOTICE @@ -1,6 +1,7 @@ -Simple Build Tool Launcher -Copyright 2008, 2009, 2010 Mark Harrah, David MacIver -Licensed under BSD-style license (see LICENSE) +sbt Launcher +Copyright 2011 - 2017, Lightbend, Inc. +Copyright 2008 - 2010, Mark Harrah, David MacIver +Licensed under BSD-3-Clause license (see LICENSE) Classes from the Scala library are distributed with the launcher. Copyright 2002-2013 EPFL, Lausanne diff --git a/launch/src/main/input_resources/sbt/sbt.boot.properties b/launch/src/main/input_resources/sbt/sbt.boot.properties index 0d3a64bb3..f13961dcf 100644 --- a/launch/src/main/input_resources/sbt/sbt.boot.properties +++ b/launch/src/main/input_resources/sbt/sbt.boot.properties @@ -12,6 +12,8 @@ [repositories] local + local-preloaded-ivy: file:///${sbt.preloaded-${sbt.global.base-${user.home}/.sbt}/preloaded/}, [organization]/[module]/[revision]/[type]s/[artifact](-[classifier]).[ext] + local-preloaded: file:///${sbt.preloaded-${sbt.global.base-${user.home}/.sbt}/preloaded/} maven-central sbt-maven-releases: https://repo.scala-sbt.org/scalasbt/maven-releases/, bootOnly sbt-maven-snapshots: https://repo.scala-sbt.org/scalasbt/maven-snapshots/, bootOnly @@ -20,6 +22,7 @@ [boot] directory: ${sbt.boot.directory-${sbt.global.base-${user.home}/.sbt}/boot/} + lock: ${sbt.boot.lock-true} [ivy] ivy-home: ${sbt.ivy.home-${user.home}/.ivy2/} diff --git a/main-actions/src/main/scala/sbt/CacheIvy.scala b/main-actions/src/main/scala/sbt/CacheIvy.scala deleted file mode 100644 index 0e270e192..000000000 --- a/main-actions/src/main/scala/sbt/CacheIvy.scala +++ /dev/null @@ -1,231 +0,0 @@ -/* sbt -- Simple Build Tool - * Copyright 2010 Mark Harrah - */ -package sbt - -import Predef.{ Map, Set, implicitly } // excludes *both 2.10.x conforms and 2.11.x $conforms in source compatible manner. - -import sbt.internal.util.{ Cache, HList, HNil, InputCache, LinePosition, LineRange, NoPosition, RangePosition, SourcePosition } -import sbt.internal.util.FileInfo.{ exists, hash } -import sbt.internal.util.Types.{ :+:, idFun } -import java.io.File -import java.{ util => ju } -import java.net.URL -import sbinary.{ DefaultProtocol, Format } -import sbt.internal.librarymanagement._ -import sbt.librarymanagement._ -import sbt.librarymanagement.RepositoryHelpers._ -import Ordering._ - -import sbt.io.Hash - -/** - * InputCaches for IvyConfiguration, ModuleSettings, and UpdateConfiguration - * The InputCaches for a basic data structure is built in two parts. - * Given the data structure: - * Data[A,B,C, ...] - * 1) Define a conversion from Data to the HList A :+: B :+: C :+: ... :+: HNil, - * excluding any members that should not be considered for caching - * 2) In theory, 1) would be enough and wrapHL would generate InputCache[Data] as long - * as all of InputCache[A], InputCache[B], ... exist. However, if any of these child - * InputCaches are constructed using wrapHL, you get a diverging implicit error. (I - * believe scalac is generating this error as specified, but that the implicits would - * be valid and not be infinite. This might take some effort to come up with a new rule - * that allows this) - * 3) So, we need to explicitly define the intermediate implicits. The general approach is: - * {{{ - * object LN { - * ... Data => HList conversions ... - * } - * import LN._ - * implicit dataCache: InputCache[Data] = wrapHL - * - * object L(N-1) ... - * }}} - * Each Data in LN only uses implicits from L(N-1). - * This way, higher levels (higher N) cannot see the HList conversions of subcomponents but can - * use the explicitly defined subcomponent implicits and there is no divergence. - * 4) Ideally, diverging implicits could be relaxed so that the ... = wrapIn lines could be removed. - */ -object CacheIvy { - def password(s: Option[String]) = new Array[Byte](0) - def names(s: Iterable[Configuration]): Set[String] = s.map(_.name).toSet - - import Cache._ - implicit def wrapHL[W, H, T <: HList](implicit f: W => H :+: T, cache: InputCache[H :+: T]): InputCache[W] = - Cache.wrapIn(f, cache) - - lazy val excludeMap: Format[Map[ModuleID, Set[String]]] = implicitly - lazy val updateIC: InputCache[IvyConfiguration :+: ModuleSettings :+: UpdateConfiguration :+: HNil] = implicitly - /* def deliverIC: InputCache[IvyConfiguration :+: ModuleSettings :+: DeliverConfiguration :+: HNil] = implicitly - def publishIC: InputCache[IvyConfiguration :+: ModuleSettings :+: PublishConfiguration :+: HNil] = implicitly*/ - lazy val moduleIDSeqIC: InputCache[Seq[ModuleID]] = implicitly - lazy val modulePositionMapFormat: Format[Map[ModuleID, SourcePosition]] = implicitly - - implicit lazy val updateReportFormat: Format[UpdateReport] = - { - import DefaultProtocol.{ StringFormat, FileFormat } - wrap[UpdateReport, (File, Seq[ConfigurationReport], UpdateStats, Map[File, Long])](rep => (rep.cachedDescriptor, rep.configurations, rep.stats, rep.stamps), { case (cd, cs, stats, stamps) => new UpdateReport(cd, cs, stats, stamps) }) - } - implicit def updateStatsFormat: Format[UpdateStats] = - wrap[UpdateStats, (Long, Long, Long)](us => (us.resolveTime, us.downloadTime, us.downloadSize), { case (rt, dt, ds) => new UpdateStats(rt, dt, ds, true) }) - implicit def confReportFormat(implicit m: Format[String], mr: Format[Seq[ModuleReport]], oar: Format[Seq[OrganizationArtifactReport]]): Format[ConfigurationReport] = - wrap[ConfigurationReport, (String, Seq[ModuleReport], Seq[OrganizationArtifactReport])](r => (r.configuration, r.modules, r.details), { case (c, m, d) => new ConfigurationReport(c, m, d) }) - implicit def moduleReportFormat(implicit cf: Format[Seq[Caller]], ff: Format[File]): Format[ModuleReport] = { - wrap[ModuleReport, (ModuleID, Seq[(Artifact, File)], Seq[Artifact], Option[String], Option[Long], Option[String], Option[String], Boolean, Option[String], Option[String], Option[String], Option[String], Map[String, String], Option[Boolean], Option[String], Seq[String], Seq[(String, Option[String])], Seq[Caller])]( - m => (m.module, m.artifacts, m.missingArtifacts, m.status, m.publicationDate map { _.getTime }, m.resolver, m.artifactResolver, m.evicted, m.evictedData, m.evictedReason, m.problem, m.homepage, m.extraAttributes, m.isDefault, m.branch, m.configurations, m.licenses, m.callers), - { case (m, as, ms, s, pd, r, a, e, ed, er, p, h, ea, d, b, cs, ls, ks) => new ModuleReport(m, as, ms, s, pd map { new ju.Date(_) }, r, a, e, ed, er, p, h, ea, d, b, cs, ls, ks) }) - } - implicit def artifactFormat(implicit sf: Format[String], uf: Format[Option[URL]]): Format[Artifact] = { - wrap[Artifact, (String, String, String, Option[String], Seq[Configuration], Option[URL], Map[String, String])]( - a => (a.name, a.`type`, a.extension, a.classifier, a.configurations.toSeq, a.url, a.extraAttributes), - { case (n, t, x, c, cs, u, e) => Artifact(n, t, x, c, cs, u, e) } - ) - } - implicit def organizationArtifactReportFormat(implicit sf: Format[String], bf: Format[Boolean], df: Format[Seq[ModuleReport]]): Format[OrganizationArtifactReport] = - wrap[OrganizationArtifactReport, (String, String, Seq[ModuleReport])](m => (m.organization, m.name, m.modules), { case (o, n, r) => OrganizationArtifactReport(o, n, r) }) - implicit def callerFormat: Format[Caller] = - wrap[Caller, (ModuleID, Seq[String], Map[String, String], Boolean, Boolean, Boolean, Boolean)](c => (c.caller, c.callerConfigurations, c.callerExtraAttributes, c.isForceDependency, c.isChangingDependency, c.isTransitiveDependency, c.isDirectlyForceDependency), - { case (c, cc, ea, fd, cd, td, df) => new Caller(c, cc, ea, fd, cd, td, df) }) - implicit def exclusionRuleFormat(implicit sf: Format[String]): Format[InclExclRule] = - wrap[InclExclRule, (String, String, String, Seq[String])](e => (e.organization, e.name, e.artifact, e.configurations), { case (o, n, a, cs) => InclExclRule(o, n, a, cs) }) - implicit def crossVersionFormat: Format[CrossVersion] = wrap(crossToInt, crossFromInt) - implicit def sourcePositionFormat: Format[SourcePosition] = - wrap[SourcePosition, (Int, String, Int, Int)]( - { - case NoPosition => (0, "", 0, 0) - case LinePosition(p, s) => (1, p, s, 0) - case RangePosition(p, LineRange(s, e)) => (2, p, s, e) - }, - { - case (0, _, _, _) => NoPosition - case (1, p, s, _) => LinePosition(p, s) - case (2, p, s, e) => RangePosition(p, LineRange(s, e)) - } - ) - private[this] final val DisabledValue = 0 - private[this] final val BinaryValue = 1 - private[this] final val FullValue = 2 - - import CrossVersion.{ Binary, Disabled, Full } - private[this] val crossFromInt = (i: Int) => i match { case BinaryValue => new Binary(idFun); case FullValue => new Full(idFun); case _ => Disabled } - private[this] val crossToInt = (c: CrossVersion) => c match { case Disabled => 0; case b: Binary => BinaryValue; case f: Full => FullValue } - - implicit def moduleIDFormat(implicit sf: Format[String], bf: Format[Boolean]): Format[ModuleID] = - wrap[ModuleID, ((String, String, String, Option[String], Option[String]), (Boolean, Boolean, Boolean, Seq[Artifact], Seq[InclusionRule], Seq[ExclusionRule], Map[String, String], CrossVersion))]( - m => ((m.organization, m.name, m.revision, m.configurations, m.branchName), (m.isChanging, m.isTransitive, m.isForce, m.explicitArtifacts, m.inclusions, m.exclusions, m.extraAttributes, m.crossVersion)), - { case ((o, n, r, cs, br), (ch, t, f, as, incl, excl, x, cv)) => ModuleID(o, n, r, cs, ch, t, f, as, incl, excl, x, cv, br) } - ) - // For some reason sbinary seems to detect unserialized instance Set[ModuleID] to be not equal. #1620 - implicit def moduleSetIC: InputCache[Set[ModuleID]] = - { - implicit def toSeq(ms: Set[ModuleID]): Seq[ModuleID] = ms.toSeq.sortBy { _.toString } - wrapIn - } - - implicit def configurationFormat(implicit sf: Format[String]): Format[Configuration] = - wrap[Configuration, String](_.name, s => new Configuration(s)) - - implicit def classpathFormat = - { - import DefaultProtocol.FileFormat - implicitly[Format[Map[String, Seq[File]]]] - } - - object L5 { - implicit def inlineIvyToHL = (i: InlineIvyConfiguration) => i.paths :+: i.resolvers :+: i.otherResolvers :+: i.moduleConfigurations :+: i.localOnly :+: i.checksums :+: HNil - } - import L5._ - - implicit def moduleSettingsIC: InputCache[ModuleSettings] = - unionInputCache[ModuleSettings, PomConfiguration :+: InlineConfiguration :+: IvyFileConfiguration :+: HNil] - - implicit def ivyConfigurationIC: InputCache[IvyConfiguration] = - unionInputCache[IvyConfiguration, InlineIvyConfiguration :+: ExternalIvyConfiguration :+: HNil] - - object L4 { - implicit val inlineToHL = (c: InlineConfiguration) => - c.module :+: c.dependencies :+: c.ivyXML :+: c.configurations :+: c.defaultConfiguration.map(_.name) :+: - c.ivyScala :+: c.validate :+: c.overrides :+: c.excludes :+: HNil - implicit def moduleConfToHL = (m: ModuleConfiguration) => m.organization :+: m.name :+: m.revision :+: m.resolver :+: HNil - // implicit def inlineToHL = (c: InlineConfiguration) => c.module :+: c.dependencies :+: c.ivyXML :+: c.configurations :+: c.defaultConfiguration.map(_.name) :+: c.ivyScala :+: c.validate :+: c.overrides :+: HNil - } - import L4._ - - implicit def inlineIC: InputCache[InlineConfiguration] = wrapIn - implicit def moduleConfIC: InputCache[ModuleConfiguration] = wrapIn - - object L3 { - implicit def mavenCacheToHL = (m: MavenCache) => m.name :+: m.rootFile.getAbsolutePath :+: HNil - implicit def mavenRToHL = (m: MavenRepository) => m.name :+: m.root :+: HNil - implicit def fileRToHL = (r: FileRepository) => r.name :+: r.configuration :+: r.patterns :+: HNil - implicit def urlRToHL = (u: URLRepository) => u.name :+: u.patterns :+: HNil - implicit def sshRToHL = (s: SshRepository) => s.name :+: s.connection :+: s.patterns :+: s.publishPermissions :+: HNil - implicit def sftpRToHL = (s: SftpRepository) => s.name :+: s.connection :+: s.patterns :+: HNil - implicit def rawRToHL = (r: RawRepository) => r.name :+: r.resolver.getClass.getName :+: HNil - implicit def chainRToHL = (c: ChainedResolver) => c.name :+: c.resolvers :+: HNil - implicit def moduleToHL = (m: ModuleID) => m.organization :+: m.name :+: m.revision :+: m.configurations :+: m.isChanging :+: m.isTransitive :+: m.explicitArtifacts :+: m.exclusions :+: m.inclusions :+: m.extraAttributes :+: m.crossVersion :+: HNil - } - import L3._ - - implicit lazy val chainedIC: InputCache[ChainedResolver] = InputCache.lzy(wrapIn) - implicit lazy val resolverIC: InputCache[Resolver] = - unionInputCache[Resolver, ChainedResolver :+: MavenRepository :+: MavenCache :+: FileRepository :+: URLRepository :+: SshRepository :+: SftpRepository :+: RawRepository :+: HNil] - implicit def moduleIC: InputCache[ModuleID] = wrapIn - implicitly[InputCache[Seq[Configuration]]] - - object L2 { - implicit def updateConfToHL = (u: UpdateConfiguration) => u.retrieve :+: u.missingOk :+: HNil - implicit def pomConfigurationHL = (c: PomConfiguration) => hash(c.file) :+: c.ivyScala :+: c.validate :+: HNil - implicit def ivyFileConfigurationHL = (c: IvyFileConfiguration) => hash(c.file) :+: c.ivyScala :+: c.validate :+: HNil - implicit def sshConnectionToHL = (s: SshConnection) => s.authentication :+: s.hostname :+: s.port :+: HNil - - implicit def artifactToHL = (a: Artifact) => a.name :+: a.`type` :+: a.extension :+: a.classifier :+: names(a.configurations) :+: a.url :+: a.extraAttributes :+: HNil - implicit def inclExclToHL = (e: InclExclRule) => e.organization :+: e.name :+: e.artifact :+: e.configurations :+: HNil - implicit def sbtExclusionToHL = (e: SbtExclusionRule) => e.organization :+: e.name :+: e.artifact :+: e.configurations :+: e.crossVersion :+: HNil - implicit def crossToHL = (c: CrossVersion) => crossToInt(c) :+: HNil - - /* implicit def deliverConfToHL = (p: DeliverConfiguration) => p.deliverIvyPattern :+: p.status :+: p.configurations :+: HNil - implicit def publishConfToHL = (p: PublishConfiguration) => p.ivyFile :+: p.resolverName :+: p.artifacts :+: HNil*/ - } - import L2._ - - implicit def updateConfIC: InputCache[UpdateConfiguration] = wrapIn - implicit def pomIC: InputCache[PomConfiguration] = wrapIn - implicit def ivyFileIC: InputCache[IvyFileConfiguration] = wrapIn - implicit def connectionIC: InputCache[SshConnection] = wrapIn - implicit def artifactIC: InputCache[Artifact] = wrapIn - implicit def exclusionIC: InputCache[InclExclRule] = wrapIn - implicit def sbtExclusionIC: InputCache[SbtExclusionRule] = wrapIn - implicit def crossVersionIC: InputCache[CrossVersion] = wrapIn - /* implicit def publishConfIC: InputCache[PublishConfiguration] = wrapIn - implicit def deliverConfIC: InputCache[DeliverConfiguration] = wrapIn*/ - - object L1 { - implicit def retrieveToHL = (r: RetrieveConfiguration) => exists(r.retrieveDirectory) :+: r.outputPattern :+: HNil - implicit def ivyPathsToHL = (p: IvyPaths) => exists(p.baseDirectory) :+: p.ivyHome.map(exists.apply) :+: HNil - implicit def ivyScalaHL = (i: IvyScala) => i.scalaFullVersion :+: i.scalaBinaryVersion :+: names(i.configurations) :+: i.checkExplicit :+: i.filterImplicit :+: HNil - implicit def configurationToHL = (c: Configuration) => c.name :+: c.description :+: c.isPublic :+: names(c.extendsConfigs) :+: c.transitive :+: HNil - - implicit def passwordToHL = (s: PasswordAuthentication) => Hash(s.user) :+: password(s.password) :+: HNil - implicit def keyFileToHL = (s: KeyFileAuthentication) => Hash(s.user) :+: hash(s.keyfile) :+: password(s.password) :+: HNil - - implicit def patternsToHL = (p: Patterns) => p.ivyPatterns :+: p.artifactPatterns :+: p.isMavenCompatible :+: HNil - implicit def fileConfToHL = (f: FileConfiguration) => f.isLocal :+: f.isTransactional :+: HNil - - implicit def externalIvyConfigurationToHL = (e: ExternalIvyConfiguration) => - exists(e.baseDirectory) :+: Hash.contentsIfLocal(e.uri) :+: HNil - } - import L1._ - - implicit def ivyScalaIC: InputCache[IvyScala] = wrapIn - implicit def ivyPathsIC: InputCache[IvyPaths] = wrapIn - implicit def retrieveIC: InputCache[RetrieveConfiguration] = wrapIn - implicit def patternsIC: InputCache[Patterns] = wrapIn - implicit def fileConfIC: InputCache[FileConfiguration] = wrapIn - implicit def extIvyIC: InputCache[ExternalIvyConfiguration] = wrapIn - implicit def confIC: InputCache[Configuration] = wrapIn - - implicit def authIC: InputCache[SshAuthentication] = - unionInputCache[SshAuthentication, PasswordAuthentication :+: KeyFileAuthentication :+: HNil] -} diff --git a/main-actions/src/main/scala/sbt/Compiler.scala b/main-actions/src/main/scala/sbt/Compiler.scala deleted file mode 100644 index ca69ca5bc..000000000 --- a/main-actions/src/main/scala/sbt/Compiler.scala +++ /dev/null @@ -1,156 +0,0 @@ -/* sbt -- Simple Build Tool - * Copyright 2010 Mark Harrah - */ -package sbt - -import sbt.internal.inc.javac.JavaTools -import sbt.internal.inc.{ AnalyzingCompiler, ComponentCompiler, ScalaInstance } -import xsbti.{ Logger => _, _ } -import xsbti.compile.{ ClasspathOptions, Compilers, CompileResult, Inputs } -import java.io.File - -import sbt.internal.librarymanagement.{ ComponentManager, IvyConfiguration } -import sbt.librarymanagement.{ ModuleID, VersionNumber } -import sbt.util.Logger - -object Compiler { - val DefaultMaxErrors = 100 - private[sbt] def defaultCompilerBridgeSource(sv: String): ModuleID = - VersionNumber(sv) match { - case VersionNumber(ns, _, _) if (ns.size == 3) && (ns(0) == 2) && (ns(1) <= 10) => scalaCompilerBridgeSource2_10 - case _ => scalaCompilerBridgeSource2_11 - } - private[sbt] def scalaCompilerBridgeSource2_10: ModuleID = - ModuleID(xsbti.ArtifactInfo.SbtOrganization, "compiler-bridge_2.10", - ComponentCompiler.incrementalVersion, Some("component")).sources() - private[sbt] def scalaCompilerBridgeSource2_11: ModuleID = - ModuleID(xsbti.ArtifactInfo.SbtOrganization, "compiler-bridge_2.11", - ComponentCompiler.incrementalVersion, Some("component")).sources() - - /** Inputs necessary to run the incremental compiler. */ - // final case class Inputs(compilers: Compilers, config: Options, incSetup: IncSetup) - // /** The inputs for the copiler *and* the previous analysis of source dependecnies. */ - // final case class InputsWithPrevious(inputs: Inputs, previousAnalysis: PreviousAnalysis) - // final case class Options(classpath: Seq[File], sources: Seq[File], classesDirectory: File, options: Seq[String], javacOptions: Seq[String], maxErrors: Int, sourcePositionMapper: Position => Position, order: CompileOrder) - // final case class IncSetup(analysisMap: File => Option[Analysis], definesClass: DefinesClass, skip: Boolean, cacheFile: File, cache: GlobalsCache, incOptions: IncOptions) - - // private[sbt] trait JavaToolWithNewInterface extends JavaTool { - // def newJavac: IncrementalCompilerJavaTools - // } - /** The instances of Scalac/Javac used to compile the current project. */ - // final case class Compilers(scalac: AnalyzingCompiler, javac: IncrementalCompilerJavaTools) - - /** The previous source dependency analysis result from compilation. */ - // final case class PreviousAnalysis(analysis: Analysis, setup: Option[MiniSetup]) - - // def inputs(classpath: Seq[File], sources: Seq[File], classesDirectory: File, options: Seq[String], - // javacOptions: Seq[String], maxErrors: Int, sourcePositionMappers: Seq[Position => Option[Position]], - // order: CompileOrder)(implicit compilers: Compilers, incSetup: IncSetup, log: Logger): Inputs = - // new Inputs( - // compilers, - // new Options(classpath, sources, classesDirectory, options, javacOptions, maxErrors, foldMappers(sourcePositionMappers), order), - // incSetup - // ) - - // @deprecated("Use `compilers(ScalaInstance, ClasspathOptions, Option[File], IvyConfiguration)`.", "0.13.10") - // def compilers(instance: ScalaInstance, cpOptions: ClasspathOptions, javaHome: Option[File])(implicit app: AppConfiguration, log: Logger): Compilers = - // { - // val javac = - // AggressiveCompile.directOrFork(instance, cpOptions, javaHome) - // val javac2 = - // JavaTools.directOrFork(instance, cpOptions, javaHome) - // // Hackery to enable both the new and deprecated APIs to coexist peacefully. - // case class CheaterJavaTool(newJavac: IncrementalCompilerJavaTools, delegate: JavaTool) extends JavaTool with JavaToolWithNewInterface { - // def compile(contract: JavacContract, sources: Seq[File], classpath: Seq[File], outputDirectory: File, options: Seq[String])(implicit log: Logger): Unit = - // javac.compile(contract, sources, classpath, outputDirectory, options)(log) - // def onArgs(f: Seq[String] => Unit): JavaTool = CheaterJavaTool(newJavac, delegate.onArgs(f)) - // } - // compilers(instance, cpOptions, CheaterJavaTool(javac2, javac)) - // } - // def compilers(instance: ScalaInstance, cpOptions: ClasspathOptions, javaHome: Option[File], ivyConfiguration: IvyConfiguration)(implicit app: AppConfiguration, log: Logger): Compilers = - // { - // val javac = - // AggressiveCompile.directOrFork(instance, cpOptions, javaHome) - // val javac2 = - // JavaTools.directOrFork(instance, cpOptions, javaHome) - // // Hackery to enable both the new and deprecated APIs to coexist peacefully. - // case class CheaterJavaTool(newJavac: IncrementalCompilerJavaTools, delegate: JavaTool) extends JavaTool with JavaToolWithNewInterface { - // def compile(contract: JavacContract, sources: Seq[File], classpath: Seq[File], outputDirectory: File, options: Seq[String])(implicit log: Logger): Unit = - // javac.compile(contract, sources, classpath, outputDirectory, options)(log) - // def onArgs(f: Seq[String] => Unit): JavaTool = CheaterJavaTool(newJavac, delegate.onArgs(f)) - // } - // val scalac = scalaCompiler(instance, cpOptions, ivyConfiguration) - // new Compilers(scalac, CheaterJavaTool(javac2, javac)) - // } - // @deprecated("Deprecated in favor of new sbt.compiler.javac package.", "0.13.8") - // def compilers(instance: ScalaInstance, cpOptions: ClasspathOptions, javac: sbt.compiler.JavaCompiler.Fork)(implicit app: AppConfiguration, log: Logger): Compilers = - // { - // val javaCompiler = sbt.compiler.JavaCompiler.fork(cpOptions, instance)(javac) - // compilers(instance, cpOptions, javaCompiler) - // } - // @deprecated("Deprecated in favor of new sbt.compiler.javac package.", "0.13.8") - // def compilers(instance: ScalaInstance, cpOptions: ClasspathOptions, javac: JavaTool)(implicit app: AppConfiguration, log: Logger): Compilers = - // { - // val scalac = scalaCompiler(instance, cpOptions) - // new Compilers(scalac, javac) - // } - // @deprecated("Use `scalaCompiler(ScalaInstance, ClasspathOptions, IvyConfiguration)`.", "0.13.10") - // def scalaCompiler(instance: ScalaInstance, cpOptions: ClasspathOptions)(implicit app: AppConfiguration, log: Logger): AnalyzingCompiler = - // { - // val launcher = app.provider.scalaProvider.launcher - // val componentManager = new ComponentManager(launcher.globalLock, app.provider.components, Option(launcher.ivyHome), log) - // val provider = ComponentCompiler.interfaceProvider(componentManager) - // new AnalyzingCompiler(instance, provider, cpOptions) - // } - - def compilers(cpOptions: ClasspathOptions, ivyConfiguration: IvyConfiguration)(implicit app: AppConfiguration, log: Logger): Compilers = - { - val scalaProvider = app.provider.scalaProvider - val instance = ScalaInstance(scalaProvider.version, scalaProvider.launcher) - val sourceModule = scalaCompilerBridgeSource2_11 - compilers(instance, cpOptions, None, ivyConfiguration, sourceModule) - } - - // def compilers(instance: ScalaInstance, cpOptions: ClasspathOptions)(implicit app: AppConfiguration, log: Logger): Compilers = - // compilers(instance, cpOptions, None) - - // TODO: Get java compiler - def compilers(instance: ScalaInstance, cpOptions: ClasspathOptions, javaHome: Option[File], - ivyConfiguration: IvyConfiguration, sourcesModule: ModuleID)(implicit app: AppConfiguration, log: Logger): Compilers = { - val scalac = scalaCompiler(instance, cpOptions, javaHome, ivyConfiguration, sourcesModule) - val javac = JavaTools.directOrFork(instance, cpOptions, javaHome) - new Compilers(scalac, javac) - } - def scalaCompiler(instance: ScalaInstance, cpOptions: ClasspathOptions, javaHome: Option[File], ivyConfiguration: IvyConfiguration, sourcesModule: ModuleID)(implicit app: AppConfiguration, log: Logger): AnalyzingCompiler = - { - val launcher = app.provider.scalaProvider.launcher - val componentManager = new ComponentManager(launcher.globalLock, app.provider.components, Option(launcher.ivyHome), log) - val provider = ComponentCompiler.interfaceProvider(componentManager, ivyConfiguration, sourcesModule) - new AnalyzingCompiler(instance, provider, cpOptions) - } - - def compile(in: Inputs, log: Logger): CompileResult = - { - sbt.inc.IncrementalCompilerUtil.defaultIncrementalCompiler.compile(in, log) - // import in.inputs.config._ - // compile(in, log, new LoggerReporter(maxErrors, log, sourcePositionMapper)) - } - // def compile(in: Inputs, log: Logger, reporter: xsbti.Reporter): CompileResult = - // { - // import in.inputs.compilers._ - // import in.inputs.config._ - // import in.inputs.incSetup._ - // // Here is some trickery to choose the more recent (reporter-using) java compiler rather - // // than the previously defined versions. - // // TODO - Remove this hackery in sbt 1.0. - // val javacChosen: xsbti.compile.JavaCompiler = - // in.inputs.compilers.javac.xsbtiCompiler // ).getOrElse(in.inputs.compilers.javac) - // // TODO - Why are we not using the IC interface??? - // val compiler = new IncrementalCompilerImpl - // compiler.incrementalCompile(scalac, javacChosen, sources, classpath, CompileOutput(classesDirectory), cache, None, options, javacOptions, - // in.previousAnalysis.analysis, in.previousAnalysis.setup, analysisMap, definesClass, reporter, order, skip, incOptions)(log) - // } - - private[sbt] def foldMappers[A](mappers: Seq[A => Option[A]]) = - mappers.foldRight({ p: A => p }) { (mapper, mappers) => { p: A => mapper(p).getOrElse(mappers(p)) } } -} diff --git a/main-actions/src/main/scala/sbt/Console.scala b/main-actions/src/main/scala/sbt/Console.scala index b9a721ef8..a40c12a27 100644 --- a/main-actions/src/main/scala/sbt/Console.scala +++ b/main-actions/src/main/scala/sbt/Console.scala @@ -1,33 +1,85 @@ -/* sbt -- Simple Build Tool - * Copyright 2008, 2009, 2010 Mark Harrah +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) */ + package sbt import java.io.File -import sbt.internal.inc.AnalyzingCompiler +import java.nio.channels.ClosedChannelException +import sbt.internal.inc.{ AnalyzingCompiler, MappedFileConverter, PlainVirtualFile } +import sbt.internal.util.{ DeprecatedJLine, Terminal } import sbt.util.Logger +import xsbti.compile.{ Compilers, Inputs } -import xsbti.compile.{ Inputs, Compilers } +import scala.util.Try final class Console(compiler: AnalyzingCompiler) { + /** Starts an interactive scala interpreter session with the given classpath.*/ - def apply(classpath: Seq[File], log: Logger): Option[String] = + def apply(classpath: Seq[File], log: Logger): Try[Unit] = apply(classpath, Nil, "", "", log) - def apply(classpath: Seq[File], options: Seq[String], initialCommands: String, cleanupCommands: String, log: Logger): Option[String] = + def apply( + classpath: Seq[File], + options: Seq[String], + initialCommands: String, + cleanupCommands: String, + log: Logger + ): Try[Unit] = apply(classpath, options, initialCommands, cleanupCommands)(None, Nil)(log) - def apply(classpath: Seq[File], options: Seq[String], loader: ClassLoader, initialCommands: String, cleanupCommands: String)(bindings: (String, Any)*)(implicit log: Logger): Option[String] = + def apply( + classpath: Seq[File], + options: Seq[String], + loader: ClassLoader, + initialCommands: String, + cleanupCommands: String + )(bindings: (String, Any)*)(implicit log: Logger): Try[Unit] = apply(classpath, options, initialCommands, cleanupCommands)(Some(loader), bindings) - def apply(classpath: Seq[File], options: Seq[String], initialCommands: String, cleanupCommands: String)(loader: Option[ClassLoader], bindings: Seq[(String, Any)])(implicit log: Logger): Option[String] = - { - def console0() = compiler.console(classpath, options, initialCommands, cleanupCommands, log)(loader, bindings) - // TODO: Fix JLine - //JLine.withJLine(Run.executeTrapExit(console0, log)) - Run.executeTrapExit(console0, log) + def apply( + classpath: Seq[File], + options: Seq[String], + initialCommands: String, + cleanupCommands: String + )(loader: Option[ClassLoader], bindings: Seq[(String, Any)])(implicit log: Logger): Try[Unit] = { + apply(classpath, options, initialCommands, cleanupCommands, Terminal.get)(loader, bindings) + } + def apply( + classpath: Seq[File], + options: Seq[String], + initialCommands: String, + cleanupCommands: String, + terminal: Terminal + )(loader: Option[ClassLoader], bindings: Seq[(String, Any)])(implicit log: Logger): Try[Unit] = { + def console0(): Unit = + try { + compiler.console(classpath map { x => + PlainVirtualFile(x.toPath) + }, MappedFileConverter.empty, options, initialCommands, cleanupCommands, log)( + loader, + bindings + ) + } catch { case _: InterruptedException | _: ClosedChannelException => } + val previous = sys.props.get("scala.color").getOrElse("auto") + val jline3term = sbt.internal.util.JLine3(terminal) + try { + sys.props("scala.color") = if (terminal.isColorEnabled) "true" else "false" + terminal.withRawOutput { + jline.TerminalFactory.set(terminal.toJLine) + DeprecatedJLine.setTerminalOverride(jline3term) + terminal.withRawInput(Run.executeTrapExit(console0, log)) + } + } finally { + sys.props("scala.color") = previous + jline3term.close() } + } } + object Console { def apply(conf: Inputs): Console = conf.compilers match { diff --git a/main-actions/src/main/scala/sbt/Doc.scala b/main-actions/src/main/scala/sbt/Doc.scala index f21d5eff1..798e0b8d4 100644 --- a/main-actions/src/main/scala/sbt/Doc.scala +++ b/main-actions/src/main/scala/sbt/Doc.scala @@ -1,88 +1,85 @@ -/* sbt -- Simple Build Tool - * Copyright 2008, 2009, 2010, 2011 Mark Harrah, Indrajit Raychaudhuri +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) */ + package sbt import java.io.File -import sbt.internal.inc.AnalyzingCompiler - -import Predef.{ conforms => _, _ } -import sbt.io.syntax._ -import sbt.io.IO - +import sbt.internal.inc.{ AnalyzingCompiler, PlainVirtualFile } +import sbt.internal.util.ManagedLogger +import sbt.util.CacheStoreFactory +import sbt.util.Logger import xsbti.Reporter import xsbti.compile.JavaTools - -import sbt.util.Logger +import sbt.internal.inc.MappedFileConverter object Doc { import RawCompileLike._ - def scaladoc(label: String, cache: File, compiler: AnalyzingCompiler): Gen = - scaladoc(label, cache, compiler, Seq()) - def scaladoc(label: String, cache: File, compiler: AnalyzingCompiler, fileInputOptions: Seq[String]): Gen = - cached(cache, fileInputOptions, prepare(label + " Scala API documentation", compiler.doc)) - def javadoc(label: String, cache: File, doc: JavaTools, log: Logger, reporter: Reporter): Gen = - javadoc(label, cache, doc, log, reporter, Seq()) - def javadoc(label: String, cache: File, doc: JavaTools, log: Logger, reporter: Reporter, fileInputOptions: Seq[String]): Gen = - cached(cache, fileInputOptions, prepare(label + " Java API documentation", filterSources(javaSourcesOnly, - (sources: Seq[File], classpath: Seq[File], outputDirectory: File, options: Seq[String], maxErrors: Int, log: Logger) => { - // doc.doc - ??? - }))) + def scaladoc( + label: String, + cacheStoreFactory: CacheStoreFactory, + compiler: AnalyzingCompiler + ): Gen = + scaladoc(label, cacheStoreFactory, compiler, Seq()) + + def scaladoc( + label: String, + cacheStoreFactory: CacheStoreFactory, + compiler: AnalyzingCompiler, + fileInputOptions: Seq[String] + ): Gen = + cached( + cacheStoreFactory, + fileInputOptions, + prepare( + label + " Scala API documentation", + (sources, classpath, outputDirectory, options, maxErrors, log) => { + compiler.doc( + sources map { x => + PlainVirtualFile(x.toPath) + }, + classpath map { x => + PlainVirtualFile(x.toPath) + }, + MappedFileConverter.empty, + outputDirectory.toPath, + options, + maxErrors, + log + ) + } + ) + ) + + @deprecated("Going away", "1.1.1") + def javadoc( + label: String, + cacheStoreFactory: CacheStoreFactory, + doc: JavaTools, + log: Logger, + reporter: Reporter, + ): Gen = ??? + + @deprecated("Going away", "1.1.1") + def javadoc( + label: String, + cacheStoreFactory: CacheStoreFactory, + doc: JavaTools, + log: Logger, + reporter: Reporter, + fileInputOptions: Seq[String], + ): Gen = ??? + + @deprecated("Going away", "1.1.1") val javaSourcesOnly: File => Boolean = _.getName.endsWith(".java") - - // @deprecated("Use `scaladoc`", "0.13.0") - // def apply(maximumErrors: Int, compiler: AnalyzingCompiler) = new Scaladoc(maximumErrors, compiler) - - // @deprecated("Use `javadoc`", "0.13.0") - // def apply(maximumErrors: Int, compiler: sbt.compiler.Javadoc) = new Javadoc(maximumErrors, compiler) - - private[sbt] final class Scaladoc(maximumErrors: Int, compiler: AnalyzingCompiler) extends Doc { - def apply(label: String, sources: Seq[File], classpath: Seq[File], outputDirectory: File, options: Seq[String], log: Logger): Unit = { - generate("Scala", label, compiler.doc, sources, classpath, outputDirectory, options, maximumErrors, log) - } - } - - // private[sbt] final class Javadoc(maximumErrors: Int, doc: sbt.internal.inc.Javadoc) extends Doc { - // def apply(label: String, sources: Seq[File], classpath: Seq[File], outputDirectory: File, options: Seq[String], log: Logger) { - // // javadoc doesn't handle *.scala properly, so we evict them from javadoc sources list. - // generate("Java", label, doc.doc, sources.filterNot(_.name.endsWith(".scala")), classpath, outputDirectory, options, maximumErrors, log) - // } - // } } -// @deprecated("No longer used. See `Doc.javadoc` or `Doc.scaladoc`", "0.13.0") + +@deprecated("Going away", "1.1.1") sealed trait Doc { - type Gen = (Seq[File], Seq[File], File, Seq[String], Int, Logger) => Unit - - // def apply(label: String, sources: Seq[File], classpath: Seq[File], outputDirectory: File, options: Seq[String], log: Logger): Unit - - private[sbt] final def generate(variant: String, label: String, docf: Gen, sources: Seq[File], classpath: Seq[File], outputDirectory: File, options: Seq[String], maxErrors: Int, log: Logger): Unit = { - val logSnip = variant + " API documentation" - if (sources.isEmpty) - log.info("No sources available, skipping " + logSnip + "...") - else { - log.info("Generating " + logSnip + " for " + label + " sources to " + outputDirectory.absolutePath + "...") - IO.delete(outputDirectory) - IO.createDirectory(outputDirectory) - docf(sources, classpath, outputDirectory, options, maxErrors, log) - log.info(logSnip + " generation successful.") - } - } - - // def cached(cache: File, label: String, sources: Seq[File], classpath: Seq[File], outputDirectory: File, options: Seq[String], log: Logger) { - // type Inputs = FilesInfo[HashFileInfo] :+: FilesInfo[ModifiedFileInfo] :+: String :+: File :+: Seq[String] :+: HNil - // val inputs: Inputs = hash(sources.toSet) :+: lastModified(classpath.toSet) :+: classpath.absString :+: outputDirectory :+: options :+: HNil - // implicit val stringEquiv: Equiv[String] = defaultEquiv - // implicit val fileEquiv: Equiv[File] = defaultEquiv - // val cachedDoc = inputChanged(cache / "inputs") { (inChanged, in: Inputs) => - // outputChanged(cache / "output") { (outChanged, outputs: FilesInfo[PlainFileInfo]) => - // if (inChanged || outChanged) - // apply(label, sources, classpath, outputDirectory, options, log) - // else - // log.debug("Doc uptodate: " + outputDirectory.getAbsolutePath) - // } - // } - // cachedDoc(inputs)(() => exists(outputDirectory.allPaths.get.toSet)) - // } + @deprecated("Going away", "1.1.1") + type Gen = (Seq[File], Seq[File], File, Seq[String], Int, ManagedLogger) => Unit } diff --git a/main-actions/src/main/scala/sbt/DotGraph.scala b/main-actions/src/main/scala/sbt/DotGraph.scala index 46d33503a..6d39109a3 100644 --- a/main-actions/src/main/scala/sbt/DotGraph.scala +++ b/main-actions/src/main/scala/sbt/DotGraph.scala @@ -1,41 +1,38 @@ -/* sbt -- Simple Build Tool - * Copyright 2008, 2009, 2010 Mark Harrah +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) */ + package sbt import java.io.File import sbt.internal.inc.Relations - import sbt.internal.util.Relation import sbt.io.IO object DotGraph { - private def fToString(roots: Iterable[File]): (File => String) = - (x: File) => sourceToString(roots, x) - def sources(relations: Relations, outputDirectory: File, sourceRoots: Iterable[File]): Unit = { - val toString = fToString(sourceRoots) - apply(relations, outputDirectory, toString, toString) - } - def packages(relations: Relations, outputDirectory: File, sourceRoots: Iterable[File]): Unit = { - val packageOnly = (path: String) => - { - val last = path.lastIndexOf(File.separatorChar.toInt) - val packagePath = (if (last > 0) path.substring(0, last) else path).trim - if (packagePath.isEmpty) "" else packagePath.replace(File.separatorChar, '.') - } - val toString = packageOnly compose fToString(sourceRoots) - apply(relations, outputDirectory, toString, toString) - } - def apply(relations: Relations, outputDir: File, sourceToString: File => String, externalToString: File => String): Unit = { - def file(name: String) = new File(outputDir, name) - IO.createDirectory(outputDir) - generateGraph(file("int-class-deps"), "dependencies", relations.internalClassDep, identity[String], identity[String]) - generateGraph(file("binary-dependencies"), "externalDependencies", relations.binaryDep, externalToString, sourceToString) - } + @deprecated("not used", "1.4.0") + def sources(relations: Relations, outputDirectory: File, sourceRoots: Iterable[File]): Unit = ??? + @deprecated("not used", "1.4.0") + def packages(relations: Relations, outputDirectory: File, sourceRoots: Iterable[File]): Unit = ??? + @deprecated("not used", "1.4.0") + def apply( + relations: Relations, + outputDir: File, + sourceToString: File => String, + externalToString: File => String + ): Unit = ??? - def generateGraph[K, V](file: File, graphName: String, relation: Relation[K, V], - keyToString: K => String, valueToString: V => String): Unit = { + def generateGraph[K, V]( + file: File, + graphName: String, + relation: Relation[K, V], + keyToString: K => String, + valueToString: V => String + ): Unit = { import scala.collection.mutable.{ HashMap, HashSet } val mappedGraph = new HashMap[String, HashSet[String]] for ((key, values) <- relation.forwardMap; keyString = keyToString(key); value <- values) @@ -58,10 +55,9 @@ object DotGraph { def sourceToString(roots: Iterable[File], source: File) = relativized(roots, source).trim.stripSuffix(".scala").stripSuffix(".java") - private def relativized(roots: Iterable[File], path: File): String = - { - val relativized = roots.flatMap(root => IO.relativize(root, path)) - val shortest = (Int.MaxValue /: relativized)(_ min _.length) - relativized.find(_.length == shortest).getOrElse(path.getName) - } + private def relativized(roots: Iterable[File], path: File): String = { + val relativized = roots.flatMap(root => IO.relativize(root, path)) + val shortest = relativized.foldLeft(Int.MaxValue)(_ min _.length) + relativized.find(_.length == shortest).getOrElse(path.getName) + } } diff --git a/main-actions/src/main/scala/sbt/ForkTests.scala b/main-actions/src/main/scala/sbt/ForkTests.scala index 6f6cb97a3..ddc01a850 100755 --- a/main-actions/src/main/scala/sbt/ForkTests.scala +++ b/main-actions/src/main/scala/sbt/ForkTests.scala @@ -1,20 +1,35 @@ -/* sbt -- Simple Build Tool - * Copyright 2012 Eugene Vigdorchik +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) */ + package sbt import scala.collection.mutable -import testing.{ Logger => _, _ } +import testing.{ Logger => _, Task => _, _ } +import scala.util.control.NonFatal import java.net.ServerSocket import java.io._ import Tests.{ Output => TestOutput, _ } import sbt.io.IO import sbt.util.Logger +import sbt.ConcurrentRestrictions.Tag +import sbt.protocol.testing._ +import sbt.internal.util.Util.{ AnyOps, none } +import sbt.internal.util.{ RunningProcesses, Terminal => UTerminal } private[sbt] object ForkTests { - def apply(runners: Map[TestFramework, Runner], tests: List[TestDefinition], config: Execution, classpath: Seq[File], fork: ForkOptions, log: Logger): Task[TestOutput] = { - val opts = processOptions(config, tests, log) - + def apply( + runners: Map[TestFramework, Runner], + opts: ProcessedOptions, + config: Execution, + classpath: Seq[File], + fork: ForkOptions, + log: Logger, + tags: (Tag, Int)* + ): Task[TestOutput] = { import std.TaskExtra._ val dummyLoader = this.getClass.getClassLoader // can't provide the loader for test classes, which is in another jvm def all(work: Seq[ClassLoader => Unit]) = work.fork(f => f(dummyLoader)) @@ -24,22 +39,55 @@ private[sbt] object ForkTests { constant(TestOutput(TestResult.Passed, Map.empty[String, SuiteResult], Iterable.empty)) else mainTestTask(runners, opts, classpath, fork, log, config.parallel).tagw(config.tags: _*) - main.dependsOn(all(opts.setup): _*) flatMap { results => + main.tagw(tags: _*).dependsOn(all(opts.setup): _*) flatMap { results => all(opts.cleanup).join.map(_ => results) } } - private[this] def mainTestTask(runners: Map[TestFramework, Runner], opts: ProcessedOptions, classpath: Seq[File], fork: ForkOptions, log: Logger, parallel: Boolean): Task[TestOutput] = + def apply( + runners: Map[TestFramework, Runner], + tests: Vector[TestDefinition], + config: Execution, + classpath: Seq[File], + fork: ForkOptions, + log: Logger, + tags: (Tag, Int)* + ): Task[TestOutput] = { + val opts = processOptions(config, tests, log) + apply(runners, opts, config, classpath, fork, log, tags: _*) + } + + def apply( + runners: Map[TestFramework, Runner], + tests: Vector[TestDefinition], + config: Execution, + classpath: Seq[File], + fork: ForkOptions, + log: Logger, + tag: Tag + ): Task[TestOutput] = { + apply(runners, tests, config, classpath, fork, log, tag -> 1) + } + + private[this] def mainTestTask( + runners: Map[TestFramework, Runner], + opts: ProcessedOptions, + classpath: Seq[File], + fork: ForkOptions, + log: Logger, + parallel: Boolean + ): Task[TestOutput] = std.TaskExtra.task { val server = new ServerSocket(0) val testListeners = opts.testListeners flatMap { - case tl: TestsListener => Some(tl) - case _ => None + case tl: TestsListener => tl.some + case _ => none[TestsListener] } object Acceptor extends Runnable { val resultsAcc = mutable.Map.empty[String, SuiteResult] - lazy val result = TestOutput(overall(resultsAcc.values.map(_.result)), resultsAcc.toMap, Iterable.empty) + lazy val result = + TestOutput(overall(resultsAcc.values.map(_.result)), resultsAcc.toMap, Iterable.empty) def run(): Unit = { val socket = @@ -47,7 +95,9 @@ private[sbt] object ForkTests { server.accept() } catch { case e: java.net.SocketException => - log.error("Could not accept connection from test agent: " + e.getClass + ": " + e.getMessage) + log.error( + "Could not accept connection from test agent: " + e.getClass + ": " + e.getMessage + ) log.trace(e) server.close() return @@ -58,10 +108,17 @@ private[sbt] object ForkTests { val is = new ObjectInputStream(socket.getInputStream) try { - val config = new ForkConfiguration(log.ansiCodesSupported, parallel) + val config = new ForkConfiguration(UTerminal.isAnsiSupported, parallel) os.writeObject(config) - val taskdefs = opts.tests.map(t => new TaskDef(t.name, forkFingerprint(t.fingerprint), t.explicitlySpecified, t.selectors)) + val taskdefs = opts.tests.map { t => + new TaskDef( + t.name, + forkFingerprint(t.fingerprint), + t.explicitlySpecified, + t.selectors + ) + } os.writeObject(taskdefs.toArray) os.writeInt(runners.size) @@ -73,6 +130,13 @@ private[sbt] object ForkTests { os.flush() new React(is, os, log, opts.testListeners, resultsAcc).react() + } catch { + case NonFatal(e) => + def throwableToString(t: Throwable) = { + import java.io._; val sw = new StringWriter; t.printStackTrace(new PrintWriter(sw)); + sw.toString + } + resultsAcc("Forked test harness failed: " + throwableToString(e)) = SuiteResult.Error } finally { is.close(); os.close(); socket.close() } @@ -84,12 +148,33 @@ private[sbt] object ForkTests { val acceptorThread = new Thread(Acceptor) acceptorThread.start() - val fullCp = classpath ++: Seq(IO.classLocationFile[ForkMain], IO.classLocationFile[Framework]) - val options = Seq("-classpath", fullCp mkString File.pathSeparator, classOf[ForkMain].getCanonicalName, server.getLocalPort.toString) - val ec = Fork.java(fork, options) + val fullCp = classpath ++ Seq( + IO.classLocationPath[ForkMain].toFile, + IO.classLocationPath[Framework].toFile + ) + val options = Seq( + "-classpath", + fullCp mkString File.pathSeparator, + classOf[ForkMain].getCanonicalName, + server.getLocalPort.toString + ) + val p = Fork.java.fork(fork, options) + RunningProcesses.add(p) + val ec = try p.exitValue() + finally { + if (p.isAlive) p.destroy() + RunningProcesses.remove(p) + } val result = if (ec != 0) - TestOutput(TestResult.Error, Map("Running java with options " + options.mkString(" ") + " failed with exit code " + ec -> SuiteResult.Error), Iterable.empty) + TestOutput( + TestResult.Error, + Map( + "Running java with options " + options + .mkString(" ") + " failed with exit code " + ec -> SuiteResult.Error + ), + Iterable.empty + ) else { // Need to wait acceptor thread to finish its business acceptorThread.join() @@ -110,9 +195,16 @@ private[sbt] object ForkTests { case _ => sys.error("Unknown fingerprint type: " + f.getClass) } } -private final class React(is: ObjectInputStream, os: ObjectOutputStream, log: Logger, listeners: Seq[TestReportListener], results: mutable.Map[String, SuiteResult]) { +private final class React( + is: ObjectInputStream, + os: ObjectOutputStream, + log: Logger, + listeners: Seq[TestReportListener], + results: mutable.Map[String, SuiteResult] +) { import ForkTags._ - @annotation.tailrec def react(): Unit = is.readObject match { + @annotation.tailrec + def react(): Unit = is.readObject match { case `Done` => os.writeObject(Done); os.flush() case Array(`Error`, s: String) => diff --git a/main-actions/src/main/scala/sbt/Package.scala b/main-actions/src/main/scala/sbt/Package.scala index 63f2fa395..e083cc494 100644 --- a/main-actions/src/main/scala/sbt/Package.scala +++ b/main-actions/src/main/scala/sbt/Package.scala @@ -1,37 +1,83 @@ -/* sbt -- Simple Build Tool - * Copyright 2010 Mark Harrah +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) */ + package sbt -import Predef.{ conforms => _, _ } import java.io.File +import java.time.OffsetDateTime import java.util.jar.{ Attributes, Manifest } -import collection.JavaConverters._ +import scala.collection.JavaConverters._ import sbt.internal.util.Types.:+: -import sbt.io.syntax._ import sbt.io.IO -import sbinary.{ DefaultProtocol, Format } -import DefaultProtocol.{ FileFormat, immutableMapFormat, StringFormat } -import sbt.internal.util.{ Cache, FileInfo, FilesInfo, HNil, ModifiedFileInfo, PlainFileInfo, Tracked } -import Cache.{ defaultEquiv, hConsCache, hNilCache, streamFormat } -import Tracked.{ inputChanged, outputChanged } -import FileInfo.exists -import FilesInfo.lastModified +import sjsonnew.JsonFormat import sbt.util.Logger +import sbt.util.{ CacheStoreFactory, FilesInfo, ModifiedFileInfo, PlainFileInfo } +import sbt.internal.util.HNil +import sbt.internal.util.HListFormats._ +import sbt.util.FileInfo.{ exists, lastModified } +import sbt.util.CacheImplicits._ +import sbt.util.Tracked.{ inputChanged, outputChanged } +import scala.sys.process.Process + sealed trait PackageOption + +/** + * == Package == + * + * This module provides an API to package jar files. + * + * @see [[https://docs.oracle.com/javase/tutorial/deployment/jar/index.html]] + */ object Package { final case class JarManifest(m: Manifest) extends PackageOption { assert(m != null) } final case class MainClass(mainClassName: String) extends PackageOption final case class ManifestAttributes(attributes: (Attributes.Name, String)*) extends PackageOption - def ManifestAttributes(attributes: (String, String)*): ManifestAttributes = - { - val converted = for ((name, value) <- attributes) yield (new Attributes.Name(name), value) - new ManifestAttributes(converted: _*) + def ManifestAttributes(attributes: (String, String)*): ManifestAttributes = { + val converted = for ((name, value) <- attributes) yield (new Attributes.Name(name), value) + new ManifestAttributes(converted: _*) + } + // 2010-01-01 + private val default2010Timestamp: Long = 1262304000000L + final case class FixedTimestamp(value: Option[Long]) extends PackageOption + val keepTimestamps: Option[Long] = None + val fixed2010Timestamp: Option[Long] = Some(default2010Timestamp) + def gitCommitDateTimestamp: Option[Long] = + try { + Some( + OffsetDateTime + .parse(Process("git show -s --format=%cI").!!.trim) + .toInstant() + .toEpochMilli() + ) + } catch { + case e: Exception if e.getMessage.startsWith("Nonzero") => + sys.error( + s"git repository was expected for package timestamp; use Package.fixed2010Timestamp or Package.keepTimestamps instead" + ) + } + def setFixedTimestamp(value: Option[Long]): PackageOption = + FixedTimestamp(value) + + /** by default we overwrite all timestamps in JAR to epoch time 2010-01-01 for repeatable build */ + lazy val defaultTimestamp: Option[Long] = + sys.env + .get("SOURCE_DATE_EPOCH") + .map(_.toLong * 1000) + .orElse(Some(default2010Timestamp)) + + def timeFromConfiguration(config: Configuration): Option[Long] = + (config.options.collect { case t: FixedTimestamp => t }).headOption match { + case Some(FixedTimestamp(value)) => value + case _ => defaultTimestamp } def mergeAttributes(a1: Attributes, a2: Attributes) = a1.asScala ++= a2.asScala @@ -41,71 +87,156 @@ object Package { val entryMap = manifest.getEntries.asScala for ((key, value) <- mergeManifest.getEntries.asScala) { entryMap.get(key) match { - case Some(attributes) => mergeAttributes(attributes, value) - case None => entryMap put (key, value) + case Some(attributes) => mergeAttributes(attributes, value); () + case None => entryMap put (key, value); () } } } - final class Configuration(val sources: Seq[(File, String)], val jar: File, val options: Seq[PackageOption]) - def apply(conf: Configuration, cacheFile: File, log: Logger): Unit = { + /** + * The jar package configuration. Contains all relevant information to create a jar file. + * + * @param sources the jar contents + * @param jar the destination jar file + * @param options additional package information, e.g. jar manifest, main class or manifest attributes + */ + final class Configuration( + val sources: Seq[(File, String)], + val jar: File, + val options: Seq[PackageOption] + ) + + /** + * + * @param conf the package configuration that should be build + * @param cacheStoreFactory used for jar caching. We try to avoid rebuilds as much as possible + * @param log feedback for the user + */ + def apply(conf: Configuration, cacheStoreFactory: CacheStoreFactory, log: Logger): Unit = + apply(conf, cacheStoreFactory, log, timeFromConfiguration(conf)) + + /** + * + * @param conf the package configuration that should be build + * @param cacheStoreFactory used for jar caching. We try to avoid rebuilds as much as possible + * @param log feedback for the user + * @param time static timestamp to use for all entries, if any. + */ + def apply( + conf: Configuration, + cacheStoreFactory: CacheStoreFactory, + log: Logger, + time: Option[Long] + ): Unit = { val manifest = new Manifest val main = manifest.getMainAttributes for (option <- conf.options) { option match { - case JarManifest(mergeManifest) => mergeManifests(manifest, mergeManifest) - case MainClass(mainClassName) => main.put(Attributes.Name.MAIN_CLASS, mainClassName) - case ManifestAttributes(attributes @ _*) => main.asScala ++= attributes + case JarManifest(mergeManifest) => mergeManifests(manifest, mergeManifest); () + case MainClass(mainClassName) => main.put(Attributes.Name.MAIN_CLASS, mainClassName); () + case ManifestAttributes(attributes @ _*) => main.asScala ++= attributes; () + case FixedTimestamp(value) => () case _ => log.warn("Ignored unknown package option " + option) } } setVersion(main) - val cachedMakeJar = inputChanged(cacheFile / "inputs") { (inChanged, inputs: Map[File, String] :+: FilesInfo[ModifiedFileInfo] :+: Manifest :+: HNil) => - val sources :+: _ :+: manifest :+: HNil = inputs - outputChanged(cacheFile / "output") { (outChanged, jar: PlainFileInfo) => - if (inChanged || outChanged) - makeJar(sources.toSeq, jar.file, manifest, log) - else - log.debug("Jar uptodate: " + jar.file) - } + type Inputs = Seq[(File, String)] :+: FilesInfo[ModifiedFileInfo] :+: Manifest :+: HNil + val cachedMakeJar = inputChanged(cacheStoreFactory make "inputs") { + (inChanged, inputs: Inputs) => + import exists.format + val sources :+: _ :+: manifest :+: HNil = inputs + outputChanged(cacheStoreFactory make "output") { (outChanged, jar: PlainFileInfo) => + if (inChanged || outChanged) { + makeJar(sources, jar.file, manifest, log, time) + jar.file + () + } else + log.debug("Jar uptodate: " + jar.file) + } } - val map = conf.sources.toMap - val inputs = map :+: lastModified(map.keySet) :+: manifest :+: HNil + val inputFiles = conf.sources.map(_._1).toSet + val inputs = conf.sources.distinct :+: lastModified(inputFiles) :+: manifest :+: HNil cachedMakeJar(inputs)(() => exists(conf.jar)) + () } + + /** + * updates the manifest version is there is none present. + * + * @param main the current jar attributes + */ def setVersion(main: Attributes): Unit = { val version = Attributes.Name.MANIFEST_VERSION - if (main.getValue(version) eq null) + if (main.getValue(version) eq null) { main.put(version, "1.0") + () + } } - def addSpecManifestAttributes(name: String, version: String, orgName: String): PackageOption = - { - import Attributes.Name._ - val attribKeys = Seq(SPECIFICATION_TITLE, SPECIFICATION_VERSION, SPECIFICATION_VENDOR) - val attribVals = Seq(name, version, orgName) - ManifestAttributes(attribKeys zip attribVals: _*) - } - def addImplManifestAttributes(name: String, version: String, homepage: Option[java.net.URL], org: String, orgName: String): PackageOption = - { - import Attributes.Name._ - val attribKeys = Seq(IMPLEMENTATION_TITLE, IMPLEMENTATION_VERSION, IMPLEMENTATION_VENDOR, IMPLEMENTATION_VENDOR_ID) - val attribVals = Seq(name, version, orgName, org) - ManifestAttributes((attribKeys zip attribVals) ++ { homepage map (h => (IMPLEMENTATION_URL, h.toString)) }: _*) - } - def makeJar(sources: Seq[(File, String)], jar: File, manifest: Manifest, log: Logger): Unit = { - log.info("Packaging " + jar.getAbsolutePath + " ...") - IO.delete(jar) + def addSpecManifestAttributes(name: String, version: String, orgName: String): PackageOption = { + import Attributes.Name._ + val attribKeys = Seq(SPECIFICATION_TITLE, SPECIFICATION_VERSION, SPECIFICATION_VENDOR) + val attribVals = Seq(name, version, orgName) + ManifestAttributes(attribKeys zip attribVals: _*) + } + def addImplManifestAttributes( + name: String, + version: String, + homepage: Option[java.net.URL], + org: String, + orgName: String + ): PackageOption = { + import Attributes.Name._ + + // The ones in Attributes.Name are deprecated saying: + // "Extension mechanism will be removed in a future release. Use class path instead." + val IMPLEMENTATION_VENDOR_ID = new Attributes.Name("Implementation-Vendor-Id") + val IMPLEMENTATION_URL = new Attributes.Name("Implementation-URL") + + val attribKeys = Seq( + IMPLEMENTATION_TITLE, + IMPLEMENTATION_VERSION, + IMPLEMENTATION_VENDOR, + IMPLEMENTATION_VENDOR_ID, + ) + val attribVals = Seq(name, version, orgName, org) + ManifestAttributes((attribKeys zip attribVals) ++ { + homepage map (h => (IMPLEMENTATION_URL, h.toString)) + }: _*) + } + + @deprecated("Specify whether to use a static timestamp", "1.4.0") + def makeJar(sources: Seq[(File, String)], jar: File, manifest: Manifest, log: Logger): Unit = + makeJar(sources, jar, manifest, log, None) + + def makeJar( + sources: Seq[(File, String)], + jar: File, + manifest: Manifest, + log: Logger, + time: Option[Long] + ): Unit = { + val path = jar.getAbsolutePath + log.debug("Packaging " + path + " ...") + if (jar.exists) + if (jar.isFile) + IO.delete(jar) + else + sys.error(path + " exists, but is not a regular file") log.debug(sourcesDebugString(sources)) - IO.jar(sources, jar, manifest) - log.info("Done packaging.") + IO.jar(sources, jar, manifest, time) + log.debug("Done packaging.") } def sourcesDebugString(sources: Seq[(File, String)]): String = "Input file mappings:\n\t" + (sources map { case (f, s) => s + "\n\t " + f } mkString ("\n\t")) - implicit def manifestEquiv: Equiv[Manifest] = defaultEquiv - implicit def manifestFormat: Format[Manifest] = streamFormat(_ write _, in => new Manifest(in)) - - implicit def stringMapEquiv: Equiv[Map[File, String]] = defaultEquiv + implicit def manifestFormat: JsonFormat[Manifest] = projectFormat[Manifest, Array[Byte]]( + m => { + val bos = new java.io.ByteArrayOutputStream() + m write bos + bos.toByteArray + }, + bs => new Manifest(new java.io.ByteArrayInputStream(bs)) + ) } diff --git a/main-actions/src/main/scala/sbt/RawCompileLike.scala b/main-actions/src/main/scala/sbt/RawCompileLike.scala index 48f591e72..7ac1a12cc 100644 --- a/main-actions/src/main/scala/sbt/RawCompileLike.scala +++ b/main-actions/src/main/scala/sbt/RawCompileLike.scala @@ -1,65 +1,75 @@ -/* sbt -- Simple Build Tool - * Copyright 2008, 2009, 2010, 2011 Mark Harrah, Indrajit Raychaudhuri +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) */ + package sbt +import scala.annotation.tailrec import java.io.File -import sbt.internal.inc.{ RawCompiler, ScalaInstance } - -import Predef.{ conforms => _, _ } import sbt.io.syntax._ import sbt.io.IO - -import sbinary.DefaultProtocol.FileFormat +import sbt.internal.inc.{ RawCompiler, ScalaInstance } import sbt.internal.util.Types.:+: -import sbt.internal.util.Cache.{ defaultEquiv, hConsCache, hNilCache, IntFormat, seqCache, StringFormat } -import sbt.internal.util.Tracked.{ inputChanged, outputChanged } -import sbt.internal.util.{ FilesInfo, HashFileInfo, HNil, ModifiedFileInfo, PlainFileInfo } -import sbt.internal.util.FilesInfo.{ exists, hash, lastModified } +import sbt.internal.util.HListFormats._ +import sbt.internal.util.HNil +import sbt.internal.util.HListFormats._ +import sbt.util.CacheImplicits._ +import sbt.util.Tracked.inputChanged +import sbt.util.{ CacheStoreFactory, FilesInfo, HashFileInfo, ModifiedFileInfo, PlainFileInfo } +import sbt.util.FileInfo.{ exists, hash, lastModified } +import sbt.internal.util.ManagedLogger import xsbti.compile.ClasspathOptions -import sbt.util.Logger - object RawCompileLike { - type Gen = (Seq[File], Seq[File], File, Seq[String], Int, Logger) => Unit + type Gen = (Seq[File], Seq[File], File, Seq[String], Int, ManagedLogger) => Unit - private def optionFiles(options: Seq[String], fileInputOpts: Seq[String]): List[File] = - { - @annotation.tailrec - def loop(opt: List[String], result: List[File]): List[File] = { - opt.dropWhile(!fileInputOpts.contains(_)) match { - case List(_, fileOpt, tail @ _*) => - { - val file = new File(fileOpt) - if (file.isFile) loop(tail.toList, file :: result) - else loop(tail.toList, result) - } - case Nil | List(_) => result + private def optionFiles(options: Seq[String], fileInputOpts: Seq[String]): List[File] = { + @tailrec + def loop(opt: List[String], result: List[File]): List[File] = { + opt.dropWhile(!fileInputOpts.contains(_)) match { + case List(_, fileOpt, tail @ _*) => { + val file = new File(fileOpt) + if (file.isFile) loop(tail.toList, file :: result) + else loop(tail.toList, result) + } + case Nil | List(_) => result + } + } + loop(options.toList, Nil) + } + + def cached(cacheStoreFactory: CacheStoreFactory, doCompile: Gen): Gen = + cached(cacheStoreFactory, Seq(), doCompile) + + def cached( + cacheStoreFactory: CacheStoreFactory, + fileInputOpts: Seq[String], + doCompile: Gen + ): Gen = + (sources, classpath, outputDirectory, options, maxErrors, log) => { + type Inputs = + FilesInfo[HashFileInfo] :+: FilesInfo[ModifiedFileInfo] :+: Seq[File] :+: File :+: + Seq[String] :+: Int :+: HNil + val inputs: Inputs = hash(sources.toSet ++ optionFiles(options, fileInputOpts)) :+: + FilesInfo(classpath.toSet.map(lastModified.fileOrDirectoryMax)) :+: classpath :+: + outputDirectory :+: options :+: maxErrors :+: HNil + val cachedComp = inputChanged(cacheStoreFactory make "inputs") { (inChanged, in: Inputs) => + inputChanged(cacheStoreFactory make "output") { + (outChanged, outputs: FilesInfo[PlainFileInfo]) => + if (inChanged || outChanged) + doCompile(sources, classpath, outputDirectory, options, maxErrors, log) + else + log.debug("Uptodate: " + outputDirectory.getAbsolutePath) } } - loop(options.toList, Nil) + cachedComp(inputs)(exists(outputDirectory.allPaths.get.toSet)) } - def cached(cache: File, doCompile: Gen): Gen = cached(cache, Seq(), doCompile) - def cached(cache: File, fileInputOpts: Seq[String], doCompile: Gen): Gen = (sources, classpath, outputDirectory, options, maxErrors, log) => - { - type Inputs = FilesInfo[HashFileInfo] :+: FilesInfo[ModifiedFileInfo] :+: Seq[File] :+: File :+: Seq[String] :+: Int :+: HNil - val inputs: Inputs = hash(sources.toSet ++ optionFiles(options, fileInputOpts)) :+: lastModified(classpath.toSet) :+: classpath :+: outputDirectory :+: options :+: maxErrors :+: HNil - implicit val stringEquiv: Equiv[String] = defaultEquiv - implicit val fileEquiv: Equiv[File] = defaultEquiv - implicit val intEquiv: Equiv[Int] = defaultEquiv - val cachedComp = inputChanged(cache / "inputs") { (inChanged, in: Inputs) => - outputChanged(cache / "output") { (outChanged, outputs: FilesInfo[PlainFileInfo]) => - if (inChanged || outChanged) - doCompile(sources, classpath, outputDirectory, options, maxErrors, log) - else - log.debug("Uptodate: " + outputDirectory.getAbsolutePath) - } - } - cachedComp(inputs)(() => exists(outputDirectory.allPaths.get.toSet)) - } - def prepare(description: String, doCompile: Gen): Gen = (sources, classpath, outputDirectory, options, maxErrors, log) => - { + def prepare(description: String, doCompile: Gen): Gen = + (sources, classpath, outputDirectory, options, maxErrors, log) => { if (sources.isEmpty) log.info("No sources available, skipping " + description + "...") else { @@ -70,16 +80,24 @@ object RawCompileLike { log.info(description.capitalize + " successful.") } } - def filterSources(f: File => Boolean, doCompile: Gen): Gen = (sources, classpath, outputDirectory, options, maxErrors, log) => - doCompile(sources filter f, classpath, outputDirectory, options, maxErrors, log) - def rawCompile(instance: ScalaInstance, cpOptions: ClasspathOptions): Gen = (sources, classpath, outputDirectory, options, maxErrors, log) => - { + def filterSources(f: File => Boolean, doCompile: Gen): Gen = + (sources, classpath, outputDirectory, options, maxErrors, log) => + doCompile(sources filter f, classpath, outputDirectory, options, maxErrors, log) + + def rawCompile(instance: ScalaInstance, cpOptions: ClasspathOptions): Gen = + (sources, classpath, outputDirectory, options, _, log) => { val compiler = new RawCompiler(instance, cpOptions, log) - compiler(sources, classpath, outputDirectory, options) + compiler(sources.map(_.toPath), classpath.map(_.toPath), outputDirectory.toPath, options) } - def compile(label: String, cache: File, instance: ScalaInstance, cpOptions: ClasspathOptions): Gen = - cached(cache, prepare(label + " sources", rawCompile(instance, cpOptions))) - val nop: Gen = (sources, classpath, outputDirectory, options, maxErrors, log) => () + def compile( + label: String, + cacheStoreFactory: CacheStoreFactory, + instance: ScalaInstance, + cpOptions: ClasspathOptions + ): Gen = + cached(cacheStoreFactory, prepare(label + " sources", rawCompile(instance, cpOptions))) + + val nop: Gen = (_, _, _, _, _, _) => () } diff --git a/main-actions/src/main/scala/sbt/Sync.scala b/main-actions/src/main/scala/sbt/Sync.scala index df23912ad..b0a3f3b46 100644 --- a/main-actions/src/main/scala/sbt/Sync.scala +++ b/main-actions/src/main/scala/sbt/Sync.scala @@ -1,14 +1,22 @@ -/* sbt -- Simple Build Tool - * Copyright 2010 Mark Harrah +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) */ + package sbt -import java.io.File - -import sbt.internal.util.{ FileInfo, Relation } - +import java.io.{ File, IOException } +import java.util.zip.ZipException +import sbt.internal.util.Relation +import sbt.internal.io.TranslatedException +import sbt.util.CacheImplicits._ +import sbt.util.{ FileInfo, CacheStore } import sbt.io.IO +import sjsonnew.{ Builder, JsonFormat, Unbuilder, deserializationError } + /** * Maintains a set of mappings so that they are uptodate. * Specifically, 'apply' applies the mappings by creating target directories and copying source files to their destination. @@ -22,77 +30,113 @@ import sbt.io.IO * It is safe to use for its intended purpose: copying resources to a class output directory. */ object Sync { - def apply(cacheFile: File, inStyle: FileInfo.Style = FileInfo.lastModified, outStyle: FileInfo.Style = FileInfo.exists): Traversable[(File, File)] => Relation[File, File] = - mappings => - { - val relation = Relation.empty ++ mappings - noDuplicateTargets(relation) - val currentInfo = relation._1s.map(s => (s, inStyle(s))).toMap + @deprecated("Use sync, which doesn't take the unused outStyle param", "1.1.1") + def apply( + store: CacheStore, + inStyle: FileInfo.Style = FileInfo.lastModified, + outStyle: FileInfo.Style = FileInfo.exists, + ): Traversable[(File, File)] => Relation[File, File] = + sync(store, inStyle) - val (previousRelation, previousInfo) = readInfo(cacheFile)(inStyle.format) - val removeTargets = previousRelation._2s -- relation._2s + def sync( + store: CacheStore, + inStyle: FileInfo.Style = FileInfo.lastModified, + ): Traversable[(File, File)] => Relation[File, File] = + mappings => { + val relation = Relation.empty ++ mappings + noDuplicateTargets(relation) + val currentInfo = relation._1s.map(s => (s, inStyle(s))).toMap - def outofdate(source: File, target: File): Boolean = - !previousRelation.contains(source, target) || - (previousInfo get source) != (currentInfo get source) || - !target.exists || - target.isDirectory != source.isDirectory + val (previousRelation, previousInfo) = readInfo(store)(inStyle.format) + val removeTargets = previousRelation._2s -- relation._2s - val updates = relation filter outofdate + def outofdate(source: File, target: File): Boolean = + !previousRelation.contains(source, target) || + (previousInfo get source) != (currentInfo get source) || + !target.exists || + target.isDirectory != source.isDirectory - val (cleanDirs, cleanFiles) = (updates._2s ++ removeTargets).partition(_.isDirectory) + val updates = relation filter outofdate - IO.delete(cleanFiles) - IO.deleteIfEmpty(cleanDirs) - updates.all.foreach((copy _).tupled) + val (cleanDirs, cleanFiles) = (updates._2s ++ removeTargets).partition(_.isDirectory) - writeInfo(cacheFile, relation, currentInfo)(inStyle.format) - relation - } + IO.delete(cleanFiles) + IO.deleteIfEmpty(cleanDirs) + updates.all.foreach((copy _).tupled) + + writeInfo(store, relation, currentInfo)(inStyle.format) + relation + } def copy(source: File, target: File): Unit = if (source.isFile) IO.copyFile(source, target, true) - else if (!target.exists) // we don't want to update the last modified time of an existing directory - { + else if (!target.exists) { // we don't want to update the last modified time of an existing directory IO.createDirectory(target) IO.copyLastModified(source, target) + () } def noDuplicateTargets(relation: Relation[File, File]): Unit = { - val dups = relation.reverseMap.filter { - case (target, srcs) => - srcs.size >= 2 && srcs.exists(!_.isDirectory) - } map { - case (target, srcs) => - "\n\t" + target + "\nfrom\n\t" + srcs.mkString("\n\t\t") - } + val dups = relation.reverseMap + .filter { case (_, srcs) => srcs.size >= 2 && srcs.exists(!_.isDirectory) } + .map { case (target, srcs) => "\n\t" + target + "\nfrom\n\t" + srcs.mkString("\n\t\t") } if (dups.nonEmpty) sys.error("Duplicate mappings:" + dups.mkString) } - import java.io.{ File, IOException } - import sbinary._ - import Operations.{ read, write } - import DefaultProtocol.{ FileFormat => _, _ } - //import sbt.inc.AnalysisFormats.{ fileFormat, relationFormat } - implicit def fileFormat: Format[File] = wrap[File, String](_.getAbsolutePath, s => new File(s)) - implicit def relationFormat[A, B](implicit af: Format[Map[A, Set[B]]], bf: Format[Map[B, Set[A]]]): Format[Relation[A, B]] = - asProduct2[Relation[A, B], Map[A, Set[B]], Map[B, Set[A]]](Relation.make _)(r => (r.forwardMap, r.reverseMap))(af, bf) + implicit def relationFormat[A, B]( + implicit af: JsonFormat[Map[A, Set[B]]], + bf: JsonFormat[Map[B, Set[A]]] + ): JsonFormat[Relation[A, B]] = + new JsonFormat[Relation[A, B]] { + def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): Relation[A, B] = + jsOpt match { + case Some(js) => + unbuilder.beginArray(js) + val jForward = unbuilder.nextElement + val jReverse = unbuilder.nextElement + unbuilder.endArray() + Relation.make(af.read(Some(jForward), unbuilder), bf.read(Some(jReverse), unbuilder)) + case None => + deserializationError("Expected JsArray but found None") + } + + def write[J](obj: Relation[A, B], builder: Builder[J]): Unit = { + builder.beginArray() + af.write(obj.forwardMap, builder) + bf.write(obj.reverseMap, builder) + builder.endArray() + } - def writeInfo[F <: FileInfo](file: File, relation: Relation[File, File], info: Map[File, F])(implicit infoFormat: Format[F]): Unit = - IO.gzipFileOut(file) { out => - write(out, (relation, info)) } + def writeInfo[F <: FileInfo]( + store: CacheStore, + relation: Relation[File, File], + info: Map[File, F] + )(implicit infoFormat: JsonFormat[F]): Unit = + store.write((relation, info)) + type RelationInfo[F] = (Relation[File, File], Map[File, F]) - def readInfo[F <: FileInfo](file: File)(implicit infoFormat: Format[F]): RelationInfo[F] = - try { readUncaught(file)(infoFormat) } - catch { case e: IOException => (Relation.empty, Map.empty) } - - def readUncaught[F <: FileInfo](file: File)(implicit infoFormat: Format[F]): RelationInfo[F] = - IO.gzipFileIn(file) { in => - read[RelationInfo[F]](in) + def readInfo[F <: FileInfo]( + store: CacheStore + )(implicit infoFormat: JsonFormat[F]): RelationInfo[F] = + try { + readUncaught[F](store)(infoFormat) + } catch { + case _: IOException => (Relation.empty[File, File], Map.empty[File, F]) + case _: ZipException => (Relation.empty[File, File], Map.empty[File, F]) + case e: TranslatedException => + e.getCause match { + case _: ZipException => (Relation.empty[File, File], Map.empty[File, F]) + case _ => throw e + } } + + private def readUncaught[F <: FileInfo]( + store: CacheStore + )(implicit infoFormat: JsonFormat[F]): RelationInfo[F] = + store.read(default = (Relation.empty[File, File], Map.empty[File, F])) } diff --git a/main-actions/src/main/scala/sbt/TestResultLogger.scala b/main-actions/src/main/scala/sbt/TestResultLogger.scala index 708e9c781..d7675fb34 100644 --- a/main-actions/src/main/scala/sbt/TestResultLogger.scala +++ b/main-actions/src/main/scala/sbt/TestResultLogger.scala @@ -1,6 +1,14 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + package sbt import sbt.Tests.{ Output, Summary } +import sbt.protocol.testing.TestResult import sbt.util.{ Level, Logger } /** @@ -23,11 +31,17 @@ trait TestResultLogger { def run(log: Logger, results: Output, taskName: String): Unit /** Only allow invocation if certain criteria is met, else use another `TestResultLogger` (defaulting to nothing) . */ - final def onlyIf(f: (Output, String) => Boolean, otherwise: TestResultLogger = TestResultLogger.Null) = + final def onlyIf( + f: (Output, String) => Boolean, + otherwise: TestResultLogger = TestResultLogger.Null + ) = TestResultLogger.choose(f, this, otherwise) /** Allow invocation unless a certain predicate passes, in which case use another `TestResultLogger` (defaulting to nothing) . */ - final def unless(f: (Output, String) => Boolean, otherwise: TestResultLogger = TestResultLogger.Null) = + final def unless( + f: (Output, String) => Boolean, + otherwise: TestResultLogger = TestResultLogger.Null + ) = TestResultLogger.choose(f, otherwise, this) } @@ -36,7 +50,7 @@ object TestResultLogger { /** A `TestResultLogger` that does nothing. */ val Null = const(_ => ()) - /** SBT's default `TestResultLogger`. Use `copy()` to change selective portions. */ + /** sbt's default `TestResultLogger`. Use `copy()` to change selective portions. */ val Default = Defaults.Main() /** Twist on the default which is completely silent when the subject module doesn't contain any tests. */ @@ -44,10 +58,7 @@ object TestResultLogger { /** Creates a `TestResultLogger` using a given function. */ def apply(f: (Logger, Output, String) => Unit): TestResultLogger = - new TestResultLogger { - override def run(log: Logger, results: Output, taskName: String) = - f(log, results, taskName) - } + (log, results, taskName) => f(log, results, taskName) /** Creates a `TestResultLogger` that ignores its input and always performs the same logging. */ def const(f: Logger => Unit) = apply((l, _, _) => f(l)) @@ -59,8 +70,10 @@ object TestResultLogger { * @param f The `TestResultLogger` to choose if the predicate fails. */ def choose(cond: (Output, String) => Boolean, t: TestResultLogger, f: TestResultLogger) = - TestResultLogger((log, results, taskName) => - (if (cond(results, taskName)) t else f).run(log, results, taskName)) + TestResultLogger( + (log, results, taskName) => + (if (cond(results, taskName)) t else f).run(log, results, taskName) + ) /** Transforms the input to be completely silent when the subject module doesn't contain any tests. */ def silenceWhenNoTests(d: Defaults.Main) = @@ -71,13 +84,14 @@ object TestResultLogger { object Defaults { - /** SBT's default `TestResultLogger`. Use `copy()` to change selective portions. */ + /** sbt's default `TestResultLogger`. Use `copy()` to change selective portions. */ case class Main( printStandard_? : Output => Boolean = Defaults.printStandard_?, printSummary: TestResultLogger = Defaults.printSummary, printStandard: TestResultLogger = Defaults.printStandard, printFailures: TestResultLogger = Defaults.printFailures, - printNoTests: TestResultLogger = Defaults.printNoTests) extends TestResultLogger { + printNoTests: TestResultLogger = Defaults.printNoTests + ) extends TestResultLogger { override def run(log: Logger, results: Output, taskName: String): Unit = { def run(r: TestResultLogger): Unit = r.run(log, results, taskName) @@ -116,16 +130,39 @@ object TestResultLogger { results.summaries.size > 1 || results.summaries.headOption.forall(_.summaryText.isEmpty) val printStandard = TestResultLogger((log, results, _) => { - val (skippedCount, errorsCount, passedCount, failuresCount, ignoredCount, canceledCount, pendingCount) = + val ( + skippedCount, + errorsCount, + passedCount, + failuresCount, + ignoredCount, + canceledCount, + pendingCount, + ) = results.events.foldLeft((0, 0, 0, 0, 0, 0, 0)) { - case ((skippedAcc, errorAcc, passedAcc, failureAcc, ignoredAcc, canceledAcc, pendingAcc), (name, testEvent)) => - (skippedAcc + testEvent.skippedCount, errorAcc + testEvent.errorCount, passedAcc + testEvent.passedCount, failureAcc + testEvent.failureCount, - ignoredAcc + testEvent.ignoredCount, canceledAcc + testEvent.canceledCount, pendingAcc + testEvent.pendingCount) + case (acc, (_, testEvent)) => + val (skippedAcc, errorAcc, passedAcc, failureAcc, ignoredAcc, canceledAcc, pendingAcc) = + acc + ( + skippedAcc + testEvent.skippedCount, + errorAcc + testEvent.errorCount, + passedAcc + testEvent.passedCount, + failureAcc + testEvent.failureCount, + ignoredAcc + testEvent.ignoredCount, + canceledAcc + testEvent.canceledCount, + pendingAcc + testEvent.pendingCount, + ) } val totalCount = failuresCount + errorsCount + skippedCount + passedCount - val base = s"Total $totalCount, Failed $failuresCount, Errors $errorsCount, Passed $passedCount" + val base = + s"Total $totalCount, Failed $failuresCount, Errors $errorsCount, Passed $passedCount" - val otherCounts = Seq("Skipped" -> skippedCount, "Ignored" -> ignoredCount, "Canceled" -> canceledCount, "Pending" -> pendingCount) + val otherCounts = Seq( + "Skipped" -> skippedCount, + "Ignored" -> ignoredCount, + "Canceled" -> canceledCount, + "Pending" -> pendingCount + ) val extra = otherCounts.filter(_._2 > 0).map { case (label, count) => s", $label $count" } val postfix = base + extra.mkString @@ -137,7 +174,7 @@ object TestResultLogger { }) val printFailures = TestResultLogger((log, results, _) => { - def select(resultTpe: TestResult.Value) = results.events collect { + def select(resultTpe: TestResult) = results.events collect { case (name, tpe) if tpe.result == resultTpe => scala.reflect.NameTransformer.decode(name) } @@ -153,8 +190,8 @@ object TestResultLogger { show("Error during tests:", Level.Error, select(TestResult.Error)) }) - val printNoTests = TestResultLogger((log, results, taskName) => - log.info("No tests to run for " + taskName) + val printNoTests = TestResultLogger( + (log, results, taskName) => log.info("No tests to run for " + taskName) ) } } diff --git a/main-actions/src/main/scala/sbt/Tests.scala b/main-actions/src/main/scala/sbt/Tests.scala index 5148a3db4..90f64b29e 100644 --- a/main-actions/src/main/scala/sbt/Tests.scala +++ b/main-actions/src/main/scala/sbt/Tests.scala @@ -1,6 +1,10 @@ -/* sbt -- Simple Build Tool - * Copyright 2010 Mark Harrah +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) */ + package sbt import std._ @@ -9,16 +13,32 @@ import sbt.internal.inc.Analysis import TaskExtra._ import sbt.internal.util.FeedbackProvidedException import xsbti.api.Definition +import xsbti.api.ClassLike import xsbti.compile.CompileAnalysis import ConcurrentRestrictions.Tag +import testing.{ + AnnotatedFingerprint, + Fingerprint, + Framework, + Runner, + Selector, + SubclassFingerprint, + SuiteSelector, + TaskDef, + Task => TestTask +} -import testing.{ AnnotatedFingerprint, Fingerprint, Framework, SubclassFingerprint, Runner, TaskDef, SuiteSelector, Task => TestTask } import scala.annotation.tailrec - +import sbt.internal.util.ManagedLogger import sbt.util.Logger +import sbt.protocol.testing.TestResult + +import scala.runtime.AbstractFunction3 sealed trait TestOption + object Tests { + /** * The result of a test run. * @@ -26,7 +46,11 @@ object Tests { * @param events The result of each test group (suite) executed during this test run. * @param summaries Explicit summaries directly provided by test frameworks. This may be empty, in which case a default summary will be generated. */ - final case class Output(overall: TestResult.Value, events: Map[String, SuiteResult], summaries: Iterable[Summary]) + final case class Output( + overall: TestResult, + events: Map[String, SuiteResult], + summaries: Iterable[Summary] + ) /** * Summarizes a test run. @@ -108,113 +132,308 @@ object Tests { /** Configures a group of tests to be forked in a new JVM with forking options specified by `config`. */ final case class SubProcess(config: ForkOptions) extends TestRunPolicy - object SubProcess { - @deprecated("Construct SubProcess with a ForkOptions argument.", "0.13.0") - def apply(javaOptions: Seq[String]): SubProcess = SubProcess(ForkOptions(runJVMOptions = javaOptions)) - } /** A named group of tests configured to run in the same JVM or be forked. */ - final case class Group(name: String, tests: Seq[TestDefinition], runPolicy: TestRunPolicy) + final class Group( + val name: String, + val tests: Seq[TestDefinition], + val runPolicy: TestRunPolicy, + val tags: Seq[(Tag, Int)] + ) extends Product + with Serializable { + + def this(name: String, tests: Seq[TestDefinition], runPolicy: TestRunPolicy) = { + this(name, tests, runPolicy, Seq.empty) + } + + def withName(name: String): Group = { + new Group(name, tests, runPolicy, tags) + } + + def withTests(tests: Seq[TestDefinition]): Group = { + new Group(name, tests, runPolicy, tags) + } + + def withRunPolicy(runPolicy: TestRunPolicy): Group = { + new Group(name, tests, runPolicy, tags) + } + + def withTags(tags: Seq[(Tag, Int)]): Group = { + new Group(name, tests, runPolicy, tags) + } + + //- EXPANDED CASE CLASS METHOD BEGIN -// + @deprecated("Methods generated for case class will be removed in the future.", "1.4.0") + def copy( + name: String = this.name, + tests: Seq[TestDefinition] = this.tests, + runPolicy: TestRunPolicy = this.runPolicy + ): Group = { + new Group(name, tests, runPolicy, this.tags) + } + + @deprecated("Methods generated for case class will be removed in the future.", "1.4.0") + override def productElement(x$1: Int): Any = x$1 match { + case 0 => Group.this.name + case 1 => Group.this.tests + case 2 => Group.this.runPolicy + case 3 => Group.this.tags + } + + @deprecated("Methods generated for case class will be removed in the future.", "1.4.0") + override def productArity: Int = 4 + + @deprecated("Methods generated for case class will be removed in the future.", "1.4.0") + def canEqual(x$1: Any): Boolean = x$1.isInstanceOf[Group] + + override def hashCode(): Int = { + scala.runtime.ScalaRunTime._hashCode(Group.this) + } + + override def toString(): String = scala.runtime.ScalaRunTime._toString(Group.this) + + override def equals(x$1: Any): Boolean = { + this.eq(x$1.asInstanceOf[Object]) || (x$1.isInstanceOf[Group] && ({ + val Group$1: Group = x$1.asInstanceOf[Group] + name == Group$1.name && tests == Group$1.tests && + runPolicy == Group$1.runPolicy && tags == Group$1.tags + })) + } + //- EXPANDED CASE CLASS METHOD END -// + } + + object Group + extends AbstractFunction3[String, Seq[TestDefinition], TestRunPolicy, Group] + with Serializable { + //- EXPANDED CASE CLASS METHOD BEGIN -// + final override def toString(): String = "Group" + def apply( + name: String, + tests: Seq[TestDefinition], + runPolicy: TestRunPolicy + ): Group = { + new Group(name, tests, runPolicy, Seq.empty) + } + + def apply( + name: String, + tests: Seq[TestDefinition], + runPolicy: TestRunPolicy, + tags: Seq[(Tag, Int)] + ): Group = { + new Group(name, tests, runPolicy, tags) + } + + @deprecated("Methods generated for case class will be removed in the future.", "1.4.0") + def unapply( + x$0: Group + ): Option[(String, Seq[TestDefinition], TestRunPolicy)] = { + if (x$0 == null) None + else + Some.apply[(String, Seq[TestDefinition], TestRunPolicy)]( + Tuple3.apply[String, Seq[TestDefinition], TestRunPolicy]( + x$0.name, + x$0.tests, + x$0.runPolicy + ) + ) + } + private def readResolve(): Object = Group + //- EXPANDED CASE CLASS METHOD END -// + } private[sbt] final class ProcessedOptions( - val tests: Seq[TestDefinition], - val setup: Seq[ClassLoader => Unit], - val cleanup: Seq[ClassLoader => Unit], - val testListeners: Seq[TestReportListener]) - private[sbt] def processOptions(config: Execution, discovered: Seq[TestDefinition], log: Logger): ProcessedOptions = - { - import collection.mutable.{ HashSet, ListBuffer } - val testFilters = new ListBuffer[String => Boolean] - var orderedFilters = Seq[String => Boolean]() - val excludeTestsSet = new HashSet[String] - val setup, cleanup = new ListBuffer[ClassLoader => Unit] - val testListeners = new ListBuffer[TestReportListener] - val undefinedFrameworks = new ListBuffer[String] + val tests: Vector[TestDefinition], + val setup: Vector[ClassLoader => Unit], + val cleanup: Vector[ClassLoader => Unit], + val testListeners: Vector[TestReportListener] + ) + private[sbt] def processOptions( + config: Execution, + discovered: Vector[TestDefinition], + log: Logger + ): ProcessedOptions = { + import collection.mutable.{ HashSet, ListBuffer } + val testFilters = new ListBuffer[String => Boolean] + var orderedFilters = Seq[String => Boolean]() + val excludeTestsSet = new HashSet[String] + val setup, cleanup = new ListBuffer[ClassLoader => Unit] + val testListeners = new ListBuffer[TestReportListener] + val undefinedFrameworks = new ListBuffer[String] - for (option <- config.options) { - option match { - case Filter(include) => testFilters += include - case Filters(includes) => if (orderedFilters.nonEmpty) sys.error("Cannot define multiple ordered test filters.") else orderedFilters = includes - case Exclude(exclude) => excludeTestsSet ++= exclude - case Listeners(listeners) => testListeners ++= listeners - case Setup(setupFunction) => setup += setupFunction - case Cleanup(cleanupFunction) => cleanup += cleanupFunction - case a: Argument => // now handled by whatever constructs `runners` + for (option <- config.options) { + option match { + case Filter(include) => testFilters += include; () + case Filters(includes) => + if (orderedFilters.nonEmpty) sys.error("Cannot define multiple ordered test filters.") + else orderedFilters = includes + () + case Exclude(exclude) => excludeTestsSet ++= exclude; () + case Listeners(listeners) => testListeners ++= listeners; () + case Setup(setupFunction) => setup += setupFunction; () + case Cleanup(cleanupFunction) => cleanup += cleanupFunction; () + case _: Argument => // now handled by whatever constructs `runners` + } + } + + if (excludeTestsSet.nonEmpty) + log.debug(excludeTestsSet.mkString("Excluding tests: \n\t", "\n\t", "")) + if (undefinedFrameworks.nonEmpty) + log.warn( + "Arguments defined for test frameworks that are not present:\n\t" + undefinedFrameworks + .mkString("\n\t") + ) + + def includeTest(test: TestDefinition) = + !excludeTestsSet.contains(test.name) && testFilters.forall(filter => filter(test.name)) + val filtered0 = discovered.filter(includeTest).toList.distinct + val tests = + if (orderedFilters.isEmpty) filtered0 + else orderedFilters.flatMap(f => filtered0.filter(d => f(d.name))).toList.distinct + val uniqueTests = distinctBy(tests)(_.name) + new ProcessedOptions( + uniqueTests.toVector, + setup.toVector, + cleanup.toVector, + testListeners.toVector + ) + } + + private[this] def distinctBy[T, K](in: Seq[T])(f: T => K): Seq[T] = { + val seen = new collection.mutable.HashSet[K] + in.filter(t => seen.add(f(t))) + } + + def apply( + frameworks: Map[TestFramework, Framework], + testLoader: ClassLoader, + runners: Map[TestFramework, Runner], + o: ProcessedOptions, + config: Execution, + log: ManagedLogger + ): Task[Output] = { + testTask( + testLoader, + frameworks, + runners, + o.tests, + o.setup, + o.cleanup, + log, + o.testListeners, + config + ) + } + + def apply( + frameworks: Map[TestFramework, Framework], + testLoader: ClassLoader, + runners: Map[TestFramework, Runner], + discovered: Vector[TestDefinition], + config: Execution, + log: ManagedLogger + ): Task[Output] = { + val o = processOptions(config, discovered, log) + apply(frameworks, testLoader, runners, o, config, log) + } + + def testTask( + loader: ClassLoader, + frameworks: Map[TestFramework, Framework], + runners: Map[TestFramework, Runner], + tests: Vector[TestDefinition], + userSetup: Iterable[ClassLoader => Unit], + userCleanup: Iterable[ClassLoader => Unit], + log: ManagedLogger, + testListeners: Vector[TestReportListener], + config: Execution + ): Task[Output] = { + def fj(actions: Iterable[() => Unit]): Task[Unit] = nop.dependsOn(actions.toSeq.fork(_()): _*) + def partApp(actions: Iterable[ClassLoader => Unit]) = actions.toSeq map { a => () => + a(loader) + } + + val (frameworkSetup, runnables, frameworkCleanup) = + TestFramework.testTasks(frameworks, runners, loader, tests, log, testListeners) + + val setupTasks = fj(partApp(userSetup) :+ frameworkSetup) + val mainTasks = + if (config.parallel) + makeParallel(loader, runnables, setupTasks, config.tags).map(_.toList) + else + makeSerial(loader, runnables, setupTasks) + val taggedMainTasks = mainTasks.tagw(config.tags: _*) + taggedMainTasks + .map(processResults) + .flatMap { results => + val cleanupTasks = fj(partApp(userCleanup) :+ frameworkCleanup(results.overall)) + cleanupTasks map { _ => + results } } - - if (excludeTestsSet.nonEmpty) - log.debug(excludeTestsSet.mkString("Excluding tests: \n\t", "\n\t", "")) - if (undefinedFrameworks.nonEmpty) - log.warn("Arguments defined for test frameworks that are not present:\n\t" + undefinedFrameworks.mkString("\n\t")) - - def includeTest(test: TestDefinition) = !excludeTestsSet.contains(test.name) && testFilters.forall(filter => filter(test.name)) - val filtered0 = discovered.filter(includeTest).toList.distinct - val tests = if (orderedFilters.isEmpty) filtered0 else orderedFilters.flatMap(f => filtered0.filter(d => f(d.name))).toList.distinct - val uniqueTests = distinctBy(tests)(_.name) - new ProcessedOptions(uniqueTests, setup.toList, cleanup.toList, testListeners.toList) - } - - private[this] def distinctBy[T, K](in: Seq[T])(f: T => K): Seq[T] = - { - val seen = new collection.mutable.HashSet[K] - in.filter(t => seen.add(f(t))) - } - - def apply(frameworks: Map[TestFramework, Framework], testLoader: ClassLoader, runners: Map[TestFramework, Runner], discovered: Seq[TestDefinition], config: Execution, log: Logger): Task[Output] = - { - val o = processOptions(config, discovered, log) - testTask(testLoader, frameworks, runners, o.tests, o.setup, o.cleanup, log, o.testListeners, config) - } - - def testTask(loader: ClassLoader, frameworks: Map[TestFramework, Framework], runners: Map[TestFramework, Runner], tests: Seq[TestDefinition], - userSetup: Iterable[ClassLoader => Unit], userCleanup: Iterable[ClassLoader => Unit], - log: Logger, testListeners: Seq[TestReportListener], config: Execution): Task[Output] = - { - def fj(actions: Iterable[() => Unit]): Task[Unit] = nop.dependsOn(actions.toSeq.fork(_()): _*) - def partApp(actions: Iterable[ClassLoader => Unit]) = actions.toSeq map { a => () => a(loader) } - - val (frameworkSetup, runnables, frameworkCleanup) = - TestFramework.testTasks(frameworks, runners, loader, tests, log, testListeners) - - val setupTasks = fj(partApp(userSetup) :+ frameworkSetup) - val mainTasks = - if (config.parallel) - makeParallel(loader, runnables, setupTasks, config.tags) //.toSeq.join - else - makeSerial(loader, runnables, setupTasks, config.tags) - val taggedMainTasks = mainTasks.tagw(config.tags: _*) - taggedMainTasks map processResults flatMap { results => - val cleanupTasks = fj(partApp(userCleanup) :+ frameworkCleanup(results.overall)) - cleanupTasks map { _ => results } - } - } + } type TestRunnable = (String, TestFunction) - private def createNestedRunnables(loader: ClassLoader, testFun: TestFunction, nestedTasks: Seq[TestTask]): Seq[(String, TestFunction)] = - nestedTasks.view.zipWithIndex map { + private def createNestedRunnables( + loader: ClassLoader, + testFun: TestFunction, + nestedTasks: Seq[TestTask] + ): Seq[(String, TestFunction)] = + (nestedTasks.view.zipWithIndex map { case (nt, idx) => val testFunDef = testFun.taskDef - (testFunDef.fullyQualifiedName, TestFramework.createTestFunction(loader, new TaskDef(testFunDef.fullyQualifiedName + "-" + idx, testFunDef.fingerprint, testFunDef.explicitlySpecified, testFunDef.selectors), testFun.runner, nt)) - } + ( + testFunDef.fullyQualifiedName, + TestFramework.createTestFunction( + loader, + new TaskDef( + testFunDef.fullyQualifiedName + "-" + idx, + testFunDef.fingerprint, + testFunDef.explicitlySpecified, + testFunDef.selectors + ), + testFun.runner, + nt + ) + ) + }).toSeq - def makeParallel(loader: ClassLoader, runnables: Iterable[TestRunnable], setupTasks: Task[Unit], tags: Seq[(Tag, Int)]): Task[Map[String, SuiteResult]] = + def makeParallel( + loader: ClassLoader, + runnables: Iterable[TestRunnable], + setupTasks: Task[Unit], + tags: Seq[(Tag, Int)] + ): Task[Map[String, SuiteResult]] = toTasks(loader, runnables.toSeq, tags).dependsOn(setupTasks) - def toTasks(loader: ClassLoader, runnables: Seq[TestRunnable], tags: Seq[(Tag, Int)]): Task[Map[String, SuiteResult]] = { + def toTasks( + loader: ClassLoader, + runnables: Seq[TestRunnable], + tags: Seq[(Tag, Int)] + ): Task[Map[String, SuiteResult]] = { val tasks = runnables.map { case (name, test) => toTask(loader, name, test, tags) } tasks.join.map(_.foldLeft(Map.empty[String, SuiteResult]) { case (sum, e) => val merged = sum.toSeq ++ e.toSeq val grouped = merged.groupBy(_._1) - grouped.mapValues(_.map(_._2).foldLeft(SuiteResult.Empty) { - case (resultSum, result) => resultSum + result - }) + grouped + .mapValues(_.map(_._2).foldLeft(SuiteResult.Empty) { + case (resultSum, result) => resultSum + result + }) + .toMap }) } - def toTask(loader: ClassLoader, name: String, fun: TestFunction, tags: Seq[(Tag, Int)]): Task[Map[String, SuiteResult]] = { - val base = task { (name, fun.apply()) } + def toTask( + loader: ClassLoader, + name: String, + fun: TestFunction, + tags: Seq[(Tag, Int)] + ): Task[Map[String, SuiteResult]] = { + val base = Task[(String, (SuiteResult, Seq[TestTask]))]( + Info[(String, (SuiteResult, Seq[TestTask]))]().setName(name), + Pure(() => (name, fun.apply()), `inline` = false) + ) val taggedBase = base.tagw(tags: _*).tag(fun.tags.map(ConcurrentRestrictions.Tag(_)): _*) taggedBase flatMap { case (name, (result, nested)) => @@ -230,44 +449,89 @@ object Tests { } } - def makeSerial(loader: ClassLoader, runnables: Seq[TestRunnable], setupTasks: Task[Unit], tags: Seq[(Tag, Int)]): Task[List[(String, SuiteResult)]] = - { - @tailrec - def processRunnable(runnableList: List[TestRunnable], acc: List[(String, SuiteResult)]): List[(String, SuiteResult)] = - runnableList match { - case hd :: rst => - val testFun = hd._2 - val (result, nestedTasks) = testFun.apply() - val nestedRunnables = createNestedRunnables(loader, testFun, nestedTasks) - processRunnable(nestedRunnables.toList ::: rst, (hd._1, result) :: acc) - case Nil => acc - } + @deprecated("Use the variant without tags", "1.1.1") + def makeSerial( + loader: ClassLoader, + runnables: Seq[TestRunnable], + setupTasks: Task[Unit], + tags: Seq[(Tag, Int)], + ): Task[List[(String, SuiteResult)]] = + makeSerial(loader, runnables, setupTasks) - task { processRunnable(runnables.toList, List.empty) } dependsOn (setupTasks) - } + def makeSerial( + loader: ClassLoader, + runnables: Seq[TestRunnable], + setupTasks: Task[Unit], + ): Task[List[(String, SuiteResult)]] = { + @tailrec + def processRunnable( + runnableList: List[TestRunnable], + acc: List[(String, SuiteResult)] + ): List[(String, SuiteResult)] = + runnableList match { + case hd :: rst => + val testFun = hd._2 + val (result, nestedTasks) = testFun.apply() + val nestedRunnables = createNestedRunnables(loader, testFun, nestedTasks) + processRunnable(nestedRunnables.toList ::: rst, (hd._1, result) :: acc) + case Nil => acc + } + + task { processRunnable(runnables.toList, List.empty) } dependsOn (setupTasks) + } def processResults(results: Iterable[(String, SuiteResult)]): Output = Output(overall(results.map(_._2.result)), results.toMap, Iterable.empty) + + private def severity(r: TestResult): Int = + r match { + case TestResult.Passed => 0 + case TestResult.Failed => 1 + case TestResult.Error => 2 + } + def foldTasks(results: Seq[Task[Output]], parallel: Boolean): Task[Output] = - if (results.isEmpty) + if (results.isEmpty) { task { Output(TestResult.Passed, Map.empty, Nil) } - else if (parallel) - reduced(results.toIndexedSeq, { - case (Output(v1, m1, _), Output(v2, m2, _)) => Output(if (v1.id < v2.id) v2 else v1, m1 ++ m2, Iterable.empty) - }) - else { - def sequence(tasks: List[Task[Output]], acc: List[Output]): Task[List[Output]] = tasks match { - case Nil => task(acc.reverse) - case hd :: tl => hd flatMap { out => sequence(tl, out :: acc) } - } + } else if (parallel) { + reduced[Output]( + results.toIndexedSeq, { + case (Output(v1, m1, _), Output(v2, m2, _)) => + Output( + (if (severity(v1) < severity(v2)) v2 else v1): TestResult, + Map((m1.toSeq ++ m2.toSeq): _*), + Iterable.empty[Summary] + ) + } + ) + } else { + def sequence(tasks: List[Task[Output]], acc: List[Output]): Task[List[Output]] = + tasks match { + case Nil => task(acc.reverse) + case hd :: tl => + hd flatMap { out => + sequence(tl, out :: acc) + } + } sequence(results.toList, List()) map { ress => - val (rs, ms) = ress.unzip { e => (e.overall, e.events) } - Output(overall(rs), ms reduce (_ ++ _), Iterable.empty) + val (rs, ms) = ress.unzip { e => + (e.overall, e.events) + } + val m = ms reduce { (m1: Map[String, SuiteResult], m2: Map[String, SuiteResult]) => + Map((m1.toSeq ++ m2.toSeq): _*) + } + Output(overall(rs), m, Iterable.empty) } } - def overall(results: Iterable[TestResult.Value]): TestResult.Value = - (TestResult.Passed /: results) { (acc, result) => if (acc.id < result.id) result else acc } - def discover(frameworks: Seq[Framework], analysis: CompileAnalysis, log: Logger): (Seq[TestDefinition], Set[String]) = + def overall(results: Iterable[TestResult]): TestResult = + results.foldLeft(TestResult.Passed: TestResult) { (acc, result) => + if (severity(acc) < severity(result)) result else acc + } + def discover( + frameworks: Seq[Framework], + analysis: CompileAnalysis, + log: Logger + ): (Seq[TestDefinition], Set[String]) = discover(frameworks flatMap TestFramework.getFingerprints, allDefs(analysis), log) def allDefs(analysis: CompileAnalysis) = analysis match { @@ -276,39 +540,57 @@ object Tests { acs.flatMap { ac => val companions = ac.api val all = - Seq(companions.classApi, companions.objectApi) ++ - companions.classApi.structure.declared ++ companions.classApi.structure.inherited ++ - companions.objectApi.structure.declared ++ companions.objectApi.structure.inherited + Seq(companions.classApi: Definition, companions.objectApi: Definition) ++ + (companions.classApi.structure.declared.toSeq: Seq[Definition]) ++ + (companions.classApi.structure.inherited.toSeq: Seq[Definition]) ++ + (companions.objectApi.structure.declared.toSeq: Seq[Definition]) ++ + (companions.objectApi.structure.inherited.toSeq: Seq[Definition]) all }.toSeq } - def discover(fingerprints: Seq[Fingerprint], definitions: Seq[Definition], log: Logger): (Seq[TestDefinition], Set[String]) = - { - val subclasses = fingerprints collect { case sub: SubclassFingerprint => (sub.superclassName, sub.isModule, sub) }; - val annotations = fingerprints collect { case ann: AnnotatedFingerprint => (ann.annotationName, ann.isModule, ann) }; - log.debug("Subclass fingerprints: " + subclasses) - log.debug("Annotation fingerprints: " + annotations) + def discover( + fingerprints: Seq[Fingerprint], + definitions: Seq[Definition], + log: Logger + ): (Seq[TestDefinition], Set[String]) = { + val subclasses = fingerprints collect { + case sub: SubclassFingerprint => (sub.superclassName, sub.isModule, sub) + }; + val annotations = fingerprints collect { + case ann: AnnotatedFingerprint => (ann.annotationName, ann.isModule, ann) + }; + log.debug("Subclass fingerprints: " + subclasses) + log.debug("Annotation fingerprints: " + annotations) - def firsts[A, B, C](s: Seq[(A, B, C)]): Set[A] = s.map(_._1).toSet - def defined(in: Seq[(String, Boolean, Fingerprint)], names: Set[String], IsModule: Boolean): Seq[Fingerprint] = - in collect { case (name, IsModule, print) if names(name) => print } + def firsts[A, B, C](s: Seq[(A, B, C)]): Set[A] = s.map(_._1).toSet + def defined( + in: Seq[(String, Boolean, Fingerprint)], + names: Set[String], + IsModule: Boolean + ): Seq[Fingerprint] = + in collect { case (name, IsModule, print) if names(name) => print } - def toFingerprints(d: Discovered): Seq[Fingerprint] = - defined(subclasses, d.baseClasses, d.isModule) ++ - defined(annotations, d.annotations, d.isModule) + def toFingerprints(d: Discovered): Seq[Fingerprint] = + defined(subclasses, d.baseClasses, d.isModule) ++ + defined(annotations, d.annotations, d.isModule) - val discovered = Discovery(firsts(subclasses), firsts(annotations))(definitions) - // TODO: To pass in correct explicitlySpecified and selectors - val tests = for ((df, di) <- discovered; fingerprint <- toFingerprints(di)) yield new TestDefinition(df.name, fingerprint, false, Array(new SuiteSelector)) - val mains = discovered collect { case (df, di) if di.hasMain => df.name } - (tests, mains.toSet) - } - - @deprecated("Tests.showResults() has been superseded with TestResultLogger and setting 'testResultLogger'.", "0.13.5") - def showResults(log: Logger, results: Output, noTestsMessage: => String): Unit = - TestResultLogger.Default.copy(printNoTests = TestResultLogger.const(_ info noTestsMessage)) - .run(log, results, "") + val discovered = Discovery(firsts(subclasses), firsts(annotations))(definitions.filter { + case c: ClassLike => + c.topLevel + case _ => false + }) + // TODO: To pass in correct explicitlySpecified and selectors + val tests = + for { + (df, di) <- discovered + fingerprint <- toFingerprints(di) + } yield new TestDefinition(df.name, fingerprint, false, Array(new SuiteSelector: Selector)) + val mains = discovered collect { case (df, di) if di.hasMain => df.name } + (tests, mains.toSet) + } } -final class TestsFailedException extends RuntimeException("Tests unsuccessful") with FeedbackProvidedException +final class TestsFailedException + extends RuntimeException("Tests unsuccessful") + with FeedbackProvidedException diff --git a/main-actions/src/main/scala/sbt/compiler/Eval.scala b/main-actions/src/main/scala/sbt/compiler/Eval.scala index 65bb9ad10..461a8c727 100644 --- a/main-actions/src/main/scala/sbt/compiler/Eval.scala +++ b/main-actions/src/main/scala/sbt/compiler/Eval.scala @@ -1,3 +1,10 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + package sbt package compiler @@ -8,9 +15,10 @@ import ast.parser.Tokens import reporters.{ ConsoleReporter, Reporter } import scala.reflect.internal.util.{ AbstractFileClassLoader, BatchSourceFile } import Tokens.{ EOF, NEWLINE, NEWLINES, SEMI } -import java.io.File +import java.io.{ File, FileNotFoundException } import java.nio.ByteBuffer import java.net.URLClassLoader +import java.security.MessageDigest import Eval.{ getModule, getValue, WrapValName } import sbt.io.{ DirectoryFilter, FileFilter, GlobFilter, Hash, IO, Path } @@ -26,7 +34,12 @@ final class EvalImports(val strings: Seq[(String, Int)], val srcName: String) * the module from that class loader. `generated` contains the compiled classes and cache files related * to the expression. The name of the auto-generated module wrapping the expression is `enclosingModule`. */ -final class EvalResult(val tpe: String, val getValue: ClassLoader => Any, val generated: Seq[File], val enclosingModule: String) +final class EvalResult( + val tpe: String, + val getValue: ClassLoader => Any, + val generated: Seq[File], + val enclosingModule: String +) /** * The result of evaluating a group of Scala definitions. The definitions are wrapped in an auto-generated, @@ -35,7 +48,12 @@ final class EvalResult(val tpe: String, val getValue: ClassLoader => Any, val ge * from the classpath that the definitions were compiled against. The list of vals with the requested types is `valNames`. * The values for these may be obtained by providing the parent class loader to `values` as is done with `loader`. */ -final class EvalDefinitions(val loader: ClassLoader => ClassLoader, val generated: Seq[File], val enclosingModule: String, val valNames: Seq[String]) { +final class EvalDefinitions( + val loader: ClassLoader => ClassLoader, + val generated: Seq[File], + val enclosingModule: String, + val valNames: Seq[String] +) { def values(parent: ClassLoader): Seq[Any] = { val module = getModule(enclosingModule, loader(parent)) for (n <- valNames) yield module.getClass.getMethod(n).invoke(module) @@ -44,27 +62,37 @@ final class EvalDefinitions(val loader: ClassLoader => ClassLoader, val generate final class EvalException(msg: String) extends RuntimeException(msg) // not thread safe, since it reuses a Global instance -final class Eval(optionsNoncp: Seq[String], classpath: Seq[File], mkReporter: Settings => Reporter, backing: Option[File]) { - def this(mkReporter: Settings => Reporter, backing: Option[File]) = this(Nil, IO.classLocationFile[Product] :: Nil, mkReporter, backing) +final class Eval( + optionsNoncp: Seq[String], + classpath: Seq[File], + mkReporter: Settings => Reporter, + backing: Option[File] +) { + def this(mkReporter: Settings => Reporter, backing: Option[File]) = + this(Nil, IO.classLocationPath[Product].toFile :: Nil, mkReporter, backing) def this() = this(s => new ConsoleReporter(s), None) backing.foreach(IO.createDirectory) val classpathString = Path.makeString(classpath ++ backing.toList) val options = "-cp" +: classpathString +: optionsNoncp - lazy val settings = - { - val s = new Settings(println) - new CompilerCommand(options.toList, s) // this side-effects on Settings.. - s - } + lazy val settings = { + val s = new Settings(println) + new CompilerCommand(options.toList, s) // this side-effects on Settings.. + s + } lazy val reporter = mkReporter(settings) + /** * Subclass of Global which allows us to mutate currentRun from outside. * See for rationale https://issues.scala-lang.org/browse/SI-8794 */ - final class EvalGlobal(settings: Settings, reporter: Reporter) extends Global(settings, reporter) { - override def currentRun: Run = curRun + final class EvalGlobal(settings: Settings, reporter: Reporter) + extends Global(settings, reporter) { + override def currentRun: Run = curRun match { + case null => super.currentRun // https://github.com/scala/bug/issues/11381 + case r => r + } var curRun: Run = null } lazy val global: EvalGlobal = new EvalGlobal(settings, reporter) @@ -78,116 +106,167 @@ final class Eval(optionsNoncp: Seq[String], classpath: Seq[File], mkReporter: Se private[this] var toUnlinkLater = List[Symbol]() private[this] def unlink(sym: Symbol) = sym.owner.info.decls.unlink(sym) - def eval(expression: String, imports: EvalImports = noImports, tpeName: Option[String] = None, srcName: String = "", line: Int = DefaultStartLine): EvalResult = - { - val ev = new EvalType[String] { - def makeUnit = mkUnit(srcName, line, expression) - def unlink = true - def unitBody(unit: CompilationUnit, importTrees: Seq[Tree], moduleName: String): Tree = { - val (parser, tree) = parse(unit, settingErrorStrings, _.expr()) - val tpt: Tree = expectedType(tpeName) - augment(parser, importTrees, tree, tpt, moduleName) - } - def extra(run: Run, unit: CompilationUnit) = enteringPhase(run.typerPhase.next) { (new TypeExtractor).getType(unit.body) } - def read(file: File) = IO.read(file) - def write(value: String, f: File) = IO.write(f, value) - def extraHash = "" + def eval( + expression: String, + imports: EvalImports = noImports, + tpeName: Option[String] = None, + srcName: String = "", + line: Int = DefaultStartLine + ): EvalResult = { + val ev = new EvalType[String] { + def makeUnit = mkUnit(srcName, line, expression) + def unlink = true + def unitBody(unit: CompilationUnit, importTrees: Seq[Tree], moduleName: String): Tree = { + val (parser, tree) = parse(unit, settingErrorStrings, _.expr()) + val tpt: Tree = expectedType(tpeName) + augment(parser, importTrees, tree, tpt, moduleName) } - val i = evalCommon(expression :: Nil, imports, tpeName, ev) - val value = (cl: ClassLoader) => getValue[Any](i.enclosingModule, i.loader(cl)) - new EvalResult(i.extra, value, i.generated, i.enclosingModule) + def extra(run: Run, unit: CompilationUnit) = enteringPhase(run.typerPhase.next) { + (new TypeExtractor).getType(unit.body) + } + def read(file: File) = IO.read(file) + def write(value: String, f: File) = IO.write(f, value) + def extraHash = "" } - def evalDefinitions(definitions: Seq[(String, scala.Range)], imports: EvalImports, srcName: String, file: Option[File], valTypes: Seq[String]): EvalDefinitions = - { - require(definitions.nonEmpty, "Definitions to evaluate cannot be empty.") - val ev = new EvalType[Seq[String]] { - lazy val (fullUnit, defUnits) = mkDefsUnit(srcName, definitions) - def makeUnit = fullUnit - def unlink = false - def unitBody(unit: CompilationUnit, importTrees: Seq[Tree], moduleName: String): Tree = { - val fullParser = new syntaxAnalyzer.UnitParser(unit) - val trees = defUnits flatMap parseDefinitions - syntheticModule(fullParser, importTrees, trees.toList, moduleName) - } - def extra(run: Run, unit: CompilationUnit) = { - enteringPhase(run.typerPhase.next) { (new ValExtractor(valTypes.toSet)).getVals(unit.body) } - } - def read(file: File) = IO.readLines(file) - def write(value: Seq[String], file: File) = IO.writeLines(file, value) - def extraHash = file match { - case Some(f) => f.getAbsolutePath - case None => "" + val i = evalCommon(expression :: Nil, imports, tpeName, ev) + val value = (cl: ClassLoader) => getValue[Any](i.enclosingModule, i.loader(cl)) + new EvalResult(i.extra, value, i.generated, i.enclosingModule) + } + def evalDefinitions( + definitions: Seq[(String, scala.Range)], + imports: EvalImports, + srcName: String, + file: Option[File], + valTypes: Seq[String] + ): EvalDefinitions = { + require(definitions.nonEmpty, "Definitions to evaluate cannot be empty.") + val ev = new EvalType[Seq[String]] { + lazy val (fullUnit, defUnits) = mkDefsUnit(srcName, definitions) + def makeUnit = fullUnit + def unlink = false + def unitBody(unit: CompilationUnit, importTrees: Seq[Tree], moduleName: String): Tree = { + val fullParser = new syntaxAnalyzer.UnitParser(unit) + val trees = defUnits flatMap parseDefinitions + syntheticModule(fullParser, importTrees, trees.toList, moduleName) + } + def extra(run: Run, unit: CompilationUnit) = { + enteringPhase(run.typerPhase.next) { + (new ValExtractor(valTypes.toSet)).getVals(unit.body) } } - val i = evalCommon(definitions.map(_._1), imports, Some(""), ev) - new EvalDefinitions(i.loader, i.generated, i.enclosingModule, i.extra) + def read(file: File) = IO.readLines(file) + def write(value: Seq[String], file: File) = IO.writeLines(file, value) + def extraHash = file match { + case Some(f) => f.getAbsolutePath + case None => "" + } + } + val i: EvalIntermediate[Seq[String]] = evalCommon(definitions.map(_._1), imports, Some(""), ev) + new EvalDefinitions(i.loader, i.generated, i.enclosingModule, i.extra.reverse) + } + + private[this] def evalCommon[T]( + content: Seq[String], + imports: EvalImports, + tpeName: Option[String], + ev: EvalType[T] + ): EvalIntermediate[T] = { + import Eval._ + // TODO - We also encode the source of the setting into the hash to avoid conflicts where the exact SAME setting + // is defined in multiple evaluated instances with a backing. This leads to issues with finding a previous + // value on the classpath when compiling. + + // This is a hot path. + val digester = MessageDigest.getInstance("SHA") + content foreach { c => + digester.update(bytes(c)) + } + backing foreach { x => + digester.update(fileExistsBytes(x)) + } + options foreach { o => + digester.update(bytes(o)) + } + classpath foreach { f => + fileModifiedHash(f, digester) + } + imports.strings.map(_._1) foreach { x => + digester.update(bytes(x)) + } + tpeName foreach { x => + digester.update(bytes(x)) + } + digester.update(bytes(ev.extraHash)) + val d = digester.digest() + + val hash = Hash.toHex(d) + val moduleName = makeModuleName(hash) + + lazy val unit = { + reporter.reset + ev.makeUnit + } + lazy val run = new Run { + override def units = (unit :: Nil).iterator + } + def unlinkAll(): Unit = + for ((sym, _) <- run.symSource) if (ev.unlink) unlink(sym) else toUnlinkLater ::= sym + + val (extra, loader) = backing match { + case Some(back) if classExists(back, moduleName) => + val loader = (parent: ClassLoader) => + (new URLClassLoader(Array(back.toURI.toURL), parent): ClassLoader) + val extra = ev.read(cacheFile(back, moduleName)) + (extra, loader) + case _ => + try { + compileAndLoad(run, unit, imports, backing, moduleName, ev) + } finally { + unlinkAll() + } } - private[this] def evalCommon[T](content: Seq[String], imports: EvalImports, tpeName: Option[String], ev: EvalType[T]): EvalIntermediate[T] = - { - import Eval._ - // TODO - We also encode the source of the setting into the hash to avoid conflicts where the exact SAME setting - // is defined in multiple evaluated instances with a backing. This leads to issues with finding a previous - // value on the classpath when compiling. - val hash = Hash.toHex(Hash(bytes(stringSeqBytes(content) :: optBytes(backing)(fileExistsBytes) :: stringSeqBytes(options) :: - seqBytes(classpath)(fileModifiedBytes) :: stringSeqBytes(imports.strings.map(_._1)) :: optBytes(tpeName)(bytes) :: - bytes(ev.extraHash) :: Nil))) - val moduleName = makeModuleName(hash) - - lazy val unit = { - reporter.reset - ev.makeUnit - } - lazy val run = new Run { - override def units = (unit :: Nil).iterator - } - def unlinkAll(): Unit = for ((sym, _) <- run.symSource) if (ev.unlink) unlink(sym) else toUnlinkLater ::= sym - - val (extra, loader) = backing match { - case Some(back) if classExists(back, moduleName) => - val loader = (parent: ClassLoader) => new URLClassLoader(Array(back.toURI.toURL), parent) - val extra = ev.read(cacheFile(back, moduleName)) - (extra, loader) - case _ => - try { compileAndLoad(run, unit, imports, backing, moduleName, ev) } - finally { unlinkAll() } - } - - val generatedFiles = getGeneratedFiles(backing, moduleName) - new EvalIntermediate(extra, loader, generatedFiles, moduleName) - } + val generatedFiles = getGeneratedFiles(backing, moduleName) + new EvalIntermediate(extra, loader, generatedFiles, moduleName) + } // location of the cached type or definition information - private[this] def cacheFile(base: File, moduleName: String): File = new File(base, moduleName + ".cache") - private[this] def compileAndLoad[T](run: Run, unit: CompilationUnit, imports: EvalImports, backing: Option[File], moduleName: String, ev: EvalType[T]): (T, ClassLoader => ClassLoader) = - { - global.curRun = run - run.currentUnit = unit - val dir = outputDirectory(backing) - settings.outputDirs setSingleOutput dir + private[this] def cacheFile(base: File, moduleName: String): File = + new File(base, moduleName + ".cache") + private[this] def compileAndLoad[T]( + run: Run, + unit: CompilationUnit, + imports: EvalImports, + backing: Option[File], + moduleName: String, + ev: EvalType[T] + ): (T, ClassLoader => ClassLoader) = { + global.curRun = run + run.currentUnit = unit + val dir = outputDirectory(backing) + settings.outputDirs setSingleOutput dir - val importTrees = parseImports(imports) - unit.body = ev.unitBody(unit, importTrees, moduleName) + val importTrees = parseImports(imports) + unit.body = ev.unitBody(unit, importTrees, moduleName) - def compile(phase: Phase): Unit = - { - globalPhase = phase - if (phase == null || phase == phase.next || reporter.hasErrors) - () - else { - enteringPhase(phase) { phase.run } - compile(phase.next) - } - } - - compile(run.namerPhase) - checkError("Type error in expression") - - val extra = ev.extra(run, unit) - for (f <- backing) ev.write(extra, cacheFile(f, moduleName)) - val loader = (parent: ClassLoader) => new AbstractFileClassLoader(dir, parent) - (extra, loader) + def compile(phase: Phase): Unit = { + globalPhase = phase + if (phase == null || phase == phase.next || reporter.hasErrors) + () + else { + enteringPhase(phase) { phase.run } + compile(phase.next) + } } + compile(run.namerPhase) + checkError("Type error in expression") + + val extra = ev.extra(run, unit) + for (f <- backing) ev.write(extra, cacheFile(f, moduleName)) + val loader = (parent: ClassLoader) => new AbstractFileClassLoader(dir, parent) + (extra, loader) + } + private[this] def expectedType(tpeName: Option[String]): Tree = tpeName match { case Some(tpe) => parseType(tpe) @@ -195,43 +274,62 @@ final class Eval(optionsNoncp: Seq[String], classpath: Seq[File], mkReporter: Se } private[this] def outputDirectory(backing: Option[File]): AbstractFile = - backing match { case None => new VirtualDirectory("", None); case Some(dir) => new PlainFile(dir) } + backing match { + case None => new VirtualDirectory("", None); case Some(dir) => new PlainFile(dir) + } - def load(dir: AbstractFile, moduleName: String): ClassLoader => Any = parent => getValue[Any](moduleName, new AbstractFileClassLoader(dir, parent)) - def loadPlain(dir: File, moduleName: String): ClassLoader => Any = parent => getValue[Any](moduleName, new URLClassLoader(Array(dir.toURI.toURL), parent)) + def load(dir: AbstractFile, moduleName: String): ClassLoader => Any = + parent => getValue[Any](moduleName, new AbstractFileClassLoader(dir, parent)) + def loadPlain(dir: File, moduleName: String): ClassLoader => Any = + parent => getValue[Any](moduleName, new URLClassLoader(Array(dir.toURI.toURL), parent)) //wrap tree in object objectName { def WrapValName = } - def augment(parser: global.syntaxAnalyzer.UnitParser, imports: Seq[Tree], tree: Tree, tpt: Tree, objectName: String): Tree = - { - val method = DefDef(NoMods, newTermName(WrapValName), Nil, Nil, tpt, tree) - syntheticModule(parser, imports, method :: Nil, objectName) - } - private[this] def syntheticModule(parser: global.syntaxAnalyzer.UnitParser, imports: Seq[Tree], definitions: List[Tree], objectName: String): Tree = - { - val emptyTypeName = nme.EMPTY.toTypeName - def emptyPkg = parser.atPos(0, 0, 0) { Ident(nme.EMPTY_PACKAGE_NAME) } - def emptyInit = DefDef( - NoMods, - nme.CONSTRUCTOR, - Nil, - List(Nil), - TypeTree(), - Block(List(Apply(Select(Super(This(emptyTypeName), emptyTypeName), nme.CONSTRUCTOR), Nil)), Literal(Constant(()))) + def augment( + parser: global.syntaxAnalyzer.UnitParser, + imports: Seq[Tree], + tree: Tree, + tpt: Tree, + objectName: String + ): Tree = { + val method = DefDef(NoMods, newTermName(WrapValName), Nil, Nil, tpt, tree) + syntheticModule(parser, imports, method :: Nil, objectName) + } + private[this] def syntheticModule( + parser: global.syntaxAnalyzer.UnitParser, + imports: Seq[Tree], + definitions: List[Tree], + objectName: String + ): Tree = { + val emptyTypeName = nme.EMPTY.toTypeName + def emptyPkg = parser.atPos(0, 0, 0) { Ident(nme.EMPTY_PACKAGE_NAME) } + def emptyInit = DefDef( + NoMods, + nme.CONSTRUCTOR, + Nil, + List(Nil), + TypeTree(), + Block( + List(Apply(Select(Super(This(emptyTypeName), emptyTypeName), nme.CONSTRUCTOR), Nil)), + Literal(Constant(())) ) + ) - def moduleBody = Template(List(gen.scalaAnyRefConstr), noSelfType, emptyInit :: definitions) - def moduleDef = ModuleDef(NoMods, newTermName(objectName), moduleBody) - parser.makePackaging(0, emptyPkg, (imports :+ moduleDef).toList) - } + def moduleBody = + Template(List(gen.scalaAnyRefConstr), noSelfType, (emptyInit: Tree) :: definitions) + def moduleDef = ModuleDef(NoMods, newTermName(objectName), moduleBody) + parser.makePackaging(0, emptyPkg, (imports :+ (moduleDef: Tree)).toList) + } private[this] final class TypeExtractor extends Traverser { private[this] var result = "" def getType(t: Tree) = { result = ""; traverse(t); result } override def traverse(tree: Tree): Unit = tree match { - case d: DefDef if d.symbol.nameString == WrapValName => result = d.symbol.tpe.finalResultType.toString + case d: DefDef if d.symbol.nameString == WrapValName => + result = d.symbol.tpe.finalResultType.toString case _ => super.traverse(tree) } } + /** Tree traverser that obtains the names of vals in a top-level module whose type is a subtype of one of `types`.*/ private[this] final class ValExtractor(tpes: Set[String]) extends Traverser { private[this] var vals = List[String]() @@ -242,15 +340,22 @@ final class Eval(optionsNoncp: Seq[String], classpath: Seq[File], mkReporter: Se } } override def traverse(tree: Tree): Unit = tree match { - case ValDef(_, n, actualTpe, _) if isTopLevelModule(tree.symbol.owner) && isAcceptableType(actualTpe.tpe) => + case ValDef(_, n, actualTpe, _) + if isTopLevelModule(tree.symbol.owner) && isAcceptableType(actualTpe.tpe) => vals ::= n.dropLocal.encoded case _ => super.traverse(tree) } } // inlined implemented of Symbol.isTopLevelModule that was removed in e5b050814deb2e7e1d6d05511d3a6cb6b013b549 - private[this] def isTopLevelModule(s: Symbol): Boolean = s.hasFlag(reflect.internal.Flags.MODULE) && s.owner.isPackageClass + private[this] def isTopLevelModule(s: Symbol): Boolean = + s.hasFlag(reflect.internal.Flags.MODULE) && s.owner.isPackageClass - private[this] final class EvalIntermediate[T](val extra: T, val loader: ClassLoader => ClassLoader, val generated: Seq[File], val enclosingModule: String) + private[this] final class EvalIntermediate[T]( + val extra: T, + val loader: ClassLoader => ClassLoader, + val generated: Seq[File], + val enclosingModule: String + ) private[this] def classExists(dir: File, name: String) = (new File(dir, name + ".class")).exists // TODO: use the code from Analyzer @@ -263,12 +368,13 @@ final class Eval(optionsNoncp: Seq[String], classpath: Seq[File], mkReporter: Se def accept(dir: File, s: String) = (s contains moduleName) } - private[this] def moduleClassFilter(moduleName: String) = new java.io.FilenameFilter { - def accept(dir: File, s: String) = - (s contains moduleName) && (s endsWith ".class") - } - private[this] class ParseErrorStrings(val base: String, val extraBlank: String, val missingBlank: String, val extraSemi: String) + private[this] class ParseErrorStrings( + val base: String, + val extraBlank: String, + val missingBlank: String, + val extraSemi: String + ) private[this] def definitionErrorStrings = new ParseErrorStrings( base = "Error parsing definition.", extraBlank = " Ensure that there are no blank lines within a definition.", @@ -279,67 +385,69 @@ final class Eval(optionsNoncp: Seq[String], classpath: Seq[File], mkReporter: Se base = "Error parsing expression.", extraBlank = " Ensure that there are no blank lines within a setting.", missingBlank = " Ensure that settings are separated by blank lines.", - extraSemi = " Note that settings are expressions and do not end with semicolons. (Semicolons are fine within {} blocks, however.)" + extraSemi = + " Note that settings are expressions and do not end with semicolons. (Semicolons are fine within {} blocks, however.)" ) /** * Parses the provided compilation `unit` according to `f` and then performs checks on the final parser state * to catch errors that are common when the content is embedded in a blank-line-delimited format. */ - private[this] def parse[T](unit: CompilationUnit, errors: ParseErrorStrings, f: syntaxAnalyzer.UnitParser => T): (syntaxAnalyzer.UnitParser, T) = - { - val parser = new syntaxAnalyzer.UnitParser(unit) + private[this] def parse[T]( + unit: CompilationUnit, + errors: ParseErrorStrings, + f: syntaxAnalyzer.UnitParser => T + ): (syntaxAnalyzer.UnitParser, T) = { + val parser = new syntaxAnalyzer.UnitParser(unit) - val tree = f(parser) - val extra = parser.in.token match { - case EOF => errors.extraBlank - case _ => "" - } - checkError(errors.base + extra) - - parser.accept(EOF) - val extra2 = parser.in.token match { - case SEMI => errors.extraSemi - case NEWLINE | NEWLINES => errors.missingBlank - case _ => "" - } - checkError(errors.base + extra2) - - (parser, tree) + val tree = f(parser) + val extra = parser.in.token match { + case EOF => errors.extraBlank + case _ => "" } - private[this] def parseType(tpe: String): Tree = - { - val tpeParser = new syntaxAnalyzer.UnitParser(mkUnit("", DefaultStartLine, tpe)) - val tpt0: Tree = tpeParser.typ() - tpeParser.accept(EOF) - checkError("Error parsing expression type.") - tpt0 + checkError(errors.base + extra) + + parser.accept(EOF) + val extra2 = parser.in.token match { + case SEMI => errors.extraSemi + case NEWLINE | NEWLINES => errors.missingBlank + case _ => "" } + checkError(errors.base + extra2) + + (parser, tree) + } + private[this] def parseType(tpe: String): Tree = { + val tpeParser = new syntaxAnalyzer.UnitParser(mkUnit("", DefaultStartLine, tpe)) + val tpt0: Tree = tpeParser.typ() + tpeParser.accept(EOF) + checkError("Error parsing expression type.") + tpt0 + } private[this] def parseImports(imports: EvalImports): Seq[Tree] = imports.strings flatMap { case (s, line) => parseImport(mkUnit(imports.srcName, line, s)) } - private[this] def parseImport(importUnit: CompilationUnit): Seq[Tree] = - { - val parser = new syntaxAnalyzer.UnitParser(importUnit) - val trees: Seq[Tree] = parser.importClause() - parser.accept(EOF) - checkError("Error parsing imports for expression.") - trees - } + private[this] def parseImport(importUnit: CompilationUnit): Seq[Tree] = { + val parser = new syntaxAnalyzer.UnitParser(importUnit) + val trees: Seq[Tree] = parser.importClause() + parser.accept(EOF) + checkError("Error parsing imports for expression.") + trees + } private[this] def parseDefinitions(du: CompilationUnit): Seq[Tree] = parse(du, definitionErrorStrings, parseDefinitions)._2 /** Parses one or more definitions (defs, vals, lazy vals, classes, traits, modules). */ - private[this] def parseDefinitions(parser: syntaxAnalyzer.UnitParser): Seq[Tree] = - { - val defs = ListBuffer[Tree]() - do { - defs ++= parser.nonLocalDefOrDcl - parser.acceptStatSepOpt() - } while (!parser.isStatSeqEnd) - defs.toList - } + private[this] def parseDefinitions(parser: syntaxAnalyzer.UnitParser): Seq[Tree] = { + val defs = ListBuffer[Tree]() + do { + defs ++= parser.nonLocalDefOrDcl + parser.acceptStatSepOpt() + } while (!parser.isStatSeqEnd) + defs.toList + } private[this] trait EvalType[T] { + /** Extracts additional information after the compilation unit is evaluated.*/ def extra(run: Run, unit: CompilationUnit): T @@ -373,47 +481,56 @@ final class Eval(optionsNoncp: Seq[String], classpath: Seq[File], mkReporter: Se val DefaultStartLine = 0 private[this] def makeModuleName(hash: String): String = "$" + Hash.halve(hash) private[this] def noImports = new EvalImports(Nil, "") - private[this] def mkUnit(srcName: String, firstLine: Int, s: String) = new CompilationUnit(new EvalSourceFile(srcName, firstLine, s)) - private[this] def checkError(label: String) = if (reporter.hasErrors) throw new EvalException(label) + private[this] def mkUnit(srcName: String, firstLine: Int, s: String) = + new CompilationUnit(new EvalSourceFile(srcName, firstLine, s)) + private[this] def checkError(label: String) = + if (reporter.hasErrors) throw new EvalException(label) - private[this] final class EvalSourceFile(name: String, startLine: Int, contents: String) extends BatchSourceFile(name, contents) { + private[this] final class EvalSourceFile(name: String, startLine: Int, contents: String) + extends BatchSourceFile(name, contents) { override def lineToOffset(line: Int): Int = super.lineToOffset((line - startLine) max 0) override def offsetToLine(offset: Int): Int = super.offsetToLine(offset) + startLine } + /** * Constructs a CompilationUnit for each definition, which can be used to independently parse the definition into a Tree. * Additionally, a CompilationUnit for the combined definitions is constructed for use by combined compilation after parsing. */ - private[this] def mkDefsUnit(srcName: String, definitions: Seq[(String, scala.Range)]): (CompilationUnit, Seq[CompilationUnit]) = - { - def fragmentUnit(content: String, lineMap: Array[Int]) = new CompilationUnit(fragmentSourceFile(srcName, content, lineMap)) + private[this] def mkDefsUnit( + srcName: String, + definitions: Seq[(String, scala.Range)] + ): (CompilationUnit, Seq[CompilationUnit]) = { + def fragmentUnit(content: String, lineMap: Array[Int]) = + new CompilationUnit(fragmentSourceFile(srcName, content, lineMap)) - import collection.mutable.ListBuffer - val lines = new ListBuffer[Int]() - val defs = new ListBuffer[CompilationUnit]() - val fullContent = new java.lang.StringBuilder() - for ((defString, range) <- definitions) { - defs += fragmentUnit(defString, range.toArray) - fullContent.append(defString) - lines ++= range - fullContent.append("\n\n") - lines ++= (range.end :: range.end :: Nil) - } - val fullUnit = fragmentUnit(fullContent.toString, lines.toArray) - (fullUnit, defs.toSeq) + import collection.mutable.ListBuffer + val lines = new ListBuffer[Int]() + val defs = new ListBuffer[CompilationUnit]() + val fullContent = new java.lang.StringBuilder() + for ((defString, range) <- definitions) { + defs += fragmentUnit(defString, range.toArray) + fullContent.append(defString) + lines ++= range + fullContent.append("\n\n") + lines ++= (range.end :: range.end :: Nil) } + val fullUnit = fragmentUnit(fullContent.toString, lines.toArray) + (fullUnit, defs.toSeq) + } /** * Source file that can map the offset in the file to and from line numbers that may discontinuous. * The values in `lineMap` must be ordered, but need not be consecutive. */ - private[this] def fragmentSourceFile(srcName: String, content: String, lineMap: Array[Int]) = new BatchSourceFile(srcName, content) { - override def lineToOffset(line: Int): Int = super.lineToOffset(lineMap.indexWhere(_ == line) max 0) - override def offsetToLine(offset: Int): Int = index(lineMap, super.offsetToLine(offset)) - // the SourceFile attribute is populated from this method, so we are required to only return the name - override def toString = new File(srcName).getName - private[this] def index(a: Array[Int], i: Int): Int = if (i < 0 || i >= a.length) 0 else a(i) - } + private[this] def fragmentSourceFile(srcName: String, content: String, lineMap: Array[Int]) = + new BatchSourceFile(srcName, content) { + override def lineToOffset(line: Int): Int = + super.lineToOffset(lineMap.indexWhere(_ == line) max 0) + override def offsetToLine(offset: Int): Int = index(lineMap, super.offsetToLine(offset)) + // the SourceFile attribute is populated from this method, so we are required to only return the name + override def toString = new File(srcName).getName + private[this] def index(a: Array[Int], i: Int): Int = if (i < 0 || i >= a.length) 0 else a(i) + } } private[sbt] object Eval { def optBytes[T](o: Option[T])(f: T => Array[Byte]): Array[Byte] = seqBytes(o.toSeq)(f) @@ -421,27 +538,41 @@ private[sbt] object Eval { def seqBytes[T](s: Seq[T])(f: T => Array[Byte]): Array[Byte] = bytes(s map f) def bytes(b: Seq[Array[Byte]]): Array[Byte] = bytes(b.length) ++ b.flatten.toArray[Byte] def bytes(b: Boolean): Array[Byte] = Array[Byte](if (b) 1 else 0) - def filesModifiedBytes(fs: Array[File]): Array[Byte] = if (fs eq null) filesModifiedBytes(Array[File]()) else seqBytes(fs)(fileModifiedBytes) - def fileModifiedBytes(f: File): Array[Byte] = - (if (f.isDirectory) filesModifiedBytes(f listFiles classDirFilter) else bytes(f.lastModified)) ++ - bytes(f.getAbsolutePath) + + // fileModifiedBytes is a hot method, taking up 0.85% of reload time + // This is a procedural version + def fileModifiedHash(f: File, digester: MessageDigest): Unit = { + if (f.isDirectory) + (f listFiles classDirFilter) foreach { x => + fileModifiedHash(x, digester) + } else digester.update(bytes(getModifiedTimeOrZero(f))) + + digester.update(bytes(f.getAbsolutePath)) + } + + // This uses NIO instead of the JNA-based IO.getModifiedTimeOrZero for speed + def getModifiedTimeOrZero(f: File): Long = + try { + sbt.io.JavaMilli.getModifiedTime(f.getPath) + } catch { + case _: FileNotFoundException => 0L + } + def fileExistsBytes(f: File): Array[Byte] = bytes(f.exists) ++ bytes(f.getAbsolutePath) def bytes(s: String): Array[Byte] = s getBytes "UTF-8" - def bytes(l: Long): Array[Byte] = - { - val buffer = ByteBuffer.allocate(8) - buffer.putLong(l) - buffer.array - } - def bytes(i: Int): Array[Byte] = - { - val buffer = ByteBuffer.allocate(4) - buffer.putInt(i) - buffer.array - } + def bytes(l: Long): Array[Byte] = { + val buffer = ByteBuffer.allocate(8) + buffer.putLong(l) + buffer.array + } + def bytes(i: Int): Array[Byte] = { + val buffer = ByteBuffer.allocate(4) + buffer.putInt(i) + buffer.array + } /** The name of the synthetic val in the synthetic module that an expression is assigned to. */ final val WrapValName = "$sbtdef" @@ -450,20 +581,18 @@ private[sbt] object Eval { * Gets the value of the expression wrapped in module `objectName`, which is accessible via `loader`. * The module name should not include the trailing `$`. */ - def getValue[T](objectName: String, loader: ClassLoader): T = - { - val module = getModule(objectName, loader) - val accessor = module.getClass.getMethod(WrapValName) - val value = accessor.invoke(module) - value.asInstanceOf[T] - } + def getValue[T](objectName: String, loader: ClassLoader): T = { + val module = getModule(objectName, loader) + val accessor = module.getClass.getMethod(WrapValName) + val value = accessor.invoke(module) + value.asInstanceOf[T] + } /** Gets the top-level module `moduleName` from the provided class `loader`. The module name should not include the trailing `$`.*/ - def getModule(moduleName: String, loader: ClassLoader): Any = - { - val clazz = Class.forName(moduleName + "$", true, loader) - clazz.getField("MODULE$").get(null) - } + def getModule(moduleName: String, loader: ClassLoader): Any = { + val clazz = Class.forName(moduleName + "$", true, loader) + clazz.getField("MODULE$").get(null) + } private val classDirFilter: FileFilter = DirectoryFilter || GlobFilter("*.class") } diff --git a/main-actions/src/test/scala/sbt/CacheIvyTest.scala b/main-actions/src/test/scala/sbt/CacheIvyTest.scala index c185ef39c..17cb778e1 100644 --- a/main-actions/src/test/scala/sbt/CacheIvyTest.scala +++ b/main-actions/src/test/scala/sbt/CacheIvyTest.scala @@ -1,34 +1,78 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + package sbt import org.scalacheck._ import org.scalacheck.Arbitrary._ import Prop._ import sbt.librarymanagement._ +import sjsonnew.shaded.scalajson.ast.unsafe.JValue class CacheIvyTest extends Properties("CacheIvy") { - import CacheIvy._ - import sbinary.Operations._ - import sbinary._ - import sbinary.DefaultProtocol._ + import sbt.util.{ CacheStore, SingletonCache } + import SingletonCache._ - private def cachePreservesEquality[T: Format](m: T, eq: (T, T) => Prop, str: T => String): Prop = { - val out = fromByteArray[T](toByteArray(m)) + import sjsonnew._ + import sjsonnew.support.scalajson.unsafe.Converter + + private class InMemoryStore(converter: SupportConverter[JValue]) extends CacheStore { + private var content: JValue = _ + override def delete(): Unit = () + override def close(): Unit = () + + override def read[T: JsonReader](): T = + try converter.fromJsonUnsafe[T](content) + catch { case t: Throwable => t.printStackTrace(); throw t } + + override def read[T: JsonReader](default: => T): T = + try read[T]() + catch { case _: Throwable => default } + + override def write[T: JsonWriter](value: T): Unit = + content = converter.toJsonUnsafe(value) + } + + private def testCache[T: JsonFormat, U]( + f: (SingletonCache[T], CacheStore) => U + )(implicit cache: SingletonCache[T]): U = { + val store = new InMemoryStore(Converter) + f(cache, store) + } + + private def cachePreservesEquality[T: JsonFormat]( + m: T, + eq: (T, T) => Prop, + str: T => String + ): Prop = testCache[T, Prop] { (cache, store) => + cache.write(store, m) + val out = cache.read(store) eq(out, m) :| s"Expected: ${str(m)}" :| s"Got: ${str(out)}" } - implicit val arbExclusionRule: Arbitrary[ExclusionRule] = Arbitrary( + implicit val arbConfigRef: Arbitrary[ConfigRef] = Arbitrary( + for { + n <- Gen.alphaStr + } yield ConfigRef(n) + ) + + implicit val arbExclusionRule: Arbitrary[InclExclRule] = Arbitrary( for { o <- Gen.alphaStr n <- Gen.alphaStr a <- Gen.alphaStr - cs <- arbitrary[List[String]] - } yield ExclusionRule(o, n, a, cs) + v <- arbCrossVersion.arbitrary + cs <- arbitrary[List[ConfigRef]] + } yield InclExclRule(o, n, a, cs.toVector, v) ) implicit val arbCrossVersion: Arbitrary[CrossVersion] = Arbitrary { // Actual functions don't matter, just Disabled vs Binary vs Full - import CrossVersion._ - Gen.oneOf(Disabled, new Binary(identity), new Full(identity)) + Gen.oneOf(Disabled(), Binary(), Full()) } implicit val arbArtifact: Arbitrary[Artifact] = Arbitrary { @@ -48,14 +92,25 @@ class CacheIvyTest extends Properties("CacheIvy") { isTransitive <- arbitrary[Boolean] isForce <- arbitrary[Boolean] explicitArtifacts <- Gen.listOf(arbitrary[Artifact]) - exclusions <- Gen.listOf(arbitrary[ExclusionRule]) - inclusions <- Gen.listOf(arbitrary[InclusionRule]) + exclusions <- Gen.listOf(arbitrary[InclExclRule]) + inclusions <- Gen.listOf(arbitrary[InclExclRule]) extraAttributes <- Gen.mapOf(arbitrary[(String, String)]) crossVersion <- arbitrary[CrossVersion] } yield ModuleID( - organization = o, name = n, revision = r, configurations = cs, isChanging = isChanging, isTransitive = isTransitive, - isForce = isForce, explicitArtifacts = explicitArtifacts, inclusions = inclusions, exclusions = exclusions, - extraAttributes = extraAttributes, crossVersion = crossVersion, branchName = branch) + organization = o, + name = n, + revision = r, + configurations = cs, + isChanging = isChanging, + isTransitive = isTransitive, + isForce = isForce, + explicitArtifacts = explicitArtifacts.toVector, + inclusions = inclusions.toVector, + exclusions = exclusions.toVector, + extraAttributes = extraAttributes, + crossVersion = crossVersion, + branchName = branch + ) } property("moduleIDFormat") = forAll { (m: ModuleID) => @@ -65,16 +120,16 @@ class CacheIvyTest extends Properties("CacheIvy") { s"$inclusions, $extraAttributes, $crossVersion, $branchName)" } def eq(a: ModuleID, b: ModuleID): Prop = { - import CrossVersion._ - def rest = a.copy(crossVersion = b.crossVersion) == b + def rest = a.withCrossVersion(b.crossVersion) == b (a.crossVersion, b.crossVersion) match { - case (Disabled, Disabled) => rest - case (_: Binary, _: Binary) => rest - case (_: Full, _: Full) => rest - case (a, b) => Prop(false) :| s"CrossVersions don't match: $a vs $b" + case (_: Disabled, _: Disabled) => rest + case (_: Binary, _: Binary) => rest + case (_: Full, _: Full) => rest + case (a, b) => Prop(false) :| s"CrossVersions don't match: $a vs $b" } } + import sbt.librarymanagement.LibraryManagementCodec._ cachePreservesEquality(m, eq _, str) } } diff --git a/main-actions/src/test/scala/sbt/compiler/EvalTest.scala b/main-actions/src/test/scala/sbt/compiler/EvalTest.scala index dfdd839cf..21ffe61b9 100644 --- a/main-actions/src/test/scala/sbt/compiler/EvalTest.scala +++ b/main-actions/src/test/scala/sbt/compiler/EvalTest.scala @@ -1,37 +1,47 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + package sbt package compiler import scala.language.reflectiveCalls import org.scalacheck._ import Prop._ +import scala.tools.nsc.Settings import scala.tools.nsc.reporters.StoreReporter import sbt.io.IO -object EvalTest extends Properties("eval") { - private[this] val reporter = new StoreReporter - import reporter.{ ERROR, Info } - private[this] val eval = new Eval(_ => reporter, None) +class EvalTest extends Properties("eval") { + private[this] lazy val reporter = new StoreReporter(new Settings()) + import reporter.ERROR + private[this] lazy val eval = new Eval(_ => reporter, None) property("inferred integer") = forAll { (i: Int) => val result = eval.eval(i.toString) (label("Value", value(result)) |: (value(result) == i)) && - (label("Type", value(result)) |: (result.tpe == IntType)) && - (label("Files", result.generated) |: (result.generated.isEmpty)) + (label("Type", value(result)) |: (result.tpe == IntType)) && + (label("Files", result.generated) |: (result.generated.isEmpty)) } property("explicit integer") = forAll { (i: Int) => val result = eval.eval(i.toString, tpeName = Some(IntType)) (label("Value", value(result)) |: (value(result) == i)) && - (label("Type", result.tpe) |: (result.tpe == IntType)) && - (label("Files", result.generated) |: (result.generated.isEmpty)) + (label("Type", result.tpe) |: (result.tpe == IntType)) && + (label("Files", result.generated) |: (result.generated.isEmpty)) } property("type mismatch") = forAll { (i: Int, l: Int) => val line = math.abs(l) val src = "mismatch" - throws(classOf[RuntimeException])(eval.eval(i.toString, tpeName = Some(BooleanType), line = line, srcName = src)) && - hasErrors(line + 1, src) + throws(classOf[RuntimeException])( + eval.eval(i.toString, tpeName = Some(BooleanType), line = line, srcName = src) + ) && + hasErrors(line + 1, src) } property("backed local class") = forAll { (i: Int) => @@ -40,8 +50,8 @@ object EvalTest extends Properties("eval") { val result = eval.eval(local(i)) val v = value(result).asInstanceOf[{ def i: Int }].i (label("Value", v) |: (v == i)) && - (label("Type", result.tpe) |: (result.tpe == LocalType)) && - (label("Files", result.generated) |: result.generated.nonEmpty) + (label("Type", result.tpe) |: (result.tpe == LocalType)) && + (label("Files", result.generated) |: result.generated.nonEmpty) } } @@ -62,35 +72,41 @@ val p = { property("val test") = secure { val defs = (ValTestContent, 1 to 7) :: Nil - val res = eval.evalDefinitions(defs, new EvalImports(Nil, ""), "", None, "scala.Int" :: Nil) + val res = + eval.evalDefinitions(defs, new EvalImports(Nil, ""), "", None, "scala.Int" :: Nil) label("Val names", res.valNames) |: (res.valNames.toSet == ValTestNames) } property("explicit import") = forAll(testImport("import math.abs" :: Nil)) property("wildcard import") = forAll(testImport("import math._" :: Nil)) - property("comma-separated imports") = forAll(testImport("import annotation._, math._, meta._" :: Nil)) - property("multiple imports") = forAll(testImport("import annotation._" :: "import math._" :: "import meta._" :: Nil)) + property("comma-separated imports") = forAll( + testImport("import annotation._, math._, meta._" :: Nil) + ) + property("multiple imports") = forAll( + testImport("import annotation._" :: "import math._" :: "import meta._" :: Nil) + ) - private[this] def testImport(imports: Seq[String]): Int => Prop = i => - value(eval.eval("abs(" + i + ")", new EvalImports(imports.zipWithIndex, "imp"))) == math.abs(i) + private[this] def testImport(imports: Seq[String]): Int => Prop = + i => + value(eval.eval("abs(" + i + ")", new EvalImports(imports.zipWithIndex, "imp"))) == math.abs( + i + ) private[this] def local(i: Int) = "{ class ETest(val i: Int); new ETest(" + i + ") }" val LocalType = "AnyRef{val i: Int}" private[this] def value(r: EvalResult) = r.getValue(getClass.getClassLoader) - private[this] def hasErrors(line: Int, src: String) = - { - val is = reporter.infos - ("Has errors" |: is.nonEmpty) && - all(is.toSeq.map(validPosition(line, src)): _*) - } - private[this] def validPosition(line: Int, src: String)(i: Info) = - { - val nme = i.pos.source.file.name - (label("Severity", i.severity) |: (i.severity == ERROR)) && - (label("Line", i.pos.line) |: (i.pos.line == line)) && - (label("Name", nme) |: (nme == src)) - } + private[this] def hasErrors(line: Int, src: String) = { + val is = reporter.infos + ("Has errors" |: is.nonEmpty) && + all(is.toSeq.map(validPosition(line, src)): _*) + } + private[this] def validPosition(line: Int, src: String)(i: StoreReporter.Info) = { + val nme = i.pos.source.file.name + (label("Severity", i.severity) |: (i.severity == ERROR)) && + (label("Line", i.pos.line) |: (i.pos.line == line)) && + (label("Name", nme) |: (nme == src)) + } val IntType = "Int" val BooleanType = "Boolean" diff --git a/main-command/src/main/contraband-scala/CommandSourceFormats.scala b/main-command/src/main/contraband-scala/CommandSourceFormats.scala new file mode 100644 index 000000000..77ef33283 --- /dev/null +++ b/main-command/src/main/contraband-scala/CommandSourceFormats.scala @@ -0,0 +1,26 @@ +/** + * This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +import _root_.sjsonnew.{ Unbuilder, Builder, JsonFormat, deserializationError } +trait CommandSourceFormats { self: sjsonnew.BasicJsonProtocol => +implicit lazy val CommandSourceFormat: JsonFormat[sbt.CommandSource] = new JsonFormat[sbt.CommandSource] { + override def read[J](__jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.CommandSource = { + __jsOpt match { + case Some(__js) => + unbuilder.beginObject(__js) + val channelName = unbuilder.readField[String]("channelName") + unbuilder.endObject() + sbt.CommandSource(channelName) + case None => + deserializationError("Expected JsObject but found None") + } + } + override def write[J](obj: sbt.CommandSource, builder: Builder[J]): Unit = { + builder.beginObject() + builder.addField("channelName", obj.channelName) + builder.endObject() + } +} +} diff --git a/main-command/src/main/contraband-scala/ConnectionTypeFormats.scala b/main-command/src/main/contraband-scala/ConnectionTypeFormats.scala new file mode 100644 index 000000000..08d115b45 --- /dev/null +++ b/main-command/src/main/contraband-scala/ConnectionTypeFormats.scala @@ -0,0 +1,28 @@ +/** + * This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +import _root_.sjsonnew.{ Unbuilder, Builder, JsonFormat, deserializationError } +trait ConnectionTypeFormats { self: sjsonnew.BasicJsonProtocol => +implicit lazy val ConnectionTypeFormat: JsonFormat[sbt.ConnectionType] = new JsonFormat[sbt.ConnectionType] { + override def read[J](__jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.ConnectionType = { + __jsOpt match { + case Some(__js) => + unbuilder.readString(__js) match { + case "Local" => sbt.ConnectionType.Local + case "Tcp" => sbt.ConnectionType.Tcp + } + case None => + deserializationError("Expected JsString but found None") + } + } + override def write[J](obj: sbt.ConnectionType, builder: Builder[J]): Unit = { + val str = obj match { + case sbt.ConnectionType.Local => "Local" + case sbt.ConnectionType.Tcp => "Tcp" + } + builder.writeString(str) + } +} +} diff --git a/main-command/src/main/contraband-scala/ExecFormats.scala b/main-command/src/main/contraband-scala/ExecFormats.scala new file mode 100644 index 000000000..c5fee86d5 --- /dev/null +++ b/main-command/src/main/contraband-scala/ExecFormats.scala @@ -0,0 +1,30 @@ +/** + * This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +import _root_.sjsonnew.{ Unbuilder, Builder, JsonFormat, deserializationError } +trait ExecFormats { self: CommandSourceFormats with sjsonnew.BasicJsonProtocol => +implicit lazy val ExecFormat: JsonFormat[sbt.Exec] = new JsonFormat[sbt.Exec] { + override def read[J](__jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.Exec = { + __jsOpt match { + case Some(__js) => + unbuilder.beginObject(__js) + val commandLine = unbuilder.readField[String]("commandLine") + val execId = unbuilder.readField[Option[String]]("execId") + val source = unbuilder.readField[Option[sbt.CommandSource]]("source") + unbuilder.endObject() + sbt.Exec(commandLine, execId, source) + case None => + deserializationError("Expected JsObject but found None") + } + } + override def write[J](obj: sbt.Exec, builder: Builder[J]): Unit = { + builder.beginObject() + builder.addField("commandLine", obj.commandLine) + builder.addField("execId", obj.execId) + builder.addField("source", obj.source) + builder.endObject() + } +} +} diff --git a/main-command/src/main/contraband-scala/ServerAuthenticationFormats.scala b/main-command/src/main/contraband-scala/ServerAuthenticationFormats.scala new file mode 100644 index 000000000..e05ca2bb1 --- /dev/null +++ b/main-command/src/main/contraband-scala/ServerAuthenticationFormats.scala @@ -0,0 +1,26 @@ +/** + * This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +import _root_.sjsonnew.{ Unbuilder, Builder, JsonFormat, deserializationError } +trait ServerAuthenticationFormats { self: sjsonnew.BasicJsonProtocol => +implicit lazy val ServerAuthenticationFormat: JsonFormat[sbt.ServerAuthentication] = new JsonFormat[sbt.ServerAuthentication] { + override def read[J](__jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.ServerAuthentication = { + __jsOpt match { + case Some(__js) => + unbuilder.readString(__js) match { + case "Token" => sbt.ServerAuthentication.Token + } + case None => + deserializationError("Expected JsString but found None") + } + } + override def write[J](obj: sbt.ServerAuthentication, builder: Builder[J]): Unit = { + val str = obj match { + case sbt.ServerAuthentication.Token => "Token" + } + builder.writeString(str) + } +} +} diff --git a/main-command/src/main/contraband-scala/sbt/CommandSource.scala b/main-command/src/main/contraband-scala/sbt/CommandSource.scala new file mode 100644 index 000000000..938fdbf7c --- /dev/null +++ b/main-command/src/main/contraband-scala/sbt/CommandSource.scala @@ -0,0 +1,32 @@ +/** + * This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt +final class CommandSource private ( + val channelName: String) extends Serializable { + + + + override def equals(o: Any): Boolean = this.eq(o.asInstanceOf[AnyRef]) || (o match { + case x: CommandSource => (this.channelName == x.channelName) + case _ => false + }) + override def hashCode: Int = { + 37 * (37 * (17 + "sbt.CommandSource".##) + channelName.##) + } + override def toString: String = { + "CommandSource(" + channelName + ")" + } + private[this] def copy(channelName: String = channelName): CommandSource = { + new CommandSource(channelName) + } + def withChannelName(channelName: String): CommandSource = { + copy(channelName = channelName) + } +} +object CommandSource { + + def apply(channelName: String): CommandSource = new CommandSource(channelName) +} diff --git a/main-command/src/main/contraband-scala/sbt/ConnectionType.scala b/main-command/src/main/contraband-scala/sbt/ConnectionType.scala new file mode 100644 index 000000000..10a0b05e0 --- /dev/null +++ b/main-command/src/main/contraband-scala/sbt/ConnectionType.scala @@ -0,0 +1,13 @@ +/** + * This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt +sealed abstract class ConnectionType extends Serializable +object ConnectionType { + + /** This uses Unix domain socket on POSIX, and named pipe on Windows. */ + case object Local extends ConnectionType + case object Tcp extends ConnectionType +} diff --git a/main-command/src/main/contraband-scala/sbt/Exec.scala b/main-command/src/main/contraband-scala/sbt/Exec.scala new file mode 100644 index 000000000..b37684818 --- /dev/null +++ b/main-command/src/main/contraband-scala/sbt/Exec.scala @@ -0,0 +1,49 @@ +/** + * This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt +final class Exec private ( + val commandLine: String, + val execId: Option[String], + val source: Option[sbt.CommandSource]) extends Serializable { + + private def this(commandLine: String, source: Option[sbt.CommandSource]) = this(commandLine, None, source) + + override def equals(o: Any): Boolean = this.eq(o.asInstanceOf[AnyRef]) || (o match { + case x: Exec => (this.commandLine == x.commandLine) && (this.execId == x.execId) && (this.source == x.source) + case _ => false + }) + override def hashCode: Int = { + 37 * (37 * (37 * (37 * (17 + "sbt.Exec".##) + commandLine.##) + execId.##) + source.##) + } + override def toString: String = { + "Exec(" + commandLine + ", " + execId + ", " + source + ")" + } + private[this] def copy(commandLine: String = commandLine, execId: Option[String] = execId, source: Option[sbt.CommandSource] = source): Exec = { + new Exec(commandLine, execId, source) + } + def withCommandLine(commandLine: String): Exec = { + copy(commandLine = commandLine) + } + def withExecId(execId: Option[String]): Exec = { + copy(execId = execId) + } + def withExecId(execId: String): Exec = { + copy(execId = Option(execId)) + } + def withSource(source: Option[sbt.CommandSource]): Exec = { + copy(source = source) + } + def withSource(source: sbt.CommandSource): Exec = { + copy(source = Option(source)) + } +} +object Exec { + def newExecId: String = java.util.UUID.randomUUID.toString + def apply(commandLine: String, source: Option[sbt.CommandSource]): Exec = new Exec(commandLine, source) + def apply(commandLine: String, source: sbt.CommandSource): Exec = new Exec(commandLine, Option(source)) + def apply(commandLine: String, execId: Option[String], source: Option[sbt.CommandSource]): Exec = new Exec(commandLine, execId, source) + def apply(commandLine: String, execId: String, source: sbt.CommandSource): Exec = new Exec(commandLine, Option(execId), Option(source)) +} diff --git a/main-command/src/main/contraband-scala/sbt/ServerAuthentication.scala b/main-command/src/main/contraband-scala/sbt/ServerAuthentication.scala new file mode 100644 index 000000000..5744c9814 --- /dev/null +++ b/main-command/src/main/contraband-scala/sbt/ServerAuthentication.scala @@ -0,0 +1,12 @@ +/** + * This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt +sealed abstract class ServerAuthentication extends Serializable +object ServerAuthentication { + + + case object Token extends ServerAuthentication +} diff --git a/main-command/src/main/contraband/state.contra b/main-command/src/main/contraband/state.contra new file mode 100644 index 000000000..2737ce8ab --- /dev/null +++ b/main-command/src/main/contraband/state.contra @@ -0,0 +1,25 @@ +package sbt +@target(Scala) + +type Exec { + commandLine: String! + execId: String @since("0.0.1") + source: sbt.CommandSource + + #xcompanion def newExecId: String = java.util.UUID.randomUUID.toString +} + +type CommandSource { + channelName: String! +} + +enum ServerAuthentication { + Token +} + +enum ConnectionType { + ## This uses Unix domain socket on POSIX, and named pipe on Windows. + Local + Tcp + # Ssh +} diff --git a/main-command/src/main/datatype-scala/sbt/internal/server/CommandMessage.scala b/main-command/src/main/datatype-scala/sbt/internal/server/CommandMessage.scala deleted file mode 100644 index 889f8fe7a..000000000 --- a/main-command/src/main/datatype-scala/sbt/internal/server/CommandMessage.scala +++ /dev/null @@ -1,39 +0,0 @@ -/** - * This code is generated using sbt-datatype. - */ - -// DO NOT EDIT MANUALLY -package sbt.internal.server -final class CommandMessage( - val `type`: String, - val commandLine: Option[String]) extends Serializable { - - def this(`type`: String) = this(`type`, None) - - override def equals(o: Any): Boolean = o match { - case x: CommandMessage => (this.`type` == x.`type`) && (this.commandLine == x.commandLine) - case _ => false - } - override def hashCode: Int = { - 37 * (37 * (17 + `type`.##) + commandLine.##) - } - override def toString: String = { - "CommandMessage(" + `type` + ", " + commandLine + ")" - } - def copy(`type`: String): CommandMessage = { - new CommandMessage(`type`, commandLine) - } - def copy(`type`: String = `type`, commandLine: Option[String] = commandLine): CommandMessage = { - new CommandMessage(`type`, commandLine) - } - def withType(`type`: String): CommandMessage = { - copy(`type` = `type`) - } - def withCommandLine(commandLine: Option[String]): CommandMessage = { - copy(commandLine = commandLine) - } -} -object CommandMessage { - def apply(`type`: String): CommandMessage = new CommandMessage(`type`, None) - def apply(`type`: String, commandLine: Option[String]): CommandMessage = new CommandMessage(`type`, commandLine) -} diff --git a/main-command/src/main/datatype-scala/sbt/internal/server/EventMessage.scala b/main-command/src/main/datatype-scala/sbt/internal/server/EventMessage.scala deleted file mode 100644 index 55946b0e7..000000000 --- a/main-command/src/main/datatype-scala/sbt/internal/server/EventMessage.scala +++ /dev/null @@ -1,59 +0,0 @@ -/** - * This code is generated using sbt-datatype. - */ - -// DO NOT EDIT MANUALLY -package sbt.internal.server -final class EventMessage( - val `type`: String, - val status: Option[String], - val commandQueue: Vector[String], - val level: Option[String], - val message: Option[String], - val success: Option[Boolean], - val commandLine: Option[String]) extends Serializable { - - def this(`type`: String) = this(`type`, None, Vector(), None, None, None, None) - - override def equals(o: Any): Boolean = o match { - case x: EventMessage => (this.`type` == x.`type`) && (this.status == x.status) && (this.commandQueue == x.commandQueue) && (this.level == x.level) && (this.message == x.message) && (this.success == x.success) && (this.commandLine == x.commandLine) - case _ => false - } - override def hashCode: Int = { - 37 * (37 * (37 * (37 * (37 * (37 * (37 * (17 + `type`.##) + status.##) + commandQueue.##) + level.##) + message.##) + success.##) + commandLine.##) - } - override def toString: String = { - "EventMessage(" + `type` + ", " + status + ", " + commandQueue + ", " + level + ", " + message + ", " + success + ", " + commandLine + ")" - } - def copy(`type`: String): EventMessage = { - new EventMessage(`type`, status, commandQueue, level, message, success, commandLine) - } - def copy(`type`: String = `type`, status: Option[String] = status, commandQueue: Vector[String] = commandQueue, level: Option[String] = level, message: Option[String] = message, success: Option[Boolean] = success, commandLine: Option[String] = commandLine): EventMessage = { - new EventMessage(`type`, status, commandQueue, level, message, success, commandLine) - } - def withType(`type`: String): EventMessage = { - copy(`type` = `type`) - } - def withStatus(status: Option[String]): EventMessage = { - copy(status = status) - } - def withCommandQueue(commandQueue: Vector[String]): EventMessage = { - copy(commandQueue = commandQueue) - } - def withLevel(level: Option[String]): EventMessage = { - copy(level = level) - } - def withMessage(message: Option[String]): EventMessage = { - copy(message = message) - } - def withSuccess(success: Option[Boolean]): EventMessage = { - copy(success = success) - } - def withCommandLine(commandLine: Option[String]): EventMessage = { - copy(commandLine = commandLine) - } -} -object EventMessage { - def apply(`type`: String): EventMessage = new EventMessage(`type`, None, Vector(), None, None, None, None) - def apply(`type`: String, status: Option[String], commandQueue: Vector[String], level: Option[String], message: Option[String], success: Option[Boolean], commandLine: Option[String]): EventMessage = new EventMessage(`type`, status, commandQueue, level, message, success, commandLine) -} diff --git a/main-command/src/main/datatype-scala/sbt/internal/server/codec/CommandMessageFormats.scala b/main-command/src/main/datatype-scala/sbt/internal/server/codec/CommandMessageFormats.scala deleted file mode 100644 index 17130c90e..000000000 --- a/main-command/src/main/datatype-scala/sbt/internal/server/codec/CommandMessageFormats.scala +++ /dev/null @@ -1,29 +0,0 @@ -/** - * This code is generated using sbt-datatype. - */ - -// DO NOT EDIT MANUALLY -package sbt.internal.server.codec -import _root_.sjsonnew.{ deserializationError, serializationError, Builder, JsonFormat, Unbuilder } -trait CommandMessageFormats { self: sjsonnew.BasicJsonProtocol => -implicit lazy val CommandMessageFormat: JsonFormat[sbt.internal.server.CommandMessage] = new JsonFormat[sbt.internal.server.CommandMessage] { - override def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.internal.server.CommandMessage = { - jsOpt match { - case Some(js) => - unbuilder.beginObject(js) - val `type` = unbuilder.readField[String]("type") - val commandLine = unbuilder.readField[Option[String]]("commandLine") - unbuilder.endObject() - new sbt.internal.server.CommandMessage(`type`, commandLine) - case None => - deserializationError("Expected JsObject but found None") - } - } - override def write[J](obj: sbt.internal.server.CommandMessage, builder: Builder[J]): Unit = { - builder.beginObject() - builder.addField("type", obj.`type`) - builder.addField("commandLine", obj.commandLine) - builder.endObject() - } -} -} diff --git a/main-command/src/main/datatype-scala/sbt/internal/server/codec/EventMessageFormats.scala b/main-command/src/main/datatype-scala/sbt/internal/server/codec/EventMessageFormats.scala deleted file mode 100644 index 4ce95d9ef..000000000 --- a/main-command/src/main/datatype-scala/sbt/internal/server/codec/EventMessageFormats.scala +++ /dev/null @@ -1,39 +0,0 @@ -/** - * This code is generated using sbt-datatype. - */ - -// DO NOT EDIT MANUALLY -package sbt.internal.server.codec -import _root_.sjsonnew.{ deserializationError, serializationError, Builder, JsonFormat, Unbuilder } -trait EventMessageFormats { self: sjsonnew.BasicJsonProtocol => -implicit lazy val EventMessageFormat: JsonFormat[sbt.internal.server.EventMessage] = new JsonFormat[sbt.internal.server.EventMessage] { - override def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.internal.server.EventMessage = { - jsOpt match { - case Some(js) => - unbuilder.beginObject(js) - val `type` = unbuilder.readField[String]("type") - val status = unbuilder.readField[Option[String]]("status") - val commandQueue = unbuilder.readField[Vector[String]]("commandQueue") - val level = unbuilder.readField[Option[String]]("level") - val message = unbuilder.readField[Option[String]]("message") - val success = unbuilder.readField[Option[Boolean]]("success") - val commandLine = unbuilder.readField[Option[String]]("commandLine") - unbuilder.endObject() - new sbt.internal.server.EventMessage(`type`, status, commandQueue, level, message, success, commandLine) - case None => - deserializationError("Expected JsObject but found None") - } - } - override def write[J](obj: sbt.internal.server.EventMessage, builder: Builder[J]): Unit = { - builder.beginObject() - builder.addField("type", obj.`type`) - builder.addField("status", obj.status) - builder.addField("commandQueue", obj.commandQueue) - builder.addField("level", obj.level) - builder.addField("message", obj.message) - builder.addField("success", obj.success) - builder.addField("commandLine", obj.commandLine) - builder.endObject() - } -} -} diff --git a/main-command/src/main/datatype/server.json b/main-command/src/main/datatype/server.json deleted file mode 100644 index 3dcc4af17..000000000 --- a/main-command/src/main/datatype/server.json +++ /dev/null @@ -1,73 +0,0 @@ -{ - "codecNamespace": "sbt.internal.server.codec", - "types": [ - { - "name": "CommandMessage", - "namespace": "sbt.internal.server", - "type": "record", - "target": "Scala", - "fields": [ - { - "name": "type", - "type": "String", - "since": "0.0.0" - }, - { - "name": "commandLine", - "type": "String?", - "default": "None", - "since": "0.1.0" - } - ] - }, - { - "name": "EventMessage", - "namespace": "sbt.internal.server", - "type": "record", - "target": "Scala", - "fields": [ - { - "name": "type", - "type": "String", - "since": "0.0.0" - }, - { - "name": "status", - "type": "String?", - "default": "None", - "since": "0.1.0" - }, - { - "name": "commandQueue", - "type": "String*", - "default": "Vector()", - "since": "0.1.0" - }, - { - "name": "level", - "type": "String?", - "default": "None", - "since": "0.1.0" - }, - { - "name": "message", - "type": "String?", - "default": "None", - "since": "0.1.0" - }, - { - "name": "success", - "type": "boolean?", - "default": "None", - "since": "0.1.0" - }, - { - "name": "commandLine", - "type": "String?", - "default": "None", - "since": "0.1.0" - } - ] - } - ] -} diff --git a/main-command/src/main/java/sbt/internal/BootServerSocket.java b/main-command/src/main/java/sbt/internal/BootServerSocket.java new file mode 100644 index 000000000..beff97566 --- /dev/null +++ b/main-command/src/main/java/sbt/internal/BootServerSocket.java @@ -0,0 +1,360 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.Socket; +import java.net.ServerSocket; +import java.net.SocketException; +import java.net.SocketTimeoutException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import net.openhft.hashing.LongHashFunction; +import org.scalasbt.ipcsocket.UnixDomainServerSocket; +import org.scalasbt.ipcsocket.Win32NamedPipeServerSocket; +import org.scalasbt.ipcsocket.Win32NamedPipeSocket; +import org.scalasbt.ipcsocket.Win32SecurityLevel; +import sbt.internal.util.Terminal; +import xsbti.AppConfiguration; + +/** + * A BootServerSocket is used for remote clients to connect to sbt for io while sbt is still loading + * the build. There are two scenarios in which this functionality is needed: + * + *

1. client a starts an sbt server and then client b tries to connect to the server before the + * server has loaded. Presently, client b will try to start a new server even though there is one + * booting. This can cause a java process leak because the second server launched by client b is + * unable to create a server because there is an existing portfile by the time it starts up. + * + *

2. a remote client initiates a reboot command. Reboot causes sbt to shutdown the server which + * makes the client disconnect. Since sbt does not start the server until the project has + * successfully loaded, there is no way for the client to see the output of the server. This is + * particularly problematic if loading fails because the server will be stuck waiting for input that + * will not be forthcoming. + * + *

To address these issues, the BootServerSocket can be used to immediately create a server + * socket before sbt even starts loading the build. It works by creating a local socket either in + * project/target/SOCK_NAME or a windows named pipe with name SOCK_NAME where SOCK_NAME is computed + * as the hash of the project's base directory (for disambiguation in the windows case). If the + * server can't create a server socket because there is already one running, it either prompts the + * user if they want to start a new server even if there is already one running if there is a + * console available or exits with the status code 2 which indicates that there is another sbt + * process starting up. + * + *

Once the server socket is created, it listens for new client connections. When a client + * connects, the server will forward its input and output to the client via Terminal.setBootStreams + * which updates the Terminal.proxyOutputStream to forward all bytes written to the + * BootServerSocket's outputStream which in turn writes the output to each of the connected clients. + * Input is handed similarly. + * + *

When the server finishes loading, it closes the boot server socket. + * + *

BootServerSocket is implemented in java so that it can be classloaded as quickly as possible. + */ +public class BootServerSocket implements AutoCloseable { + private ServerSocket serverSocket = null; + private final AtomicBoolean closed = new AtomicBoolean(false); + private final AtomicBoolean running = new AtomicBoolean(false); + private final AtomicInteger threadId = new AtomicInteger(1); + private final Future acceptFuture; + private final ExecutorService service = + Executors.newCachedThreadPool( + r -> new Thread(r, "boot-server-socket-thread-" + threadId.getAndIncrement())); + private final Set clientSockets = ConcurrentHashMap.newKeySet(); + private final Object lock = new Object(); + private final LinkedBlockingQueue clientSocketReads = new LinkedBlockingQueue<>(); + private final Path socketFile; + private final AtomicBoolean needInput = new AtomicBoolean(false); + + private class ClientSocket implements AutoCloseable { + final Socket socket; + final AtomicBoolean alive = new AtomicBoolean(true); + final Future future; + private final LinkedBlockingQueue bytes = new LinkedBlockingQueue(); + private final AtomicBoolean closed = new AtomicBoolean(false); + + @SuppressWarnings("deprecation") + ClientSocket(final Socket socket) { + this.socket = socket; + clientSockets.add(this); + Future f = null; + try { + f = + service.submit( + () -> { + try { + Terminal.console() + .getLines() + .foreach( + l -> { + try { + write((l + System.lineSeparator()).getBytes("UTF-8")); + } catch (final IOException e) { + } + return 0; + }); + final InputStream inputStream = socket.getInputStream(); + while (alive.get()) { + try { + synchronized (needInput) { + while (!needInput.get() && alive.get()) needInput.wait(); + } + if (alive.get()) { + socket.getOutputStream().write(5); + int b = inputStream.read(); + if (b != -1) { + bytes.put(b); + clientSocketReads.put(ClientSocket.this); + } else { + alive.set(false); + } + } + + } catch (IOException e) { + alive.set(false); + } + } + } catch (final Exception ex) { + } + }); + } catch (final RejectedExecutionException e) { + alive.set(false); + } + future = f; + } + + private void write(final int i) { + try { + if (alive.get()) socket.getOutputStream().write(i); + } catch (final IOException e) { + alive.set(false); + close(); + } + } + + private void write(final byte[] b) { + try { + if (alive.get()) socket.getOutputStream().write(b); + } catch (final IOException e) { + alive.set(false); + close(); + } + } + + private void write(final byte[] b, final int offset, final int len) { + try { + if (alive.get()) socket.getOutputStream().write(b, offset, len); + } catch (final IOException e) { + alive.set(false); + close(); + } + } + + private void flush() { + try { + socket.getOutputStream().flush(); + } catch (final IOException e) { + alive.set(false); + close(); + } + } + + @SuppressWarnings("EmptyCatchBlock") + @Override + public void close() { + if (closed.compareAndSet(false, true)) { + if (alive.get()) { + write(2); + bytes.forEach(this::write); + bytes.clear(); + write(3); + flush(); + } + alive.set(false); + if (future != null) future.cancel(true); + try { + socket.getOutputStream().close(); + socket.getInputStream().close(); + // Windows is very slow to close the socket for whatever reason + // We close the server socket anyway, so this should die then. + if (!System.getProperty("os.name", "").toLowerCase().startsWith("win")) socket.close(); + } catch (final IOException e) { + } + clientSockets.remove(this); + } + } + } + + private final Object writeLock = new Object(); + + public InputStream inputStream() { + return inputStream; + } + + private final InputStream inputStream = + new InputStream() { + @Override + public int read() { + if (clientSockets.isEmpty()) return Terminal.NO_BOOT_CLIENTS_CONNECTED(); + try { + synchronized (needInput) { + needInput.set(true); + needInput.notifyAll(); + } + ClientSocket clientSocket = clientSocketReads.take(); + return clientSocket.bytes.take(); + } catch (final InterruptedException e) { + return -1; + } finally { + synchronized (needInput) { + needInput.set(false); + } + } + } + }; + private final OutputStream outputStream = + new OutputStream() { + @Override + public void write(final int b) { + synchronized (lock) { + clientSockets.forEach(cs -> cs.write(b)); + } + } + + @Override + public void write(final byte[] b) { + write(b, 0, b.length); + } + + @Override + public void write(final byte[] b, final int offset, final int len) { + synchronized (lock) { + clientSockets.forEach(cs -> cs.write(b, offset, len)); + } + } + + @Override + public void flush() { + synchronized (lock) { + clientSockets.forEach(cs -> cs.flush()); + } + } + }; + + public OutputStream outputStream() { + return outputStream; + } + + private final Runnable acceptRunnable = + () -> { + try { + serverSocket.setSoTimeout(5000); + while (running.get()) { + try { + ClientSocket clientSocket = new ClientSocket(serverSocket.accept()); + } catch (final SocketTimeoutException e) { + } catch (final IOException e) { + running.set(false); + } + } + } catch (final SocketException e) { + } + }; + + public BootServerSocket(final AppConfiguration configuration) + throws ServerAlreadyBootingException, IOException { + final Path base = configuration.baseDirectory().toPath().toRealPath(); + final Path target = base.resolve("project").resolve("target"); + if (!isWindows) { + if (!Files.isDirectory(target)) Files.createDirectories(target); + socketFile = Paths.get(socketLocation(base)); + } else { + socketFile = null; + } + serverSocket = newSocket(socketLocation(base)); + if (serverSocket != null) { + running.set(true); + acceptFuture = service.submit(acceptRunnable); + } else { + closed.set(true); + acceptFuture = null; + } + } + + public static String socketLocation(final Path base) throws UnsupportedEncodingException { + final Path target = base.resolve("project").resolve("target"); + if (isWindows) { + long hash = LongHashFunction.farmNa().hashBytes(target.toString().getBytes("UTF-8")); + return "sbt-load" + hash; + } else { + return base.relativize(target.resolve("sbt-load.sock")).toString(); + } + } + + @SuppressWarnings("EmptyCatchBlock") + @Override + public void close() { + if (closed.compareAndSet(false, true)) { + // avoid concurrent modification exception + clientSockets.forEach(ClientSocket::close); + if (acceptFuture != null) acceptFuture.cancel(true); + service.shutdownNow(); + try { + if (serverSocket != null) serverSocket.close(); + } catch (final IOException e) { + } + try { + if (socketFile != null) Files.deleteIfExists(socketFile); + } catch (final IOException e) { + } + } + } + + static final boolean isWindows = + System.getProperty("os.name", "").toLowerCase().startsWith("win"); + + static ServerSocket newSocket(final String sock) throws ServerAlreadyBootingException { + ServerSocket socket = null; + String name = socketName(sock); + boolean jni = requiresJNI() || System.getProperty("sbt.ipcsocket.jni", "false").equals("true"); + try { + if (!isWindows) Files.deleteIfExists(Paths.get(sock)); + socket = + isWindows + ? new Win32NamedPipeServerSocket(name, jni, Win32SecurityLevel.OWNER_DACL) + : new UnixDomainServerSocket(name, jni); + return socket; + } catch (final IOException e) { + throw new ServerAlreadyBootingException(e); + } + } + + public static Boolean requiresJNI() { + final boolean isMac = System.getProperty("os.name").toLowerCase().startsWith("mac"); + return isMac && !System.getProperty("os.arch", "").equals("x86_64"); + } + + private static String socketName(String sock) { + return isWindows ? "\\\\.\\pipe\\" + sock : sock; + } +} diff --git a/main-command/src/main/java/sbt/internal/ServerAlreadyBootingException.java b/main-command/src/main/java/sbt/internal/ServerAlreadyBootingException.java new file mode 100644 index 000000000..90a16fdff --- /dev/null +++ b/main-command/src/main/java/sbt/internal/ServerAlreadyBootingException.java @@ -0,0 +1,17 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal; + +import java.io.IOException; + +public class ServerAlreadyBootingException extends Exception { + + public ServerAlreadyBootingException(IOException e) { + super(e); + } +} diff --git a/main-command/src/main/java/sbt/internal/classpath/WrappedLoader.java b/main-command/src/main/java/sbt/internal/classpath/WrappedLoader.java new file mode 100644 index 000000000..76942d046 --- /dev/null +++ b/main-command/src/main/java/sbt/internal/classpath/WrappedLoader.java @@ -0,0 +1,45 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.classpath; + +import java.net.URL; +import java.net.URLClassLoader; +import java.util.concurrent.atomic.AtomicBoolean; + +public class WrappedLoader extends URLClassLoader { + private final AtomicBoolean invalidated = new AtomicBoolean(false); + + static { + ClassLoader.registerAsParallelCapable(); + } + + WrappedLoader(final ClassLoader parent) { + super(new URL[] {}, parent); + } + + void invalidate() { + invalidated.set(true); + } + + boolean invalidated() { + return invalidated.get(); + } + + @Override + public URL[] getURLs() { + final ClassLoader parent = getParent(); + return (parent instanceof URLClassLoader) + ? ((URLClassLoader) parent).getURLs() + : super.getURLs(); + } + + @Override + public String toString() { + return "WrappedClassLoader(" + getParent() + ")"; + } +} diff --git a/main-command/src/main/scala/sbt/BasicCommandStrings.scala b/main-command/src/main/scala/sbt/BasicCommandStrings.scala index bfd424a65..d7a310929 100644 --- a/main-command/src/main/scala/sbt/BasicCommandStrings.scala +++ b/main-command/src/main/scala/sbt/BasicCommandStrings.scala @@ -1,50 +1,76 @@ -/* sbt -- Simple Build Tool - * Copyright 2010 Mark Harrah +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) */ + package sbt import sbt.util.Level import sbt.internal.util.complete.HistoryCommands object BasicCommandStrings { - val HelpCommand = "help" - val CompletionsCommand = "completions" - val Exit = "exit" - val Quit = "quit" + val HelpCommand: String = "help" + val CompletionsCommand: String = "completions" + val Exit: String = "exit" + val Shutdown: String = "shutdown" + val Quit: String = "quit" + val TemplateCommand: String = "new" + val Cancel: String = "cancel" /** The command name to terminate the program.*/ val TerminateAction: String = Exit - def helpBrief = (HelpCommand, s"Displays this help message or prints detailed help on requested commands (run '$HelpCommand ').") - def helpDetailed = HelpCommand + """ + def helpBrief: (String, String) = + ( + HelpCommand, + s"Displays this help message or prints detailed help on requested commands (run '$HelpCommand ')." + ) + def helpDetailed: String = s"""$HelpCommand Prints a help summary. -""" + HelpCommand + """ +$HelpCommand Prints detailed help for command . -""" + HelpCommand + """ +$HelpCommand Searches the help according to the provided regular expression. """ - def CompletionsDetailed = "Displays a list of completions for the given argument string (run 'completions ')." - def CompletionsBrief = (CompletionsCommand, CompletionsDetailed) + def CompletionsDetailed: String = + "Displays a list of completions for the given argument string (run 'completions ')." + def CompletionsBrief: (String, String) = (CompletionsCommand, CompletionsDetailed) - def HistoryHelpBrief = (HistoryCommands.Start -> "History command help. Lists and describes all history commands.") - def historyHelp = Help(Nil, (HistoryHelpBrief +: HistoryCommands.descriptions).toMap, Set(HistoryCommands.Start)) + def templateBrief: (String, String) = (TemplateCommand, "Creates a new sbt build.") + def templateDetailed: String = + TemplateCommand + """ [--options]