diff --git a/.gitattributes b/.gitattributes index d6a4d6db0..101127d2b 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,6 +1,7 @@ # https://techblog.dorogin.com/case-of-windows-line-ending-in-bash-script-7236f056abe * text=auto *.sh text eol=lf +*.bat text eol=crlf # Use linux file endings for kts scripts because some of them contain shebangs and are thus partially interpreted by bash @@ -11,4 +12,3 @@ kscript text eol=lf gradlew text eol=lf mydsl text eol=lf - diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4326841ba..437d9bd46 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -14,71 +14,98 @@ jobs: matrix: os: - ubuntu-20.04 - - macos-10.15 + - macos-12 - windows-2022 + variant: + - posix + include: + - os: windows-2022 + variant: cygwin + - os: windows-2022 + variant: cmd runs-on: ${{ matrix.os }} - timeout-minutes: 30 + timeout-minutes: 60 steps: - - uses: actions/checkout@v2 - - uses: actions/setup-java@v3 + - name: Checkout code + uses: actions/checkout@v2 + + - name: Setup Java + uses: actions/setup-java@v3 with: distribution: 'zulu' java-version: '11' - - uses: fwilhe2/setup-kotlin@main + + - name: Setup Kotlin + uses: fwilhe2/setup-kotlin@main with: version: 1.6.21 + - name: Setup Gradle + uses: gradle/gradle-build-action@v2 + with: + gradle-version: wrapper + - name: Install dependencies for ${{ runner.os }} shell: bash run: | if [ "$RUNNER_OS" == "Windows" ]; then choco install zip # Overwrite the WSL bash.exe - cp /c/msys64/usr/bin/bash.exe /c/Windows/System32/bash.exe + # cp /c/msys64/usr/bin/bash.exe /c/Windows/System32/bash.exe + mv /c/Windows/System32/bash.exe /c/Windows/System32/wsl-bash.exe fi - - name: Run tests - timeout-minutes: 10 + - name: Run tests for Posix (and MSYS on Windows) + if: matrix.variant == 'posix' shell: bash run: | + # For Windows this action is running MSYS Os type + echo "OsType: $OSTYPE" - export PATH=./build/libs:${PATH} - if [[ "$OSTYPE" == "linux-gnu"* ]]; then + gradle clean assemble test || { echo 'Compilation or Unit tests failed' ; exit 1; } + + if [[ "$OSTYPE" == "linux"* ]]; then echo "Linux test..." - ./test/test_suite.sh + gradle -DosType=$OSTYPE -DincludeTags='posix | linux' integration elif [[ "$OSTYPE" == "darwin"* ]]; then echo "MacOs test..." - ./gradlew clean build - echo "kscript path: $(which kscript)" - kscript --help - kscript -d "println(1+1)" + gradle -DosType=$OSTYPE -DincludeTags='posix | macos' integration elif [[ "$OSTYPE" == "cygwin" ]]; then echo "Cygwin test..." - ./gradlew clean build - echo "kscript path: $(which kscript)" - kscript --help - kscript -d "println(1+1)" + gradle -DosType=$OSTYPE -DincludeTags='posix | cygwin' integration elif [[ "$OSTYPE" == "msys" ]]; then echo "MSys test..." - ./gradlew clean build - echo "kscript path: $(which kscript)" - kscript --help - kscript -d "println(1+1)" - elif [[ "$OSTYPE" == "win32" ]]; then - echo "Windows test..." - ./gradlew clean build - echo "kscript path: $(which kscript)" - kscript --help - kscript -d "println(1+1)" + gradle -DosType=$OSTYPE -DincludeTags='posix | msys' integration elif [[ "$OSTYPE" == "freebsd"* ]]; then echo "FreeBsd test..." - ./gradlew clean build - echo "kscript path: $(which kscript)" - kscript --help - kscript -d "println(1+1)" + gradle -DosType=$OSTYPE -DincludeTags='posix' integration else echo "Unknown OS" exit 1 fi + + - name: Run tests specific for Windows (cmd shell) + if: matrix.variant == 'cmd' + shell: cmd + run: | + echo "Windows test..." + gradle clean assemble test + if %errorlevel% neq 0 exit /b %errorlevel% + gradle -DosType=windows -DincludeTags="windows" integration + + - name: Install Cygwin (only Windows) + if: matrix.variant == 'cygwin' + uses: egor-tensin/setup-cygwin@v3 + + - name: Run tests specific for Cygwin + if: matrix.variant == 'cygwin' + shell: C:\tools\cygwin\bin\bash.exe --login --norc -eo pipefail -o igncr '{0}' + run: | + echo $OSTYPE + echo "Cygwin test..." + echo "Changing directory to $GITHUB_WORKSPACE ..." + cd $GITHUB_WORKSPACE + gradle clean assemble test || { echo 'Compilation or Unit tests failed' ; exit 1; } + gradle -DosType=$OSTYPE -DincludeTags='posix | cygwin' integration diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 326719044..8487814d4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,5 +1,7 @@ # Created with https://github.com/marketplace/actions/create-a-release +name: release + on: push: # Sequence of patterns matched against refs/tags @@ -8,12 +10,9 @@ on: tags: - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 -name: Create Release - jobs: build: - name: Create Release runs-on: ubuntu-latest steps: - name: Checkout code @@ -38,4 +37,4 @@ jobs: body: | See [CHANGES.md](https://github.com/holgerbrandl/kscript/blob/master/NEWS.md) for new features, bug-fixes and changes. draft: false - prerelease: false \ No newline at end of file + prerelease: false diff --git a/NEWS.md b/NEWS.md index fd706fa95..b207072fa 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,6 +1,6 @@ # Changes -## 4.1.0 - TO BE RELEASED +## 4.1.0 Breaking changes @@ -9,14 +9,30 @@ Breaking changes KSCRIPT_IDEA_COMMAND -> KSCRIPT_COMMAND_IDEA KSCRIPT_GRADLE_COMMAND -> KSCRIPT_COMMAND_GRADLE -Major Enhancements - -* Initial Windows support -* Fix for resolution of dependencies - -Minor Enhancements - -## 4.0.2 +Enhancements + +* Windows support and proper Cygwin and MSys support +* File argument for specific OS should be in format of that OS (eg. Cygwin: kscript /cygdrive/c/file.kts) +* Multiplatform tests for different OS-es using Github actions +* Ability to use configuration file for kscript (thanks to [meztihn](https://github.com/meztihn)) +* kscript follows XDG Spec (Issue #323) (thanks to [meztihn](https://github.com/meztihn)) +* Packaging scripts works again (thanks to [Vsajip](https://github.com/vsajip)) +* When creating IntelliJ project 'gradle' and 'idea' do not have to be in path +* Integration tests rewritten from bash to JUnit +* Replacements for (current annotations are deprecated): + * @MavenRepository -> @Repository + * @KotlinOpts -> @KotlinOptions + * @CompilerOpts -> @CompilerOptions +* Deprecation of comment based annotations +* Report for deprecated features (--report option) + +Bugfixes +* Fix for dependency resolution +* Fix for creation of Gradle files and their indentation +* Fix for handling potentially duplicated file names in Idea projects +* Fix for Idea runtime configuration + +## 4.0.x (last 4.0.3) Released 2022-05-18 diff --git a/README.md b/README.md index 93c625823..13ad1cfb1 100644 --- a/README.md +++ b/README.md @@ -6,14 +6,13 @@ Enhanced scripting support for [Kotlin](https://kotlinlang.org/) on *nix-based systems. -Kotlin has some built-in support for scripting already but it is not yet feature-rich enough to be a viable alternative +Kotlin has some built-in support for scripting already, but it is not yet feature-rich enough to be a viable alternative in the shell. In particular this wrapper around `kotlinc` adds * Compiled script caching (using md5 checksums) * Dependency declarations using gradle-style resource locators and automatic dependency resolution - with [jcabi-aether](https://github.com/jcabi/jcabi-aether) * More options to provide scripts including interpreter mode, reading from stdin, local files or URLs * Embedded configuration for Kotlin runtime options * Support library to ease the writing of Kotlin scriptlets @@ -44,6 +43,7 @@ kotlin scripting interpreter. - [Boostrap IDEA from a `kscript`let](#boostrap-idea-from-a-kscriptlet) - [Deploy scripts as standalone binaries](#deploy-scripts-as-standalone-binaries) - [Embed kscript installer within your script](#embed-kscript-installer-within-your-script) +- [KScript configuration file](#kscript-configuration-file) - [FAQ](#faq) - [Support](#support) - [How to contribute?](#how-to-contribute) @@ -52,7 +52,7 @@ kotlin scripting interpreter. Installation ------------ -To use `kscript` just Kotlin and Maven are required. +To use `kscript` just Kotlin is required. To [install Kotlin](https://kotlinlang.org/docs/tutorials/command-line.html) we recommend [sdkman](http://sdkman.io/install): @@ -89,7 +89,7 @@ docker run -i holgerbrandl/kscript 'println("Hello, world!")' docker run -i holgerbrandl/kscript:2.9.3 'println("Hello, world!")' ``` -To use a script file outside of the container as input, you could do +To use a script file outside the container as input, you could do ```bash docker run -i holgerbrandl/kscript - < script.kts @@ -104,9 +104,8 @@ instance [bind mounts](https://docs.docker.com/storage/bind-mounts/). #### Installation without `sdkman` -If you have Kotlin and Maven already and you would like to install the latest `kscript` release without using `sdkman` -you can do so by unzipping the [latest ](https://github.com/holgerbrandl/kscript/releases/latest) binary release. Don't -forget to update your `$PATH` accordingly. +If you have Kotlin already, and you would like to install the latest `kscript` release without using `sdkman` +you can do so by unzipping the [latest ](https://github.com/holgerbrandl/kscript/releases/latest) binary release. Don't forget to update your `$PATH` accordingly. #### Installation with Homebrew @@ -196,7 +195,7 @@ EOF ```{bash} kscript - <<"EOF" -//DEPS com.offbytwo:docopt:0.6.0.20150202 log4j:log4j:1.2.14 +@file:DependsOn("com.offbytwo:docopt:0.6.0.20150202", "log4j:log4j:1.2.14") import org.docopt.Docopt val docopt = Docopt("Usage: jl [options] []") @@ -205,7 +204,7 @@ println("hello again") EOF ``` -* Finally (for sake of completeness), it also works with process substitution and for sure you can always provide +* Finally, (for sake of completeness), it also works with process substitution and for sure you can always provide additional arguments (exposed as `args : Array` within the script) ```{bash} @@ -247,19 +246,20 @@ Script Configuration The following directives supported by `kscript` to configure scripts: -* `//DEPS` to declare dependencies with gradle-style locators -* `//KOTLIN_OPTS` to configure the kotlin/java runtime environment -* `//INCLUDE` to source kotlin files into the script -* `//ENTRY` to declare the application entrypoint for kotlin `*.kt` applications +* `@file:DependsOn` to declare dependencies with gradle-style locators +* `@file:Include` to source kotlin files into the script +* `@file:EntryPoint` to declare the application entrypoint for kotlin `*.kt` applications +* `@file:CompilerOptions` to configure the compilation options +* `@file:KotlinOptions` to configure the kotlin/java runtime environment -### Declare dependencies with `//DEPS` +### Declare dependencies with `@file:DependsOn` To specify dependencies simply use gradle-style locators. Here's an example using [docopt](https://github.com/docopt/docopt.java) and [log4j](http://logging.apache.org/log4j/2.x/) ```kotlin #!/usr/bin/env kscript -//DEPS com.offbytwo:docopt:0.6.0.20150202,log4j:log4j:1.2.14 +@file:DependsOn("com.offbytwo:docopt:0.6.0.20150202", "log4j:log4j:1.2.14") import org.docopt.Docopt import java.util.* @@ -279,26 +279,26 @@ println("Hello from Kotlin!") println("Parsed script arguments are: \n" + doArgs) ``` -`kscript` will read dependencies from all lines in a script that start with `//DEPS` (if any). Multiple dependencies can +`kscript` will read dependencies from all lines in a script that start with `@file:DependsOn` (if any). Multiple dependencies can be split by comma, space or semicolon. -### Configure the runtime with `//KOTLIN_OPTS` +### Configure the runtime with `@file:KotlinOptions` -`kscript` allows to provide a `//KOTLIN_OPTS` directive followed by parameters passed on to `kotlin` similar to how +`kscript` allows to provide a `@file:KotlinOptions` directive followed by parameters passed on to `kotlin` similar to how dependencies are defined: ```kotlin #!/usr/bin/env kscript -//KOTLIN_OPTS -J-Xmx5g -J-server +@file:KotlinOptions("-J-Xmx5g", "-J-server") println("Hello from Kotlin with 5g of heap memory running in server mode!") ``` -Note: Similar to the runtime you can also tweak the compile step by providing `//COMPILER_OPTS`. +Note: Similar to the runtime you can also tweak the compile step by providing `@file:CompilerOptions`. -### Ease prototyping with `//INCLUDE` +### Ease prototyping with `@file:Include` -`kscript` supports an `//INCLUDE` directive to directly include other source files without prior compilation. Absolute +`kscript` supports an `@file:Include` directive to directly include other source files without prior compilation. Absolute and relative paths, as well as URLs are supported. Example: ```kotlin @@ -309,14 +309,14 @@ fun Array.median(): Double { } ``` -Which can be now used using the `//INCLUDE` directive with +Which can be now used using the `@file:Include` directive with ```kotlin #!/usr/bin/env kscript -//INCLUDE utils.kt +@file:Include("utils.kt") -val robustMean = listOf(1.3, 42.3, 7.).median() +val robustMean = listOf(1.3, 42.3, 7.0).median() println(robustMean) ``` @@ -326,7 +326,7 @@ with `kscript --clear-cache`. For more examples see [here](test/resources/includes/include_variations.kts). -### Use `//ENTRY` to run applications with `main` method +### Use `@file:EntryPoint` to run applications with `main` method `kscript` also supports running regular Kotlin `kt` files. @@ -335,7 +335,7 @@ Example: `./examples/Foo.kt`: ```kotlin package examples -//ENTRY examples.Bar +@file:EntryPoint("examples.Bar") class Bar { companion object { @@ -349,45 +349,33 @@ class Bar { fun main(args: Array) = println("main was called") ``` -To run top-level main instead we would use `//ENTRY examples.FooKt` +To run top-level main instead we would use `@file:EntryPoint("examples.FooKt")` The latter is the default for `kt` files and could be omitted -### Annotation driven script configuration - -Using annotations instead of comment directives to configure scripts is cleaner and allow for better tooling. - -```kotlin -// annotation-driven script configuration -@file:DependsOn("com.github.holgerbrandl:kutils:0.12") - -// comment directive -//DEPS com.github.holgerbrandl:kutils:0.12 -``` - -To do so `kscript` supports [annotations](https://github.com/holgerbrandl/kscript-annotations) to be used instead of -comment directives: +### Examples of annotation driven configuration ```kotlin #!/usr/bin/env kscript // Declare dependencies -@file:DependsOn("com.github.holgerbrandl:kutils:0.12") @file:DependsOn( +@file:DependsOn("com.github.holgerbrandl:kutils:0.12") +@file:DependsOn( "com.beust:klaxon:0.24", "com.github.kittinunf.fuel:fuel:2.3.1" ) // To use a custom maven repository you can declare it with -@file:MavenRepository("imagej-releases", "http://maven.imagej.net/content/repositories/releases") +@file:Repository("http://maven.imagej.net/content/repositories/releases") // For compatibility with https://github.com/ligee/kotlin-jupyter kscript supports also @file:DependsOnMaven("net.clearvolume:cleargl:2.0.1") // Note that for compatibility reasons, only one locator argument is allowed for @DependsOnMaven // also protected artifact repositories are supported, see -// @file:MavenRepository("my-art", "http://localhost:8081/artifactory/authenticated_repo", user="auth_user", password="password") +// @file:Repository("my-art", "http://localhost:8081/artifactory/authenticated_repo", user="auth_user", password="password") // You can use environment variables for user and password when string surrounded by double {} brackets -// @file:MavenRepository("my-art", "http://localhost:8081/artifactory/authenticated_repo", user="{{ARTIFACTORY_USER}}", password="{{ARTIFACTORY_PASSWORD}}") +// @file:Repository("my-art", "http://localhost:8081/artifactory/authenticated_repo", user="{{ARTIFACTORY_USER}}", password="{{ARTIFACTORY_PASSWORD}}") // will be use 'ARTIFACTORY_USER' and 'ARTIFACTORY_PASSWORD' environment variables // if the value doesn't found in the script environment will fail @@ -395,7 +383,9 @@ comment directives: @file:Include("util.kt") // Define kotlin options -@file:KotlinOpts("-J-Xmx5g") @file:KotlinOpts("-J-server") @file:CompilerOpts("-jvm-target 1.8") +@file:KotlinOptions("-J-Xmx5g") +@file:KotlinOptions("-J-server") +@file:CompilerOptions("-jvm-target 1.8") // declare application entry point (applies on for kt-files) @file:EntryPoint("Foo.bar") @@ -407,7 +397,7 @@ To enable the use of these annotations in Intellij, the user must add the follow dependencies: ``` -com.github.holgerbrandl:kscript-annotations:1.2 +com.github.holgerbrandl:kscript-annotations:1.4 ``` `kscript` will automatically detect an annotation-driven script, and if so will declare a dependency on this artifact @@ -429,7 +419,7 @@ e.g. `import DependsOn`). * Define variable `val lines = kscript.text.resolveArgFile(args)` which returns an iterator over the lines in the first input argument of the script, or the standard input if no file arguments are provided to the script -This allows to to replace `awk`ward constructs (or `sed` or`perl`) with _kotlinesque_ solutions such as +This allows to replace `awk`ward constructs (or `sed` or`perl`) with _kotlinesque_ solutions such as ```bash cat some_file | kscript -t 'lines @@ -503,7 +493,7 @@ This will open [IntelliJ IDEA](https://www.jetbrains.com/idea/) with a minimalis This assumes that you have the Intellij IDEA command line launcher `idea` in your `PATH`. It can be created in IntelliJ under `Tools -> Create Command-line Launcher` or you can set the command used to launch your intellij -as `KSCRIPT_IDEA_COMMAND` env property +as `KSCRIPT_COMMAND_IDEA` env property Deploy scripts as standalone binaries -------------------------------------- @@ -516,7 +506,7 @@ kscript --package some_script.kts ``` The created binary will contain a compiled copy of the script, as well as all declared dependencies (fatjar). Also -runtime jvm parameters declared via `@file:KotlinOpts` are used to spin up the JVM. +runtime jvm parameters declared via `@file:KotlinOptions` are used to spin up the JVM. Just `java` is required to run these binaries. @@ -526,7 +516,7 @@ Embed kscript installer within your script To make a script automatically [install kscript](#installation) and its dependencies on first run if necessary, run: - ```bash +```bash kscript --add-bootstrap-header some_script.kts ``` @@ -540,6 +530,49 @@ scripts. On the other hand this doesn't embed dependencies within the script("fat jar"), so internet connection may be required on its first run. +kscript configuration file +-------------------------------------- + +To keep some options stored permanently in configuration you can create kscript configuration file. + +KScript follows XDG directory standard, so the file should be created in: + + +| OS | PATH | +|-------------|-------------------------------------------------------------------------------------| +| **Windows** | %LOCALAPPDATA%\kscript.properties | +| **Posix** | \\${XDG_CONFIG_DIR}/kscript.properties or \\${user.home}/.config/kscript.properties | + + +Content of kscript.properties file is a standard Java format, with following properties available: + +``` +scripting.preamble= +scripting.kotlin.opts= +scripting.repository.url= +scripting.repository.user= +scripting.repository.password= +``` + +Example configuration file: + +``` +scripting.preamble=// declare dependencies\n\ +@file:DependsOn("com.github.holgerbrandl:kutils:0.12")\n\ +\n\ +// make sure to also support includes in here\n\ +// @file:Include("util.kt")\n\ +@file:Include("https://raw.githubusercontent.com/holgerbrandl/kscript/master/test/resources/custom_dsl/test_dsl_include.kt")\n\ +\n\ +\n\ +// define some important variables to be used throughout the dsl\n\ +val foo = "bar" + +scripting.kotlin.opts=-J-Xmx4g +scripting.repository.url=https://repository.example +scripting.repository.user=user +scripting.repository.password=password +``` FAQ --- @@ -563,20 +596,11 @@ is a valid Kotlin `kts` script. Plain and simple, no `main`, no `companion`, jus ### Does `kscript` also work for regular kotlin `.kt` source files with a `main` as entry point? -Yes, (since v1.6) you can run kotlin source files through `kscript`. By default it will assume a top-level `main` method +Yes, (since kscript v1.6) you can run kotlin source files through `kscript`. By default, it will assume a top-level `main` method as entry-point. -However in case you're using a companion object to declare the entry point, you need to indicate this via the `//ENTRY` -/`@file:Entry` directive: - -### Why does it fail to read my script file when using cygwin? +However, in case you're using a companion object to declare the entry point, you need to indicate this via the `@file:Entry`. -In order to use cygwin you need to use windows paths to provide your scripts. You can map cygwin paths using `cygpath`. -Example - -```bash -kscript $(cygpath -w /cygdrive/z/some/path/my_script.kts) -``` ### What are performance and resource usage difference between scripting with kotlin and python? @@ -603,7 +627,7 @@ see [jbang](https://github.com/maxandersen/jbang). ### Can I use custom artifact repositories? -Yes, via the `@MavenRepository` annotation. See [annotations section](#annotation-driven-script-configuration) +Yes, via the `@Repository` annotation. See [annotations section](#annotation-driven-script-configuration) or [custom_mvn_repo_annot](test/resources/custom_mvn_repo_annot.kts) for a complete example diff --git a/TODO.md b/TODO.md index 7025cd7ec..8934e1ac5 100644 --- a/TODO.md +++ b/TODO.md @@ -1,11 +1,12 @@ -# kscript 4.1 features: +# kscript 4.2 features: -* Multiplatform tests for different OS-es -* Windows console support requires @argfiles as kotlin/kotlinc command line is too long to execute it from console. - -* Fix for IntelliJ projects consisting of files with the same names + re-enable tests -* Fix for packaging + re-enable tests -* Cleanup of ENV variables (CUSTOM_KSCRIPT_PREAMBLE -> KSCRIPT_PREAMBLE, KSCRIPT_IDEA_COMMAND -> KSCRIPT_COMMAND_IDEA, KSCRIPT_GRADLE_COMMAND -> KSCRIPT_COMMAND_GRADLE) -* Depreciation of @MavenRepository -> @Repository is Kotlin standard -* Depreciation of some old features with WARN (comment based annotations, referencing script by $HOME and by '/' - those references won't work for web scripts) -* Improve Unit tests +* Changes in kscript release process - new organization, release KScript jar to Maven, new package for Windows e.g. scoop +* Compatibility with Kotlin Scripting +* Windows console support requires @argfiles as kotlin/kotlinc command line might be too long to execute it from console (especially for big classpaths). +* Improve Unit tests coverage +* Improve batch file for Windows (currently it does not pass failed exitCode) +* Consider changing a way of executing last command, so that it is not executed by shell, but is executed directly in kscript (main concern: kotlin interactive shell, but maybe this use case is not that important) +* Use compilation option -include-runtime: https://kotlinlang.org/docs/command-line.html#create-and-run-an-application +* Integration tests - more tests should be enabled; +* kscript - some features might be disabled on specific OSes - handle that on code level e.g. throw exception if for some OS feature is not available. +* Deprecate referencing script by $HOME and by '/' (it is handled now safely, but does it make sense to keep it?) diff --git a/build.gradle.kts b/build.gradle.kts index 3a0afdacc..4f0e8f181 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,7 +1,5 @@ import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar import com.github.jengelman.gradle.plugins.shadow.transformers.ComponentsXmlResourceTransformer -import org.gradle.api.tasks.testing.logging.TestExceptionFormat -import org.gradle.api.tasks.testing.logging.TestLogEvent val kotlinVersion: String = "1.6.21" @@ -9,6 +7,7 @@ plugins { kotlin("jvm") version "1.6.21" application id("com.github.johnrengelman.shadow") version "7.1.2" + id("com.adarshr.test-logger") version "3.2.0" } repositories { @@ -17,28 +16,65 @@ repositories { group = "com.github.holgerbrandl.kscript.launcher" -tasks.test { - useJUnitPlatform() +sourceSets { + create("integration") { +// test { //With that idea can understand that 'integration' is test source set and do not complain about test +// names starting with upper case, but it doesn't compile correctly with it + java.srcDir("$projectDir/src/integration/kotlin") + resources.srcDir("$projectDir/src/integration/resources") + compileClasspath += main.get().output + test.get().output + runtimeClasspath += main.get().output + test.get().output + } +// } +} - testLogging { - events(TestLogEvent.FAILED) - exceptionFormat = TestExceptionFormat.FULL - } +configurations { + get("integrationImplementation").apply { extendsFrom(get("testImplementation")) } } -tasks.withType { - addTestListener(object : TestListener { - override fun beforeSuite(suite: TestDescriptor) { - logger.quiet("\nTest class: ${suite.displayName}") +tasks.create("integration") { + val itags = System.getProperty("includeTags") ?: "" + val etags = System.getProperty("excludeTags") ?: "" + + println("Include tags: $itags") + println("Exclude tags: $etags") + + useJUnitPlatform { + if (itags.isNotBlank()) { + includeTags(itags) } - override fun beforeTest(testDescriptor: TestDescriptor) {} - override fun afterTest(testDescriptor: TestDescriptor, result: TestResult) { - logger.quiet("${String.format("%-60s - %-10s", testDescriptor.name, result.resultType)} ") + if (etags.isNotBlank()) { + excludeTags(etags) } + } + + systemProperty("osType", System.getProperty("osType")) + systemProperty("projectPath", projectDir.absolutePath) + systemProperty("shellPath", System.getProperty("shellPath")) + + description = "Runs the integration tests." + group = "verification" + testClassesDirs = sourceSets["integration"].output.classesDirs + classpath = sourceSets["integration"].runtimeClasspath + outputs.upToDateWhen { false } + mustRunAfter(tasks["test"]) + //dependsOn(tasks["assemble"], tasks["test"]) +} - override fun afterSuite(suite: TestDescriptor, result: TestResult) {} - }) +tasks.create("printIntegrationClasspath") { + doLast { + println(sourceSets["integration"].runtimeClasspath.asPath) + } +} + +testlogger { + showStandardStreams = true + showFullStackTraces = false +} + +tasks.test { + useJUnitPlatform() } val launcherClassName: String = "kscript.app.KscriptKt" @@ -47,22 +83,30 @@ dependencies { implementation("com.offbytwo:docopt:0.6.0.20150202") implementation("org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.1") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.2") implementation("org.jetbrains.kotlin:kotlin-scripting-common:$kotlinVersion") implementation("org.jetbrains.kotlin:kotlin-scripting-jvm:$kotlinVersion") implementation("org.jetbrains.kotlin:kotlin-scripting-dependencies:$kotlinVersion") implementation("org.jetbrains.kotlin:kotlin-scripting-dependencies-maven-all:$kotlinVersion") + implementation("org.apache.commons:commons-lang3:3.12.0") implementation("commons-io:commons-io:2.11.0") implementation("commons-codec:commons-codec:1.15") + implementation("net.igsoft:tablevis:0.6.0") + implementation("io.arrow-kt:arrow-core:1.1.2") + implementation("org.slf4j:slf4j-nop:1.7.36") + + testImplementation("org.junit.platform:junit-platform-suite-engine:1.8.2") + testImplementation("org.junit.platform:junit-platform-suite-api:1.8.2") + testImplementation("org.junit.platform:junit-platform-suite-commons:1.8.2") testImplementation("org.junit.jupiter:junit-jupiter-engine:5.8.2") testImplementation("org.junit.jupiter:junit-jupiter-params:5.8.2") testImplementation("com.willowtreeapps.assertk:assertk-jvm:0.25") - testImplementation("io.mockk:mockk:1.12.3") + testImplementation("io.mockk:mockk:1.12.4") testImplementation(kotlin("script-runtime")) } @@ -86,14 +130,14 @@ application { } // Disable standard jar task to avoid building non-shadow jars -val jar by tasks.getting { +val jar: Task by tasks.getting { enabled = false } // Build shadowJar when -val assemble by tasks.getting { +val assemble: Task by tasks.getting { dependsOn(shadowJar) } -val test by tasks.getting { +val test: Task by tasks.getting { inputs.dir("${project.projectDir}/test/resources") } diff --git a/linux_env.sh b/linux_env.sh new file mode 100644 index 000000000..77bd45f01 --- /dev/null +++ b/linux_env.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash + +SCRIPT_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" +PROJECT_DIR=$(realpath "$SCRIPT_DIR") +KSCRIPT_EXEC_DIR="$PROJECT_DIR/build/libs" +KSCRIPT_TEST_DIR="$PROJECT_DIR/build/tmp/test" + +mkdir -p $KSCRIPT_EXEC_DIR +mkdir -p $KSCRIPT_TEST_DIR + +echo "Setting up environment..." +echo "SCRIPT_DIR : $SCRIPT_DIR" +echo "PROJECT_DIR: $PROJECT_DIR" +echo "KSCRIPT_EXEC_DIR: $KSCRIPT_EXEC_DIR" +echo "KSCRIPT_TEST_DIR: $KSCRIPT_TEST_DIR" +echo + +if [[ "$PATH" != *"$KSCRIPT_EXEC_DIR"* ]]; then + export PATH=$KSCRIPT_EXEC_DIR:$PATH +fi + +echo "KScript path for testing: $(which kscript)" + +alias cdk="cd $PROJECT_DIR" + +alias switchPath=' +if [[ "$PATH" != *"$KSCRIPT_EXEC_DIR"* ]]; then + export PATH="$KSCRIPT_EXEC_DIR:$PATH" + echo "Project path set." +else + export PATH=$(echo $PATH | tr ":" "\n" | grep -v "$KSCRIPT_EXEC_DIR" | grep -v "^$" | tr "\n" ":") + echo "Generic path set." +fi +' + +alias help-dev="cat $SCRIPT_DIR/test/help-dev.txt" diff --git a/src/integration/kotlin/kscript/integration/AnnotationTest.kt b/src/integration/kotlin/kscript/integration/AnnotationTest.kt new file mode 100644 index 000000000..d35b0d9d4 --- /dev/null +++ b/src/integration/kotlin/kscript/integration/AnnotationTest.kt @@ -0,0 +1,77 @@ +package kscript.integration + +import kscript.integration.tools.TestAssertion.any +import kscript.integration.tools.TestAssertion.startsWith +import kscript.integration.tools.TestAssertion.verify +import kscript.integration.tools.TestContext.projectDir +import kscript.integration.tools.TestContext.resolvePath +import org.apache.commons.io.FileUtils +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Test +import java.io.File + +class AnnotationTest : TestBase { + @Test + @Tag("posix") + @Tag("windows") + fun `There are some dependencies which are not jar, but maybe pom, aar and others - make sure they work, too`() { + verify("kscript ${resolvePath("$projectDir/test/resources/depends_on_with_type.kts")}", 0, "getBigDecimal(1L): 1\n", any()) + } + + @Test + @Tag("posix") + @Tag("windows") + fun `Make sure that DependsOn is parsed correctly`() { + verify("kscript ${resolvePath("$projectDir/test/resources/depends_on_annot.kts")}", 0, "kscript with annotations rocks!\n", any()) + } + + @Test + @Tag("posix") + @Tag("windows") + fun `Make sure that DependsOnMaven is parsed correctly`() { + verify("kscript ${resolvePath("$projectDir/test/resources/depends_on_maven_annot.kts")}", 0, "kscript with annotations rocks!\n", any()) + } + + @Test + @Tag("posix") + @Tag("windows") + fun `Make sure that dynamic versions are matched properly`() { + verify("kscript ${resolvePath("$projectDir/test/resources/depends_on_dynamic.kts")}", 0, "dynamic kscript rocks!\n", any()) + } + + @Test + @Tag("posix") + //TODO: @Tag("windows") - batch file doesn't pass correctly error exitCode + fun `Make sure that MavenRepository is parsed correctly`() { + verify( + "kscript ${resolvePath("$projectDir/test/resources/custom_mvn_repo_annot.kts")}", + 0, + "kscript with annotations rocks!\n", + startsWith("[kscript] Adding repository: Repository(id=, url=http://maven.imagej.net/content/repositories/releases, user=, password=)\n") + ) + verify( + "kscript ${resolvePath("$projectDir/test/resources/illegal_depends_on_arg.kts")}", + 1, + "", + "[kscript] [ERROR] Artifact locators must be provided as separate annotation arguments and not as comma-separated list: [com.squareup.moshi:moshi:1.5.0,com.squareup.moshi:moshi-adapters:1.5.0]\n" + ) + verify("kscript $projectDir/test/resources/script_with_compile_flags.kts", 0, "hoo_ray\n", any()) + } + + @Test + @Tag("posix") + @Tag("windows") + fun `Ensure dependencies are solved correctly #345`() { + val dependencyDirectory = File(System.getProperty("user.home") + "/.m2/repository/com/beust") + if (dependencyDirectory.exists()) { + FileUtils.cleanDirectory(dependencyDirectory) + } + + verify( + "kscript ${resolvePath("$projectDir/test/resources/depends_on_klaxon.kts")}", + 0, + "Successfully resolved klaxon\n", + "[kscript] Resolving com.beust:klaxon:5.5...\n" + ) + } +} diff --git a/src/integration/kotlin/kscript/integration/BootstrapHeaderTest.kt b/src/integration/kotlin/kscript/integration/BootstrapHeaderTest.kt new file mode 100644 index 000000000..5d9dcd1b4 --- /dev/null +++ b/src/integration/kotlin/kscript/integration/BootstrapHeaderTest.kt @@ -0,0 +1,40 @@ +package kscript.integration + +import kscript.integration.tools.TestAssertion.contains +import kscript.integration.tools.TestAssertion.startsWith +import kscript.integration.tools.TestAssertion.verify +import kscript.integration.tools.TestContext.copyToTestPath +import kscript.integration.tools.TestContext.resolvePath +import kscript.integration.tools.TestContext.testDir +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Test + +class BootstrapHeaderTest : TestBase { + @Test + @Tag("linux") + @Tag("macos") + //TODO: Doesn't work on msys and cygwin for some reason + fun `Test adding bootstrap header`() { + // ensure script works as is + val testFile = resolvePath("$testDir/echo_stdin_args.kts") + verify("echo stdin | '$testFile' --foo bar", 0, "stdin | script --foo bar\n") + + // add bootstrap header + verify("kscript --add-bootstrap-header '$testFile'", 0, "", contains("echo_stdin_args.kts updated")) + + // ensure adding it again raises an error + verify("kscript --add-bootstrap-header '$testFile'", 1, "", startsWith("[kscript] [ERROR] Bootstrap header already detected:")) + + // ensure scripts works with header, including stdin + verify("echo stdin | '$testFile' --foo bar", 0, "stdin | script --foo bar\n") + + // ensure scripts works with header invoked with explicit `kscript` + verify("echo stdin | kscript '$testFile' --foo bar", 0, "stdin | script --foo bar\n") + } + + companion object { + init { + copyToTestPath("test/resources/echo_stdin_args.kts") + } + } +} diff --git a/src/integration/kotlin/kscript/integration/CliReplTest.kt b/src/integration/kotlin/kscript/integration/CliReplTest.kt new file mode 100644 index 000000000..bdb32f607 --- /dev/null +++ b/src/integration/kotlin/kscript/integration/CliReplTest.kt @@ -0,0 +1,14 @@ +package kscript.integration + +class CliReplTest : TestBase { + fun `CLI REPL tests`() { +// ## interactive mode without dependencies +// #assert "kscript -i 'exitProcess(0)'" "To create a shell with script dependencies run:\nkotlinc -classpath ''" +// #assert "echo '' | kscript -i -" "To create a shell with script dependencies run:\nkotlinc -classpath ''" +// +// +// ## first version is disabled because support-auto-prefixing kicks in +// #assert "kscript -i '//DEPS log4j:log4j:1.2.14'" "To create a shell with script dependencies run:\nkotlinc -classpath '${HOME}/.m2/repository/log4j/log4j/1.2.14/log4j-1.2.14.jar'" +// #assert "kscript -i <(echo '//DEPS log4j:log4j:1.2.14')" "To create a shell with script dependencies run:\nkotlinc -classpath '${HOME}/.m2/repository/log4j/log4j/1.2.14/log4j-1.2.14.jar'" + } +} diff --git a/src/integration/kotlin/kscript/integration/CustomInterpretersTest.kt b/src/integration/kotlin/kscript/integration/CustomInterpretersTest.kt new file mode 100644 index 000000000..f581dccc2 --- /dev/null +++ b/src/integration/kotlin/kscript/integration/CustomInterpretersTest.kt @@ -0,0 +1,28 @@ +package kscript.integration + +import kscript.integration.tools.TestContext.copyToExecutablePath +import kscript.integration.tools.TestAssertion.any +import kscript.integration.tools.TestAssertion.verify +import kscript.integration.tools.TestContext.projectDir +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Test + +class CustomInterpretersTest : TestBase { + @Test + @Tag("posix") + fun `Execute mydsl as interpreter`() { + verify("mydsl \"println(foo)\"", 0, "bar\n", any()) + } + + @Test + @Tag("posix") + fun `Execute mydsl test with deps`() { + verify("$projectDir/test/resources/custom_dsl/mydsl_test_with_deps.kts", 0, "foobar\n", any()) + } + + companion object { + init { + copyToExecutablePath("test/resources/custom_dsl/mydsl") + } + } +} diff --git a/src/integration/kotlin/kscript/integration/DeprecatedReportTest.kt b/src/integration/kotlin/kscript/integration/DeprecatedReportTest.kt new file mode 100644 index 000000000..379d4e3d7 --- /dev/null +++ b/src/integration/kotlin/kscript/integration/DeprecatedReportTest.kt @@ -0,0 +1,35 @@ +package kscript.integration + +import kscript.integration.tools.TestAssertion.contains +import kscript.integration.tools.TestAssertion.startsWith +import kscript.integration.tools.TestAssertion.verify +import kscript.integration.tools.TestContext.projectDir +import kscript.integration.tools.TestContext.resolvePath +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Test + +class DeprecatedReportTest : TestBase { + @Test + @Tag("posix") + @Tag("windows") + fun `Make sure that for deprecated features warn is generated`() { + verify( + "kscript ${resolvePath("$projectDir/test/resources/deprecated_report.kt")}", + 0, + "made it!\n", + startsWith("[kscript] [WARN] There are deprecated features in scripts. Use --report option to print full report.") + ) + } + + @Test + @Tag("posix") + @Tag("windows") + fun `Assert that report with deprecated features is generated`() { + verify( + "kscript --report ${resolvePath("$projectDir/test/resources/deprecated_report.kt")}", + 0, + "made it!\n", + contains("@file:DependsOn(\"org.apache.commons:commons-lang3:3.12.0\")") + ) + } +} diff --git a/src/integration/kotlin/kscript/integration/EnvironmentTest.kt b/src/integration/kotlin/kscript/integration/EnvironmentTest.kt new file mode 100644 index 000000000..5e6e48591 --- /dev/null +++ b/src/integration/kotlin/kscript/integration/EnvironmentTest.kt @@ -0,0 +1,30 @@ +package kscript.integration + +import kscript.integration.tools.TestAssertion.startsWith +import kscript.integration.tools.TestAssertion.verify +import kscript.integration.tools.TestContext.projectDir +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Test + +class EnvironmentTest : TestBase { + @Test + @Tag("posix") + fun `Do not run interactive mode prep without script argument`() { + verify("kscript -i", 1, "", startsWith("kscript - Enhanced scripting support for Kotlin")) + } + + @Test + @Tag("posix") + fun `Make sure that KOTLIN_HOME can be guessed from kotlinc correctly`() { + verify("unset KOTLIN_HOME; echo 'println(99)' | kscript -", 0, "99\n") + } + + //TODO: test what happens if kotlin/kotlinc/java/gradle/idea is not in PATH + + @Test + @Tag("posix") + fun `Run script that tries to find out its own filename via environment variable`() { + val path = "$projectDir/test/resources/uses_self_file_name.kts" + verify(path, 0, "Usage: uses_self_file_name.kts [-ae] [--foo] file+\n") + } +} diff --git a/src/integration/kotlin/kscript/integration/IdeaTest.kt b/src/integration/kotlin/kscript/integration/IdeaTest.kt new file mode 100644 index 000000000..1b634bb31 --- /dev/null +++ b/src/integration/kotlin/kscript/integration/IdeaTest.kt @@ -0,0 +1,37 @@ +package kscript.integration + +import kscript.integration.tools.TestAssertion.any +import kscript.integration.tools.TestAssertion.verify +import kscript.integration.tools.TestContext.copyToExecutablePath +import kscript.integration.tools.TestContext.projectDir +import kscript.integration.tools.TestContext.resolvePath +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Test + +class IdeaTest : TestBase { + @Test + @Tag("linux") + @Tag("macos") + //TODO: On MSys and Cygwin test doesn't work, and is accomplished with timeout + fun `Temp projects with include symlinks`() { + val result = verify("kscript --idea ${resolvePath("$projectDir/test/resources/includes/include_variations.kts")}", 0, any(), any()) + val ideaDir = result.stderr.trim().lines().last().removePrefix("[kscript] ") + verify("cd $ideaDir && gradle build", 0, any(), any()) + } + + @Test + @Tag("linux") + @Tag("macos") + //TODO: On MSys and Cygwin test doesn't work, and is accomplished with timeout + fun `Support diamond-shaped include schemes (see #133)`() { + val result = verify("kscript --idea ${resolvePath("$projectDir/test/resources/includes/diamond.kts")}", 0, any(), any()) + val ideaDir = result.stderr.trim().lines().last().removePrefix("[kscript] ") + verify("cd $ideaDir && gradle build", 0, any(), any()) + } + + companion object { + init { + copyToExecutablePath("test/resources/idea") + } + } +} diff --git a/src/integration/kotlin/kscript/integration/KtSupportTest.kt b/src/integration/kotlin/kscript/integration/KtSupportTest.kt new file mode 100644 index 000000000..dd2dc8899 --- /dev/null +++ b/src/integration/kotlin/kscript/integration/KtSupportTest.kt @@ -0,0 +1,48 @@ +package kscript.integration + +import kscript.integration.tools.TestAssertion.any +import kscript.integration.tools.TestAssertion.verify +import kscript.integration.tools.TestContext.projectDir +import kscript.integration.tools.TestContext.resolvePath +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Test + +class KtSupportTest : TestBase { + @Test + @Tag("posix") + fun `Run kt via interpreter mode`() { + verify(resolvePath("$projectDir/test/resources/kt_tests/simple_app.kt"), 0, "main was called\n", any()) + } + + @Test + @Tag("posix") + @Tag("windows") + fun `Run kt via interpreter mode with dependencies`() { + verify("kscript ${resolvePath("$projectDir/test/resources/kt_tests/main_with_deps.kt")}", 0, "made it!\n", "[kscript] Resolving log4j:log4j:1.2.14...\n") + } + + @Test + @Tag("linux") + @Tag("macos") + @Tag("msys") + @Tag("windows") + //TODO: Additional new lines are in stdout for cygwin + fun `Test misc entry point with or without package configurations (no cygwin)`() { + verify("kscript ${resolvePath("$projectDir/test/resources/kt_tests/default_entry_nopckg.kt")}", 0, "main was called\n") + verify("kscript ${resolvePath("$projectDir/test/resources/kt_tests/default_entry_withpckg.kt")}", 0, "main was called\n") + } + + @Test + @Tag("posix") + @Tag("windows") + fun `Test misc entry point with or without package configurations`() { + verify("kscript ${resolvePath("$projectDir/test/resources/kt_tests/custom_entry_nopckg.kt")}", 0, "foo companion was called\n") + verify("kscript ${resolvePath("$projectDir/test/resources/kt_tests/custom_entry_withpckg.kt")}", 0, "foo companion was called\n") + } + + @Test + @Tag("posix") + fun `Also make sure that kts in package can be run via kscript`() { + verify(resolvePath("$projectDir/test/resources/script_in_pckg.kts"), 0, "I live in a package!\n") + } +} diff --git a/src/integration/kotlin/kscript/integration/MiscTest.kt b/src/integration/kotlin/kscript/integration/MiscTest.kt new file mode 100644 index 000000000..dc6791313 --- /dev/null +++ b/src/integration/kotlin/kscript/integration/MiscTest.kt @@ -0,0 +1,72 @@ +package kscript.integration + +import kscript.integration.tools.TestAssertion.any +import kscript.integration.tools.TestAssertion.contains +import kscript.integration.tools.TestAssertion.verify +import kscript.integration.tools.TestContext.projectDir +import kscript.integration.tools.TestContext.testDir +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Test + +class MiscTest : TestBase { + @Test + @Tag("linux") + @Tag("macos") + @Tag("msys") + @Tag("windows") + //TODO: Additional new lines are in stdout for cygwin + fun `Prevent regressions of #98 (no cygwin)`() { + verify("""kscript "print(args[0])" "foo bar"""", 0, "foo bar") //make sure quotes are not propagated into args + } + + @Test + @Tag("posix") + @Tag("windows") + fun `Prevent regressions of #98 (it fails to process empty or space-containing arguments)`() { + verify("""kscript "print(args.size)" foo bar""", 0, "2") //regular args + verify("""kscript "print(args.size)" "--params foo"""", 0, "1") //make sure dash args are not confused with options + verify("""kscript "print(args.size)" "foo bar"""", 0, "1") //allow for spaces + } + + @Test + @Tag("posix") + fun `Prevent regressions of #98 (only posix)`() { + verify("""kscript "print(args.size)" "" foo bar""", 0, "3") //accept empty args + } + + @Test + @Tag("posix") + fun `Prevent regression of #181`() { + verify("""echo "println(123)" > $testDir/123foo.kts; kscript $testDir/123foo.kts""", 0, "123\n") + } + + @Test + @Tag("linux") + @Tag("macos") + @Tag("msys") + //TODO: @Tag("cygwin") - doesn't work on cygwin + fun `Prevent regression of #185`() { + verify("source $projectDir/test/resources/home_dir_include.sh $testDir", 0, "42\n") + } + + @Test + @Tag("posix") + fun `Prevent regression of #173`() { + verify("source $projectDir/test/resources/compiler_opts_with_includes.sh $testDir", 0, "hello42\n", any()) + } + + @Test + @Tag("posix") + fun `Ensure relative includes with in shebang mode`() { + verify("$projectDir/test/resources/includes/shebang_mode_includes", 0, "include_1\n") + } + + @Test + @Tag("posix") + fun `Ensure that compilation errors are not cached #349`() { + //first run (not yet cached) + verify("kscript $projectDir/test/resources/invalid_script.kts", 1, "", contains("[kscript] [ERROR] Compilation of scriplet failed:")) + //real test + verify("kscript $projectDir/test/resources/invalid_script.kts", 1, "", contains("[kscript] [ERROR] Compilation of scriplet failed:")) + } +} diff --git a/src/integration/kotlin/kscript/integration/PackagingTest.kt b/src/integration/kotlin/kscript/integration/PackagingTest.kt new file mode 100644 index 000000000..8ecc81305 --- /dev/null +++ b/src/integration/kotlin/kscript/integration/PackagingTest.kt @@ -0,0 +1,43 @@ +package kscript.integration + +import kscript.integration.tools.TestAssertion.any +import kscript.integration.tools.TestAssertion.startsWith +import kscript.integration.tools.TestAssertion.verify +import kscript.integration.tools.TestContext.projectDir +import kscript.integration.tools.TestContext.resolvePath +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Test + +class PackagingTest : TestBase { + @Test + @Tag("linux") + @Tag("macos") + //TODO: doesn't work on msys, cygwin, windows + fun `Packaged script is cached`() { + //@formatter:off + verify("kscript --package \"println(1+1)\"", 0, "", startsWith("[kscript] Packaging script 'scriplet' into standalone executable...")) + verify("kscript --package \"println(1+1)\"", 0, "", startsWith("[kscript] Packaged script 'scriplet' available at path:")) + //@formatter:on + } + + @Test + @Tag("linux") + @Tag("macos") + //TODO: doesn't work on msys, cygwin, windows + fun `Packaging of simple script`() { + val result = + verify("kscript --package ${resolvePath("$projectDir/test/resources/package_example.kts")}", 0, "", any()) + val command = result.stderr.trim().lines().last().removePrefix("[kscript] ") + verify("$command argument", 0, "package_me_args_1_mem_536870912\n") + } + + @Test + @Tag("linux") + @Tag("macos") + //TODO: doesn't work on msys, cygwin, windows + fun `Packaging provided source code and execution with arguments`() { + val result = verify("""kscript --package "println(args.size)"""", 0, "", any()) + val command = result.stderr.trim().lines().last().removePrefix("[kscript] ") + verify("$command three arg uments", 0, "3\n") + } +} diff --git a/src/integration/kotlin/kscript/integration/ScriptInputModesTest.kt b/src/integration/kotlin/kscript/integration/ScriptInputModesTest.kt new file mode 100644 index 000000000..7f2fceec0 --- /dev/null +++ b/src/integration/kotlin/kscript/integration/ScriptInputModesTest.kt @@ -0,0 +1,136 @@ +package kscript.integration + +import kscript.integration.tools.TestAssertion.startsWith +import kscript.integration.tools.TestAssertion.verify +import kscript.integration.tools.TestContext.projectDir +import kscript.integration.tools.TestContext.resolvePath +import kscript.integration.tools.TestContext.testDir +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Test + +class ScriptInputModesTest : TestBase { + @Test + @Tag("posix") + fun `Make sure that scripts can be piped into kscript`() { + verify("source $projectDir/test/resources/direct_script_arg.sh", 0, "kotlin rocks\n", "") + } + + @Test + @Tag("posix") + //it doesn't work on Windows + fun `Also allow for empty programs`() { + verify("kscript ''", 0, "", "") + } + + @Test + @Tag("posix") + @Tag("windows") + fun `Provide script as direct argument`() { + verify("""kscript "println(1+1)"""", 0, "2\n", "") + } + + @Test + @Tag("linux") + @Tag("macos") + @Tag("windows") + //TODO: Doesn't work on msys, cygwin as during test execution " is replaced with '. It causes syntax error in Kotlin. + fun `Use dashed arguments`() { + verify("""kscript "println(args.joinToString(\"\"))" --arg u ments""", 0, "--arguments\n", "") + verify("""kscript -s "print(args.joinToString(\"\"))" --arg u ments""", 0, "--arguments", "") + } + + @Test + @Tag("posix") + fun `Provide script via stidin`() { + verify("echo 'println(1+1)' | kscript -", 0, "2\n") + //stdin and further switch (to avoid regressions of #94) + verify("echo 'println(1+3)' | kscript - --foo", 0, "4\n") + } + + @Test + @Tag("windows") + fun `Provide script via stidin (windows version without quotes)`() { + verify("echo println(1+1) | kscript -", 0, "2\n") + //stdin and further switch (to avoid regressions of #94) + verify("echo println(1+3) | kscript - --foo", 0, "4\n") + } + + @Test + @Tag("posix") + fun `Make sure that heredoc is accepted as argument`() { + verify("source ${projectDir}/test/resources/here_doc_test.sh", 0, "hello kotlin\n") + } + + @Test + @Tag("linux") + @Tag("macos") + //Command substitution doesn't work on msys and cygwin + fun `Make sure that command substitution works as expected`() { + verify("source ${projectDir}/test/resources/cmd_subst_test.sh", 0, "command substitution works as well\n") + } + + @Test + @Tag("posix") + fun `Make sure that it runs with local bash script files`() { + verify("source ${projectDir}/test/resources/local_script_file.sh $testDir", 0, "kscript rocks!\n") + } + + @Test + @Tag("posix") + @Tag("windows") + fun `Make sure that it runs with local script files`() { + verify( + "kscript ${resolvePath("${projectDir}/test/resources/multi_line_deps.kts")}", + 0, + "kscript is cool!\n", + "[kscript] Resolving com.offbytwo:docopt:0.6.0.20150202...\n[kscript] Resolving log4j:log4j:1.2.14...\n" + ) + } + + @Test + @Tag("posix") + @Tag("windows") + fun `Scripts with dashes in the file name should work as well`() { + verify("kscript ${resolvePath("$projectDir/test/resources/dash-test.kts")}", 0, "dash alarm!\n") + } + + @Test + @Tag("posix") + @Tag("windows") + fun `Scripts with additional dots in the file name should work as well`() { + //We also test inner uppercase letters in file name here by using .*T*est + verify("kscript ${resolvePath("$projectDir/test/resources/dot.Test.kts")}", 0, "dot alarm!\n") + } + + @Test + @Tag("posix") + @Tag("windows") + fun `Make sure that it runs with remote URLs`() { + verify( + "kscript https://raw.githubusercontent.com/holgerbrandl/kscript/master/test/resources/url_test.kts", + 0, + "I came from the internet\n" + ) + verify("kscript https://git.io/fxHBv", 0, "main was called\n", "[kscript] Resolving log4j:log4j:1.2.14...\n") + } + + @Test + @Tag("posix") + //TODO: @Tag("windows") - kscript on Windows doesn't return correctly error code () + fun `Repeated compilation of buggy same script should end up in error again`() { + verify("kscript '1-'", 1, "", startsWith("[kscript] [ERROR] Compilation of scriplet failed:")) + verify("kscript '1-'", 1, "", startsWith("[kscript] [ERROR] Compilation of scriplet failed:")) + } + + @Test + @Tag("posix") + //TODO: @Tag("windows") - kscript on Windows doesn't return correctly error code () + fun `Missing script gives always error on execution`() { + verify( + "kscript i_do_not_exist.kts", 1, "", "[kscript] [ERROR] Could not read script from 'i_do_not_exist.kts'\n" + ) + verify( + "kscript i_do_not_exist.kts", 1, "", "[kscript] [ERROR] Could not read script from 'i_do_not_exist.kts'\n" + ) + } +} diff --git a/src/integration/kotlin/kscript/integration/SimpleTest.kt b/src/integration/kotlin/kscript/integration/SimpleTest.kt new file mode 100644 index 000000000..6738ca243 --- /dev/null +++ b/src/integration/kotlin/kscript/integration/SimpleTest.kt @@ -0,0 +1,32 @@ +package kscript.integration + +import kscript.integration.tools.TestAssertion.contains +import kscript.integration.tools.TestAssertion.startsWith +import kscript.integration.tools.TestAssertion.verify +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Test + +class SimpleTest : TestBase { + @Test + @Tag("posix") + @Tag("windows") + fun `Providing source code works`() { + verify("kscript \"println(1+1)\"", 0, "2\n") + } + + @Test + @Tag("posix") + @Tag("windows") + fun `Debugging information is printed`() { + verify("kscript -d \"println(1+1)\"", 0, "2\n", contains("Debugging information for KScript")) + } + + @Test + @Tag("posix") + @Tag("windows") + fun `Help is printed`() { + //@formatter:off + verify("kscript --help", 0, "", startsWith("kscript - Enhanced scripting support for Kotlin on *nix-based systems.")) + //@formatter:on + } +} diff --git a/src/integration/kotlin/kscript/integration/SupportApiTest.kt b/src/integration/kotlin/kscript/integration/SupportApiTest.kt new file mode 100644 index 000000000..2814e6762 --- /dev/null +++ b/src/integration/kotlin/kscript/integration/SupportApiTest.kt @@ -0,0 +1,23 @@ +package kscript.integration + +import kscript.integration.tools.TestAssertion.any +import kscript.integration.tools.TestAssertion.verify +import kscript.integration.tools.TestContext.nl +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Test + +class SupportApiTest : TestBase { + @Test + @Tag("posix") + fun `Make sure that one-liners include support-api`() { + verify("""echo "foo${nl}bar" | kscript -t "stdin.print()"""", 0, "foo\nbar\n", any()) + verify("""echo "foo${nl}bar" | kscript -t "lines.print()"""", 0, "foo\nbar\n", any()) + verify("""echo 'foo${nl}bar' | kscript -t 'lines.print()'""", 0, "foo\nbar\n", any()) + verify( + """echo 'foo${nl}bar' | kscript -s --text 'lines.split().select(1,2,-3)'""", + 1, + "", + "[ERROR] Can not mix positive and negative selections\n" + ) + } +} diff --git a/src/integration/kotlin/kscript/integration/TestBase.kt b/src/integration/kotlin/kscript/integration/TestBase.kt new file mode 100644 index 000000000..942697f4a --- /dev/null +++ b/src/integration/kotlin/kscript/integration/TestBase.kt @@ -0,0 +1,16 @@ +package kscript.integration + +import kscript.integration.tools.TestContext +import org.junit.jupiter.api.BeforeAll + +interface TestBase { + companion object { + @BeforeAll + @JvmStatic + fun setUp() { + TestContext.clearCache() + TestContext.printPaths() + println("[nl] - new line; [bs] - backspace") + } + } +} diff --git a/src/integration/kotlin/kscript/integration/tools/TestAssertion.kt b/src/integration/kotlin/kscript/integration/tools/TestAssertion.kt new file mode 100644 index 000000000..e543a15ab --- /dev/null +++ b/src/integration/kotlin/kscript/integration/tools/TestAssertion.kt @@ -0,0 +1,36 @@ +package kscript.integration.tools + +import kscript.app.shell.ProcessResult +import kscript.integration.tools.TestContext.runProcess + +object TestAssertion { + fun geq(value: T) = GenericEquals(value) + + fun any() = AnyMatch() + fun eq(string: String, ignoreCase: Boolean = false) = Equals(string, ignoreCase) + fun startsWith(string: String, ignoreCase: Boolean = false) = StartsWith(string, ignoreCase) + fun contains(string: String, ignoreCase: Boolean = false) = Contains(string, ignoreCase) + + fun verify(command: String, exitCode: Int = 0, stdOut: TestMatcher, stdErr: String = ""): ProcessResult = + verify(command, exitCode, stdOut, eq(stdErr)) + + fun verify(command: String, exitCode: Int = 0, stdOut: String, stdErr: TestMatcher): ProcessResult = + verify(command, exitCode, eq(stdOut), stdErr) + + fun verify(command: String, exitCode: Int = 0, stdOut: String = "", stdErr: String = ""): ProcessResult = + verify(command, exitCode, eq(stdOut), eq(stdErr)) + + fun verify( + command: String, exitCode: Int = 0, stdOut: TestMatcher, stdErr: TestMatcher + ): ProcessResult { + val processResult = runProcess(command) + val extCde = geq(exitCode) + + extCde.checkAssertion("ExitCode", processResult.exitCode) + stdOut.checkAssertion("StdOut", processResult.stdout) + stdErr.checkAssertion("StdErr", processResult.stderr) + println() + + return processResult + } +} diff --git a/src/integration/kotlin/kscript/integration/tools/TestContext.kt b/src/integration/kotlin/kscript/integration/tools/TestContext.kt new file mode 100644 index 000000000..5bf825446 --- /dev/null +++ b/src/integration/kotlin/kscript/integration/tools/TestContext.kt @@ -0,0 +1,80 @@ +package kscript.integration.tools + +import kscript.app.model.OsType +import kscript.app.shell.* + +object TestContext { + private val osType: OsType = OsType.findOrThrow(System.getProperty("osType")) + private val nativeType = if (osType.isPosixHostedOnWindows()) OsType.WINDOWS else osType + + private val projectPath: OsPath = OsPath.createOrThrow(nativeType, System.getProperty("projectPath")) + private val execPath: OsPath = projectPath.resolve("build/libs") + private val testPath: OsPath = projectPath.resolve("build/tmp/test") + private val pathEnvName = if (osType.isWindowsLike()) "Path" else "PATH" + private val systemPath: String = System.getenv()[pathEnvName]!! + + private val pathSeparator: String = if (osType.isWindowsLike() || osType.isPosixHostedOnWindows()) ";" else ":" + private val envPath: String = "${execPath.convert(osType)}$pathSeparator$systemPath" + private val envMap = mapOf(pathEnvName to envPath) + + val nl: String = System.getProperty("line.separator") + val projectDir: String = projectPath.convert(osType).stringPath() + val testDir: String = testPath.convert(osType).stringPath() + + init { + println("osType : $osType") + println("nativeType : $nativeType") + println("projectDir : $projectDir") + println("testDir : $testDir") + println("execDir : ${execPath.convert(osType)}") + + testPath.createDirectories() + } + + fun resolvePath(path: String): String { + return OsPath.createOrThrow(osType, path).stringPath() + } + + fun runProcess(command: String): ProcessResult { + //In MSYS all quotes should be single quotes, otherwise content is interpreted e.g. backslashes. + //(MSYS bash interpreter is also replacing double quotes into the single quotes: see: bash -xc 'kscript "println(1+1)"') + val newCommand = when { + osType.isPosixHostedOnWindows() -> command.replace('"', '\'') + else -> command + } + + val result = ShellUtils.evalBash(osType, newCommand, null, envMap) + + println(result) + return result + } + + fun copyToExecutablePath(source: String) { + val sourceFile = projectPath.resolve(source).toNativeFile() + val targetFile = execPath.resolve(sourceFile.name).toNativeFile() + + sourceFile.copyTo(targetFile, overwrite = true) + targetFile.setExecutable(true) + } + + fun copyToTestPath(source: String) { + val sourceFile = projectPath.resolve(source).toNativeFile() + val targetFile = testPath.resolve(sourceFile.name).toNativeFile() + + sourceFile.copyTo(targetFile, overwrite = true) + targetFile.setExecutable(true) //Needed if the file is kotlin script + } + + fun printPaths() { + val kscriptPath = ShellUtils.commandPaths(osType, "kscript", envMap) + println("kscript path: $kscriptPath") + val kotlincPath = ShellUtils.commandPaths(osType, "kotlinc", envMap) + println("kotlinc path: $kotlincPath") + } + + fun clearCache() { + print("Clearing kscript cache... ") + ShellUtils.evalBash(osType, "kscript --clear-cache", null, envMap) + println("done.") + } +} diff --git a/src/integration/kotlin/kscript/integration/tools/TestMatcher.kt b/src/integration/kotlin/kscript/integration/tools/TestMatcher.kt new file mode 100644 index 000000000..a5c6ee4d9 --- /dev/null +++ b/src/integration/kotlin/kscript/integration/tools/TestMatcher.kt @@ -0,0 +1,48 @@ +package kscript.integration.tools + +import kscript.app.shell.ShellUtils.whitespaceCharsToSymbols +import kscript.integration.tools.TestContext.nl +import org.opentest4j.AssertionFailedError + +abstract class TestMatcher(protected val expectedValue: T, private val expressionName: String) { + abstract fun matches(value: T): Boolean + + fun checkAssertion(assertionName: String, value: T) { + if (matches(value)) { + return + } + + throw AssertionFailedError( + "$nl$nl$assertionName: expected that value '${ + whitespaceCharsToSymbols(value.toString()) + }' $expressionName '${ + whitespaceCharsToSymbols(expectedValue.toString()) + }'$nl$nl" + ) + } +} + +class GenericEquals(expectedValue: T) : TestMatcher(expectedValue, "is equal to") { + override fun matches(value: T): Boolean = (value == expectedValue) +} + +class AnyMatch : TestMatcher("", "has any value") { + override fun matches(value: String): Boolean = true +} + +class Equals(private val expectedString: String, private val ignoreCase: Boolean) : + TestMatcher(expectedString, "is equal to") { + override fun matches(value: String): Boolean = value.equals(normalize(expectedString), ignoreCase) +} + +class StartsWith(private val expectedString: String, private val ignoreCase: Boolean) : + TestMatcher(expectedString, "starts with") { + override fun matches(value: String): Boolean = value.startsWith(normalize(expectedString), ignoreCase) +} + +class Contains(private val expectedString: String, private val ignoreCase: Boolean) : + TestMatcher(expectedString, "contains") { + override fun matches(value: String): Boolean = value.contains(normalize(expectedString), ignoreCase) +} + +private fun normalize(string: String) = string.replace("\n", nl) diff --git a/src/kscript b/src/kscript index 156b32f06..5057c4ad0 100755 --- a/src/kscript +++ b/src/kscript @@ -36,9 +36,10 @@ else KOTLIN_BIN="$KOTLIN_HOME/bin/" fi -# OSTYPE can be: linux-gnu, freebsd, darwin, cygwin, msys +# OSTYPE can be: linux*, freebsd, darwin*, cygwin, msys if [[ "$OSTYPE" == "cygwin" || "$OSTYPE" == "msys" ]]; then JAR_PATH=$(cygpath -w "${JAR_PATH}") + KOTLIN_BIN=$(cygpath "${KOTLIN_BIN}") true fi @@ -46,4 +47,13 @@ fi export KSCRIPT_FILE="$1" ## run it using command substitution to have just the user process once kscript is done -eval "exec $("${KOTLIN_BIN}kotlin" -classpath "${JAR_PATH}" kscript.app.KscriptKt "$OSTYPE" "$@")" +COMMAND=$("${KOTLIN_BIN}kotlin" -classpath "${JAR_PATH}" kscript.app.KscriptKt "$OSTYPE" "$@") +RESULT=$? + +if [ ! $RESULT -eq 0 ]; then + exit $RESULT +fi + +if [ -n "$COMMAND" ]; then + eval "exec $COMMAND" +fi diff --git a/src/kscript.bat b/src/kscript.bat index ec5308550..95d84b3f0 100644 --- a/src/kscript.bat +++ b/src/kscript.bat @@ -1,8 +1,26 @@ @echo off +setlocal -for /f %%i in ('where kscript.bat') do set ABS_KSCRIPT_PATH=%%i +for /f "tokens=* USEBACKQ" %%o in (`where kscript.bat`) do set ABS_KSCRIPT_PATH=%%o set JAR_PATH=%ABS_KSCRIPT_PATH:~0,-4%.jar -rem kotlin -classpath %JAR_PATH% kscript.app.KscriptKt "windows" %* -FOR /F "tokens=* USEBACKQ" %%O IN (`kotlin -classpath %JAR_PATH% kscript.app.KscriptKt "windows" %*`) DO (SET RESULT=%%O) -%RESULT% +set COMMAND=kotlin -classpath %JAR_PATH% kscript.app.KscriptKt windows %* + +set STDOUT= +set ERRORLEVEL= + +for /f "tokens=* USEBACKQ" %%o in (`%COMMAND%`) do set STDOUT=%%o + +rem https://stackoverflow.com/questions/10935693/foolproof-way-to-check-for-nonzero-error-return-code-in-windows-batch-file/10936093#10936093 +if ERRORLEVEL 1 ( + echo Execution failure: %ERRORLEVEL% + exit /b %ERRORLEVEL% +) + +if not defined STDOUT ( + exit /b %ERRORLEVEL% +) + +%STDOUT% + +exit /b %ERRORLEVEL% diff --git a/src/main/kotlin/kscript/app/Kscript.kt b/src/main/kotlin/kscript/app/Kscript.kt index 797cb8400..68ae8ffef 100644 --- a/src/main/kotlin/kscript/app/Kscript.kt +++ b/src/main/kotlin/kscript/app/Kscript.kt @@ -4,8 +4,8 @@ import kscript.app.code.Templates import kscript.app.model.Config import kscript.app.util.Logger import kscript.app.util.Logger.errorMsg -import kscript.app.util.ShellUtils.evalBash -import kscript.app.util.ShellUtils.quit +import kscript.app.shell.ShellUtils.evalBash +import kscript.app.shell.ShellUtils.quit import kscript.app.util.VersionChecker import org.docopt.DocOptWrapper @@ -18,7 +18,7 @@ import org.docopt.DocOptWrapper * @author Marcin Kuszczak */ -const val KSCRIPT_VERSION = "4.0.4" +const val KSCRIPT_VERSION = "4.1.0" fun main(args: Array) { try { @@ -26,14 +26,14 @@ fun main(args: Array) { val remainingArgs = args.drop(1) // skip org.docopt for version and help to allow for lazy version-check - val usage = Templates.usageOptions(config.selfName, KSCRIPT_VERSION) + val usage = Templates.createUsageOptions(config.osConfig.selfName, KSCRIPT_VERSION) if (remainingArgs.size == 1 && listOf("--help", "-h", "--version", "-v").contains(remainingArgs[0])) { Logger.info(usage) VersionChecker.versionCheck(KSCRIPT_VERSION) - val systemInfo = evalBash(config.osType, "kotlin -version").stdout - Logger.info("Kotlin : " + systemInfo.split('(')[0].removePrefix("Kotlin version").trim()) - Logger.info("Java : " + systemInfo.split('(')[1].split('-', ')')[0].trim()) + val systemInfo = evalBash(config.osConfig.osType, "kotlin -version").stdout.split('(') + Logger.info("Kotlin : " + systemInfo[0].removePrefix("Kotlin version").trim()) + Logger.info("Java : " + systemInfo[1].split('-', ')')[0].trim()) return } @@ -43,7 +43,7 @@ fun main(args: Array) { val docopt = DocOptWrapper(kscriptArgs, usage) - KscriptHandler(config, docopt).handle(userArgs) + KscriptHandler(config, docopt).handle(kscriptArgs, userArgs) } catch (e: Exception) { errorMsg(e) quit(1) diff --git a/src/main/kotlin/kscript/app/KscriptHandler.kt b/src/main/kotlin/kscript/app/KscriptHandler.kt index fefec01d5..a5ad58078 100644 --- a/src/main/kotlin/kscript/app/KscriptHandler.kt +++ b/src/main/kotlin/kscript/app/KscriptHandler.kt @@ -1,40 +1,42 @@ package kscript.app -import kscript.app.appdir.AppDir +import kscript.app.cache.Cache import kscript.app.code.Templates import kscript.app.creator.* import kscript.app.model.Config import kscript.app.model.ScriptType import kscript.app.parser.Parser import kscript.app.resolver.* -import kscript.app.util.Executor +import kscript.app.shell.Executor import kscript.app.util.Logger -import kscript.app.util.Logger.devMsg +import kscript.app.util.Logger.info import kscript.app.util.Logger.infoMsg +import kscript.app.util.Logger.warnMsg import org.docopt.DocOptWrapper import java.net.URI class KscriptHandler(private val config: Config, private val docopt: DocOptWrapper) { - fun handle(userArgs: List) { + fun handle(kscriptArgs: List, userArgs: List) { Logger.silentMode = docopt.getBoolean("silent") Logger.devMode = docopt.getBoolean("development") - devMsg("KScript configuration:") - devMsg(config.toString()) - if (Logger.devMode) { - devMsg("Classpath:") - devMsg(System.getProperty("java.class.path")) + info(DebugInfoCreator().create(config, kscriptArgs, userArgs)) } - // create kscript dir if it does not yet exist - val appDir = AppDir(config.kscriptDir) + val cache = Cache(config.osConfig.kscriptCacheDir) // optionally clear up the jar cache if (docopt.getBoolean("clear-cache")) { - Logger.info("Cleaning up cache...") - appDir.clearCache() + info("Cleaning up cache...") + cache.clear() + return + } + + val scriptSource = docopt.getString("script") + + if (scriptSource.isBlank()) { return } @@ -45,65 +47,75 @@ class KscriptHandler(private val config: Config, private val docopt: DocOptWrapp add(Templates.textProcessingPreamble) } - add(config.customPreamble) + add(config.scriptingConfig.customPreamble) } - val contentResolver = ContentResolver(appDir.cache) - // see https://github.com/holgerbrandl/kscript/issues/127 -// val fileResolver = FileSystemDependenciesResolver() - val sectionResolver = SectionResolver(Parser(), contentResolver, config) - val scriptResolver = ScriptResolver(sectionResolver, contentResolver, config.kotlinOptsEnvVariable) + val inputOutputResolver = InputOutputResolver(config.osConfig, cache) + val sectionResolver = SectionResolver(inputOutputResolver, Parser(), config.scriptingConfig) + val scriptResolver = ScriptResolver(inputOutputResolver, sectionResolver, config.scriptingConfig) if (docopt.getBoolean("add-bootstrap-header")) { - val script = scriptResolver.resolve(docopt.getString("script"), maxResolutionLevel = 0) + val script = scriptResolver.resolve(scriptSource, maxResolutionLevel = 0) BootstrapCreator().create(script) return } - val script = scriptResolver.resolve(docopt.getString("script"), preambles) - val resolvedDependencies = appDir.cache.getOrCreateDependencies(script.digest) { + val script = scriptResolver.resolve(scriptSource, preambles) + + if (script.deprecatedItems.isNotEmpty()) { + if (docopt.getBoolean("report")) { + info(DeprecatedInfoCreator().create(script.deprecatedItems)) + } else { + warnMsg("There are deprecated features in scripts. Use --report option to print full report.") + } + } + + val resolvedDependencies = cache.getOrCreateDependencies(script.digest) { DependencyResolver(script.repositories).resolve(script.dependencies) } - val executor = Executor(CommandResolver(config, script), config) + val executor = Executor(CommandResolver(config.osConfig), config.osConfig) // Create temporary dev environment if (docopt.getBoolean("idea")) { - val path = appDir.cache.getOrCreateIdeaProject(script.digest) { basePath -> - val uriLocalPathProvider = { uri: URI -> contentResolver.resolve(uri).localPath } + val path = cache.getOrCreateIdeaProject(script.digest) { basePath -> + val uriLocalPathProvider = { uri: URI -> inputOutputResolver.resolveContent(uri).localPath } IdeaProjectCreator().create(basePath, script, userArgs, uriLocalPathProvider) } - infoMsg("Project set up at $path") + infoMsg("Idea project available at:") + infoMsg(path.convert(config.osConfig.osType).stringPath()) + executor.runIdea(path) return } // Optionally enter interactive mode if (docopt.getBoolean("interactive")) { - executor.runInteractiveRepl(resolvedDependencies) + executor.runInteractiveRepl(resolvedDependencies, script.compilerOpts, script.kotlinOpts) return } - // Even if we just need and support the //ENTRY directive in case of kt-class + // Even if we just need and support the @file:EntryPoint directive in case of kt-class // files, we extract it here to fail if it was used in kts files. - if (script.entryPoint != null && script.scriptType == ScriptType.KTS) { - throw IllegalStateException("@Entry directive is just supported for kt class files") + if (script.entryPoint != null && script.location.scriptType == ScriptType.KTS) { + throw IllegalStateException("@file:EntryPoint directive is just supported for kt class files") } - val jar = appDir.cache.getOrCreateJar(script.digest) { basePath -> + val jar = cache.getOrCreateJar(script.digest) { basePath -> JarArtifactCreator(executor).create(basePath, script, resolvedDependencies) } //if requested try to package the into a standalone binary if (docopt.getBoolean("package")) { - val path = appDir.cache.getOrCreatePackage(script.digest) { basePath -> - PackageCreator(executor).packageKscript(basePath, script, jar) + val path = cache.getOrCreatePackage(script.digest, script.location.scriptName) { basePath, packagePath -> + PackageCreator(executor).packageKscript(basePath, packagePath, script, jar) } - infoMsg("Package created in: $path") + infoMsg("Packaged script '${script.location.scriptName}' available at path:") + infoMsg(path.convert(config.osConfig.osType).stringPath()) return } - executor.executeKotlin(jar, resolvedDependencies, userArgs) + executor.executeKotlin(jar, resolvedDependencies, userArgs, script.kotlinOpts) } } diff --git a/src/main/kotlin/kscript/app/appdir/AppDir.kt b/src/main/kotlin/kscript/app/appdir/AppDir.kt deleted file mode 100644 index 4c5f4294a..000000000 --- a/src/main/kotlin/kscript/app/appdir/AppDir.kt +++ /dev/null @@ -1,19 +0,0 @@ -package kscript.app.appdir - -import org.apache.commons.io.FileUtils -import java.nio.file.Path -import kotlin.io.path.createDirectories - -class AppDir(path: Path) { - private val cachePath = path.resolve("cache") - - init { - cachePath.createDirectories() - } - - val cache = Cache(cachePath) - - fun clearCache() { - FileUtils.cleanDirectory(cachePath.toFile()) - } -} diff --git a/src/main/kotlin/kscript/app/appdir/Cache.kt b/src/main/kotlin/kscript/app/cache/Cache.kt similarity index 52% rename from src/main/kotlin/kscript/app/appdir/Cache.kt rename to src/main/kotlin/kscript/app/cache/Cache.kt index 501b74cf5..5ef7f3c87 100644 --- a/src/main/kotlin/kscript/app/appdir/Cache.kt +++ b/src/main/kotlin/kscript/app/cache/Cache.kt @@ -1,34 +1,48 @@ -package kscript.app.appdir +package kscript.app.cache import kscript.app.creator.JarArtifact import kscript.app.model.Content import kscript.app.model.ScriptType +import kscript.app.shell.* import org.apache.commons.codec.digest.DigestUtils +import org.apache.commons.io.FileUtils import java.net.URI -import java.net.URL -import java.nio.file.Path -import java.nio.file.Paths -import kotlin.io.path.createDirectories -import kotlin.io.path.exists -import kotlin.io.path.readText -import kotlin.io.path.writeText - -class Cache(private val path: Path) { - fun getOrCreateIdeaProject(digest: String, creator: (Path) -> Path): Path { - return directoryCache(path.resolve("idea_$digest"), creator) + +class Cache(private val cacheBasePath: OsPath) { + init { + cacheBasePath.createDirectories() + } + + fun getOrCreateIdeaProject(digest: String, creator: (OsPath) -> OsPath): OsPath { + val path = cacheBasePath.resolve("idea_$digest") + + return if (path.exists()) { + path + } else { + path.createDirectories() + creator(path) + } } - fun getOrCreatePackage(digest: String, creator: (Path) -> Path): Path { - return directoryCache(path.resolve("package_$digest"), creator) + fun getOrCreatePackage(digest: String, scriptName: String, creator: (OsPath, OsPath) -> OsPath): OsPath { + val path = cacheBasePath.resolve("package_$digest") + val cachedPackageFile = path.resolve("build/libs/$scriptName") + + return if (cachedPackageFile.exists()) { + cachedPackageFile + } else { + path.createDirectories() + creator(path, cachedPackageFile) + } } - fun getOrCreateJar(digest: String, creator: (Path) -> JarArtifact): JarArtifact { - val directory = path.resolve("jar_$digest") + fun getOrCreateJar(digest: String, creator: (OsPath) -> JarArtifact): JarArtifact { + val directory = cacheBasePath.resolve("jar_$digest") val cachedJarArtifact = directory.resolve("jarArtifact.descriptor") return if (cachedJarArtifact.exists()) { val jarArtifactLines = cachedJarArtifact.readText().lines() - JarArtifact(Paths.get(jarArtifactLines[0]), jarArtifactLines[1]) + JarArtifact(OsPath.createOrThrow(cacheBasePath.nativeType, jarArtifactLines[0]), jarArtifactLines[1]) } else { directory.createDirectories() val jarArtifact = creator(directory) @@ -37,12 +51,12 @@ class Cache(private val path: Path) { } } - fun getOrCreateUriItem(url: URL, creator: (URL, Path) -> Content): Content { - val digest = DigestUtils.md5Hex(url.toString()) + fun getOrCreateUriItem(uri: URI, creator: (URI, OsPath) -> Content): Content { + val digest = DigestUtils.md5Hex(uri.toString()) - val directory = path.resolve("url_$digest") - val descriptorFile = directory.resolve("url.descriptor") - val contentFile = directory.resolve("url.content") + val directory = cacheBasePath.resolve("uri_$digest") + val descriptorFile = directory.resolve("uri.descriptor") + val contentFile = directory.resolve("uri.content") if (descriptorFile.exists() && contentFile.exists()) { //Cache hit @@ -57,7 +71,7 @@ class Cache(private val path: Path) { } //Cache miss - val content = creator(url, contentFile) + val content = creator(uri, contentFile) directory.createDirectories() descriptorFile.writeText("${content.scriptType}\n${content.fileName}\n${content.uri}\n${content.contextUri}") @@ -66,12 +80,17 @@ class Cache(private val path: Path) { return content } - fun getOrCreateDependencies(digest: String, creator: () -> Set): Set { - val directory = path.resolve("dependencies_$digest") + fun getOrCreateDependencies(digest: String, creator: () -> Set): Set { + val directory = cacheBasePath.resolve("dependencies_$digest") val contentFile = directory.resolve("dependencies.content") - if (directory.exists()) { - val dependencies = contentFile.readText().lines().map { Paths.get(it) }.toSet() + if (contentFile.exists()) { + val dependencies = + contentFile.readText() + .lines() + .filter { it.isNotEmpty() } + .map { OsPath.createOrThrow(cacheBasePath.nativeType, it) } + .toSet() //Recheck cached paths - if there are missing artifacts skip the cached values if (dependencies.all { it.exists() }) { @@ -86,12 +105,7 @@ class Cache(private val path: Path) { return dependencies } - private fun directoryCache(path: Path, creator: (Path) -> Path): Path { - return if (path.exists()) { - path - } else { - path.createDirectories() - creator(path) - } + fun clear() { + FileUtils.cleanDirectory(cacheBasePath.toNativeFile()) } } diff --git a/src/main/kotlin/kscript/app/code/GradleTemplates.kt b/src/main/kotlin/kscript/app/code/GradleTemplates.kt index a717e94b4..2e4294178 100644 --- a/src/main/kotlin/kscript/app/code/GradleTemplates.kt +++ b/src/main/kotlin/kscript/app/code/GradleTemplates.kt @@ -5,43 +5,40 @@ import kscript.app.model.CompilerOpt import kscript.app.model.Dependency import kscript.app.model.Repository import kscript.app.model.Script -import kscript.app.util.ScriptUtils.dropExtension object GradleTemplates { fun createGradleIdeaScript(script: Script): String { - val kotlinOptions = kotlinOptions(script.compilerOpts) - val kotlinVersion = KotlinVersion.CURRENT val extendedDependencies = setOf( Dependency("org.jetbrains.kotlin:kotlin-stdlib"), - Dependency("org.jetbrains.kotlin:kotlin-script-runtime:$kotlinVersion") + Dependency("org.jetbrains.kotlin:kotlin-script-runtime:$kotlinVersion"), + Dependency("com.github.holgerbrandl:kscript-annotations:1.4"), ) + script.dependencies return """ - plugins { - id("org.jetbrains.kotlin.jvm") version "$kotlinVersion" - } - - repositories { - mavenLocal() - mavenCentral() - ${createGradleRepositoriesSection(script.repositories).prependIndent()} - } - - dependencies { - ${createGradleDependenciesSection(extendedDependencies).prependIndent()} - } - - sourceSets.getByName("main").java.srcDirs("src") - sourceSets.getByName("test").java.srcDirs("src") - - $kotlinOptions - """.trimIndent() + |plugins { + | id("org.jetbrains.kotlin.jvm") version "$kotlinVersion" + |} + | + |repositories { + | mavenLocal() + | mavenCentral() + |${createGradleRepositoriesSection(script.repositories).prependIndent()} + |} + | + |dependencies { + |${createGradleDependenciesSection(extendedDependencies).prependIndent()} + |} + | + |sourceSets.getByName("main").java.srcDirs("src") + |sourceSets.getByName("test").java.srcDirs("src") + | + |${createCompilerOptionsSection(script.compilerOpts)} + |""".trimMargin() } - //Capsule: https://github.com/ngyewch/gradle-capsule-plugin fun createGradlePackageScript(script: Script, jarArtifact: JarArtifact): String { - val kotlinOptions = kotlinOptions(script.compilerOpts) + val kotlinOptions = createCompilerOptionsSection(script.compilerOpts) val kotlinVersion = KotlinVersion.CURRENT val extendedDependencies = setOf( @@ -50,52 +47,71 @@ object GradleTemplates { ) + script.dependencies val capsuleApp = jarArtifact.execClassName + val baseName = script.location.scriptName return """ - plugins { - id("org.jetbrains.kotlin.jvm") version "$kotlinVersion" - id("it.gianluz.capsule") version "1.0.3" - application - } - - repositories { - mavenLocal() - mavenCentral() - ${createGradleRepositoriesSection(script.repositories).prependIndent()} - } - - tasks.create("simpleCapsule") { - applicationClass("$capsuleApp") - archiveFileName.set("${script.scriptName.dropExtension()}") - - // https://github.com/danthegoodman/gradle-capsule-plugin/blob/master/DOCUMENTATION.md#really-executable-capsules - reallyExecutable - - capsuleManifest.apply { - applicationClass = "$capsuleApp" - application = "${script.scriptName.dropExtension()}" - applicationScript = "exec_header.sh" - jvmArgs = listOf() - } - } - - dependencies { - implementation(files("${jarArtifact.path.parent.resolve("scriplet.jar")}")) - ${createGradleDependenciesSection(extendedDependencies).prependIndent()} - } - - $kotlinOptions - """.trimIndent() + |import java.io.* + |import java.lang.System + |import java.nio.file.Files + |import java.nio.file.Paths + | + |plugins { + | id("org.jetbrains.kotlin.jvm") version "$kotlinVersion" + | application + |} + | + |repositories { + | mavenLocal() + | mavenCentral() + |${createGradleRepositoriesSection(script.repositories).prependIndent()} + |} + | + |tasks.jar { + | manifest { + | attributes["Main-Class"] = "$capsuleApp" + | } + | baseName = "$baseName" + | configurations["compileClasspath"].forEach { file: File -> + | from(zipTree(file.absoluteFile)) + | } + | duplicatesStrategy = DuplicatesStrategy.INCLUDE + |} + | + |tasks.register("makeScript") { + | dependsOn(":jar") + | doLast { + | val headerDir = layout.projectDirectory.toString() + | val jarFileName = layout.buildDirectory.file("libs/$baseName.jar").get().toString() + | val outFileName = layout.buildDirectory.file("libs/$baseName").get().toString() + | val lineSeparator = System.getProperty("line.separator").encodeToByteArray() + | val headerPath = Paths.get(headerDir).resolve("exec_header.sh") + | val headerBytes = Files.readAllBytes(headerPath) + | val jarBytes = Files.readAllBytes(Paths.get(jarFileName)) + | val outFile = Paths.get(outFileName).toFile() + | val fileStream = FileOutputStream(outFile) + | + | fileStream.write(headerBytes) + | fileStream.write(lineSeparator) + | fileStream.write(jarBytes) + | fileStream.close() + | } + |} + | + |dependencies { + | implementation(files("${jarArtifact.path.stringPath().replace("\\", "\\\\")}")) + |${createGradleDependenciesSection(extendedDependencies).prependIndent()} + |} + | + |$kotlinOptions + """.trimStart().trimMargin() } private fun createGradleRepositoryCredentials(repository: Repository): String { if (repository.user.isNotBlank() && repository.password.isNotBlank()) { - return """ - credentials { - username = "${repository.user}" - password = "${repository.password}" - } - """.trimIndent() + return """|credentials { + | username = "${repository.user}" + | password = "${repository.password}" + |}""".trimMargin() } return "" @@ -106,36 +122,37 @@ object GradleTemplates { } private fun createGradleRepositoriesSection(repositories: Set) = repositories.joinToString("\n") { - """ - maven { - url "${it.url}" - ${createGradleRepositoryCredentials(it).prependIndent()} - } - """.trimIndent() + """|maven { + | url = uri("${it.url}") + |${createGradleRepositoryCredentials(it).prependIndent()} + |} + """.trimMargin() } - private fun kotlinOptions(compilerOpts: Set): String { - val opts = compilerOpts.map { it.value } - - var jvmTargetOption: String? = null - for (i in opts.indices) { - if (i > 0 && opts[i - 1] == "-jvm-target") { - jvmTargetOption = opts[i] - } + private fun createCompilerOptionsSection(compilerOpts: Set): String { + if (compilerOpts.isEmpty()) { + return "" } - val kotlinOpts = if (jvmTargetOption != null) { - """ - tasks.withType { - kotlinOptions { - jvmTarget = "$jvmTargetOption" + var jvmTarget = "" + val freeCompilerArgs = mutableListOf() + + for (opt in compilerOpts) { + when { + opt.value.startsWith("-jvm-target") -> { + jvmTarget = "jvmTarget = \"" + opt.value.drop(11).trim() + "\"" + } + else -> { + freeCompilerArgs.add(opt.value) } } - """.trimIndent() - } else { - "" } - return kotlinOpts + return """|tasks.withType { + | kotlinOptions { + | $jvmTarget + | freeCompilerArgs = listOf(${freeCompilerArgs.joinToString(", ") { "\"$it\"" }}) + | } + |}""".trimMargin() } } diff --git a/src/main/kotlin/kscript/app/code/Templates.kt b/src/main/kotlin/kscript/app/code/Templates.kt index 2235901b7..368799a84 100644 --- a/src/main/kotlin/kscript/app/code/Templates.kt +++ b/src/main/kotlin/kscript/app/code/Templates.kt @@ -1,119 +1,125 @@ package kscript.app.code +import kscript.app.model.KotlinOpt import kscript.app.model.PackageName -import kscript.app.util.ScriptUtils.dropExtension +import kscript.app.model.ScriptType import org.intellij.lang.annotations.Language object Templates { @Language("sh") val bootstrapHeader = """ - #!/bin/bash - - //usr/bin/env echo ' - /**** BOOTSTRAP kscript ****\'>/dev/null - command -v kscript >/dev/null 2>&1 || source /dev/stdin <<< "${'$'}(curl -L https://git.io/fpF1K)" - exec kscript $0 "$@" - \*** IMPORTANT: Any code including imports and annotations must come after this line ***/ - - """.trimIndent() + |#!/bin/bash + | + |//usr/bin/env echo ' + |/**** BOOTSTRAP kscript ****\'>/dev/null + |command -v kscript >/dev/null 2>&1 || source /dev/stdin <<< "${'$'}(curl -L https://git.io/fpF1K)" + |exec kscript $0 "$@" + |\*** IMPORTANT: Any code including imports and annotations must come after this line ***/ + | + |""".trimStart().trimMargin() val textProcessingPreamble = """ - //DEPS com.github.holgerbrandl:kscript-support-api:1.2.5 + |@file:DependsOn("com.github.holgerbrandl:kscript-support-api:1.2.5") + | + |import kscript.text.* + |val lines = resolveArgFile(args) + | + |""".trimStart().trimMargin() + + fun createExecuteHeader(kotlinOpts: Set): String { + val options = mutableListOf("") + + for (opt in kotlinOpts) { + val s = opt.value + if (s.startsWith("-J")) { + options.add(s.substring(2)) + } + } - import kscript.text.* - val lines = resolveArgFile(args) - - """.trimIndent() + val opts = options.joinToString(" ").trim() - val executeHeader = """ - #!/usr/bin/env bash - exec java -jar ${'$'}0 "${'$'}@" - """.trimIndent() + return """ + |#!/usr/bin/env bash + |exec java $opts -jar ${'$'}0 "${'$'}@" + """.trimStart().trimMargin() + } - fun wrapperForScript(packageName: PackageName, className: String): String { + fun createWrapperForScript(packageName: PackageName, className: String): String { val classReference = packageName.value + "." + className return """ - class Main_${className}{ - companion object { - @JvmStatic - fun main(args: Array) { - val script = Main_${className}::class.java.classLoader.loadClass("$classReference") - script.getDeclaredConstructor(Array::class.java).newInstance(args); - } - } - } - """.trimIndent() + |class Main_${className}{ + | companion object { + | @JvmStatic + | fun main(args: Array) { + | val script = Main_${className}::class.java.classLoader.loadClass("$classReference") + | script.getDeclaredConstructor(Array::class.java).newInstance(args); + | } + | } + |}""".trimStart().trimMargin() } - fun runConfig(rootNode: String, userArgs: List): String { - val fileNameWithoutExtension = rootNode.dropExtension() - - val runConfigurationBody = if (rootNode.endsWith(".kt")) { - """ - - - - """.trimIndent() - } else { - """ - - - """.trimIndent() + fun createRunConfig(rootScriptName: String, rootScriptType: ScriptType, userArgs: List): String { + val rootFileName = rootScriptName + rootScriptType.extension + val userArgsString = userArgs.joinToString(" ") { it } + + if (rootScriptType == ScriptType.KT) { + return """ + | + | + | + | + | + |""".trimStart().trimMargin() } + // This is Kotlin scripting configuration (other possible options: ShConfigurationType (linux), BatchConfigurationType (windows)) return """ - - $runConfigurationBody - - """.trimIndent() + | + | + | + | + | + |""".trimStart().trimMargin() } - fun usageOptions(selfName: String, version: String) = """ - $selfName - Enhanced scripting support for Kotlin on *nix-based systems. - - Usage: - $selfName [options]