diff --git a/.circleci/config.yml b/.circleci/config.yml
index a7cb1471..d41b04e5 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -12,6 +12,7 @@ jobs:
environment:
LEIN_ROOT: "true"
BABASHKA_PLATFORM: linux # could be used in jar name
+ resource_class: large
steps:
- checkout
- run:
@@ -67,9 +68,11 @@ jobs:
working_directory: ~/repo
environment:
LEIN_ROOT: "true"
- GRAALVM_HOME: /home/circleci/graalvm-ce-java8-19.3.0
+ GRAALVM_HOME: /home/circleci/graalvm-ce-java8-19.3.1
BABASHKA_PLATFORM: linux # used in release script
BABASHKA_TEST_ENV: native
+ BABASHKA_XMX: "-J-Xmx7g"
+ resource_class: large
steps:
- checkout
- run:
@@ -99,14 +102,10 @@ jobs:
name: Download GraalVM
command: |
cd ~
- if ! [ -d graalvm-ce-java8-19.3.0 ]; then
- curl -O -sL https://github.com/graalvm/graalvm-ce-builds/releases/download/vm-19.3.0/graalvm-ce-java8-linux-amd64-19.3.0.tar.gz
- tar xzf graalvm-ce-java8-linux-amd64-19.3.0.tar.gz
+ if ! [ -d graalvm-ce-java8-19.3.1 ]; then
+ curl -O -sL https://github.com/graalvm/graalvm-ce-builds/releases/download/vm-19.3.1/graalvm-ce-java8-linux-amd64-19.3.1.tar.gz
+ tar xzf graalvm-ce-java8-linux-amd64-19.3.1.tar.gz
fi
- # - run:
- # name: Install GraalVM SSL libs
- # command: |
- # .circleci/script/graalvm_ssl
- run:
name: Build binary
command: |
@@ -117,10 +116,6 @@ jobs:
command: |
script/test
script/run_lib_tests
- # - run:
- # name: Performance report
- # command: |
- # .circleci/script/performance
- run:
name: Release
command: |
@@ -128,7 +123,78 @@ jobs:
- save_cache:
paths:
- ~/.m2
- - ~/graalvm-ce-java8-19.3.0
+ - ~/graalvm-ce-java8-19.3.1
+ key: linux-{{ checksum "project.clj" }}-{{ checksum ".circleci/config.yml" }}
+ - store_artifacts:
+ path: /tmp/release
+ destination: release
+ - run:
+ name: Publish artifact link to Slack
+ command: |
+ ./bb .circleci/script/publish_artifact.clj
+ linux-static:
+ docker:
+ - image: circleci/clojure:lein-2.8.1
+ working_directory: ~/repo
+ environment:
+ LEIN_ROOT: "true"
+ GRAALVM_HOME: /home/circleci/graalvm-ce-java8-19.3.1
+ BABASHKA_PLATFORM: linux-static # used in release script
+ BABASHKA_TEST_ENV: native
+ BABASHKA_STATIC: true
+ BABASHKA_XMX: "-J-Xmx7g"
+ resource_class: large
+ steps:
+ - checkout
+ - run:
+ name: "Pull Submodules"
+ command: |
+ git submodule init
+ git submodule update
+ - restore_cache:
+ keys:
+ - linux-{{ checksum "project.clj" }}-{{ checksum ".circleci/config.yml" }}
+ - run:
+ name: Install Clojure
+ command: |
+ wget https://download.clojure.org/install/linux-install-1.10.1.447.sh
+ chmod +x linux-install-1.10.1.447.sh
+ sudo ./linux-install-1.10.1.447.sh
+ - run:
+ name: Install lsof
+ command: |
+ sudo apt-get install lsof
+ - run:
+ name: Install native dev tools
+ command: |
+ sudo apt-get update
+ sudo apt-get -y install gcc g++ zlib1g-dev
+ - run:
+ name: Download GraalVM
+ command: |
+ cd ~
+ if ! [ -d graalvm-ce-java8-19.3.1 ]; then
+ curl -O -sL https://github.com/graalvm/graalvm-ce-builds/releases/download/vm-19.3.1/graalvm-ce-java8-linux-amd64-19.3.1.tar.gz
+ tar xzf graalvm-ce-java8-linux-amd64-19.3.1.tar.gz
+ fi
+ - run:
+ name: Build binary
+ command: |
+ script/compile
+ no_output_timeout: 30m
+ - run:
+ name: Run tests
+ command: |
+ script/test
+ script/run_lib_tests
+ - run:
+ name: Release
+ command: |
+ .circleci/script/release
+ - save_cache:
+ paths:
+ - ~/.m2
+ - ~/graalvm-ce-java8-19.3.1
key: linux-{{ checksum "project.clj" }}-{{ checksum ".circleci/config.yml" }}
- store_artifacts:
path: /tmp/release
@@ -141,9 +207,11 @@ jobs:
macos:
xcode: "9.0"
environment:
- GRAALVM_HOME: /Users/distiller/graalvm-ce-java8-19.3.0/Contents/Home
+ GRAALVM_HOME: /Users/distiller/graalvm-ce-java8-19.3.1/Contents/Home
BABASHKA_PLATFORM: macos # used in release script
BABASHKA_TEST_ENV: native
+ BABASHKA_XMX: "-J-Xmx7g"
+ resource_class: large
steps:
- checkout
- run:
@@ -157,24 +225,20 @@ jobs:
- run:
name: Install Clojure
command: |
- .circleci/script/install-clojure /usr/local
+ script/install-clojure /usr/local
- run:
name: Install Leiningen
command: |
- .circleci/script/install-leiningen
+ script/install-leiningen
- run:
name: Download GraalVM
command: |
cd ~
ls -la
- if ! [ -d graalvm-ce-java8-19.3.0 ]; then
- curl -O -sL https://github.com/graalvm/graalvm-ce-builds/releases/download/vm-19.3.0/graalvm-ce-java8-darwin-amd64-19.3.0.tar.gz
- tar xzf graalvm-ce-java8-darwin-amd64-19.3.0.tar.gz
+ if ! [ -d graalvm-ce-java8-19.3.1 ]; then
+ curl -O -sL https://github.com/graalvm/graalvm-ce-builds/releases/download/vm-19.3.1/graalvm-ce-java8-darwin-amd64-19.3.1.tar.gz
+ tar xzf graalvm-ce-java8-darwin-amd64-19.3.1.tar.gz
fi
- # - run:
- # name: Install GraalVM SSL libs
- # command: |
- # .circleci/script/graalvm_ssl
- run:
name: Build binary
command: |
@@ -185,10 +249,6 @@ jobs:
command: |
script/test
script/run_lib_tests
- # - run:
- # name: Performance report
- # command: |
- # .circleci/script/performance
- run:
name: Release
command: |
@@ -196,7 +256,7 @@ jobs:
- save_cache:
paths:
- ~/.m2
- - ~/graalvm-ce-java8-19.3.0
+ - ~/graalvm-ce-java8-19.3.1
key: mac-{{ checksum "project.clj" }}-{{ checksum ".circleci/config.yml" }}
- store_artifacts:
path: /tmp/release
@@ -206,6 +266,7 @@ jobs:
command: |
./bb .circleci/script/publish_artifact.clj
deploy:
+ resource_class: large
docker:
- image: circleci/clojure:lein-2.8.1
working_directory: ~/repo
@@ -229,6 +290,7 @@ jobs:
- ~/.m2
key: v1-dependencies-{{ checksum "project.clj" }}
docker:
+ resource_class: large
docker:
- image: circleci/buildpack-deps:stretch
steps:
@@ -249,6 +311,7 @@ workflows:
jobs:
- jvm
- linux
+ - linux-static
- mac
- deploy:
filters:
@@ -257,6 +320,7 @@ workflows:
requires:
- jvm
- linux
+ - linux-static
- mac
- docker:
filters:
@@ -265,4 +329,5 @@ workflows:
requires:
- jvm
- linux
+ - linux-static
- mac
diff --git a/.circleci/script/docker b/.circleci/script/docker
index d2bda10e..d7410e0a 100755
--- a/.circleci/script/docker
+++ b/.circleci/script/docker
@@ -17,7 +17,7 @@ fi
if [ -z "$CIRCLE_PULL_REQUEST" ] && [ "$CIRCLE_BRANCH" = "master" ]; then
echo "Building Docker image $image_name:$image_tag"
echo "$DOCKERHUB_PASS" | docker login -u "$DOCKERHUB_USER" --password-stdin
- docker build -t "$image_name" .
+ docker build -t "$image_name" --build-arg BABASHKA_XMX="-J-Xmx6900m" .
docker tag "$image_name:$latest_tag" "$image_name:$image_tag"
# we only update latest when it's not a SNAPSHOT version
if [ "false" = "$snapshot" ]; then
diff --git a/.circleci/script/install-leiningen b/.circleci/script/install-leiningen
deleted file mode 100755
index ff5f8235..00000000
--- a/.circleci/script/install-leiningen
+++ /dev/null
@@ -1,6 +0,0 @@
-#!/usr/bin/env bash
-
-curl https://raw.githubusercontent.com/technomancy/leiningen/stable/bin/lein > lein
-sudo mkdir -p /usr/local/bin/
-sudo mv lein /usr/local/bin/lein
-sudo chmod a+x /usr/local/bin/lein
diff --git a/.circleci/script/publish_artifact.clj b/.circleci/script/publish_artifact.clj
index 1b5f5b74..6b0a75e9 100755
--- a/.circleci/script/publish_artifact.clj
+++ b/.circleci/script/publish_artifact.clj
@@ -1,23 +1,35 @@
-(require '[clojure.java.shell :refer [sh]]
+(require '[cheshire.core :refer [generate-string]]
'[clojure.java.io :as io]
- '[cheshire.core :refer [generate-string]]
+ '[clojure.java.shell :refer [sh]]
'[clojure.string :as str])
(def channel "#babashka_circleci_builds")
#_(def channel "#_test")
(def babashka-version (str/trim (slurp (io/file "resources" "BABASHKA_VERSION"))))
-
-(def text (format "[%s - %s@%s]: https://%s-201467090-gh.circle-artifacts.com/0/release/babashka-%s-%s-amd64.zip"
- (System/getenv "BABASHKA_PLATFORM")
- (System/getenv "CIRCLE_BRANCH")
- (System/getenv "CIRCLE_SHA1")
- (System/getenv "CIRCLE_BUILD_NUM")
- babashka-version
- (System/getenv "BABASHKA_PLATFORM")))
-
(def slack-hook-url (System/getenv "SLACK_HOOK_URL"))
-(when slack-hook-url
- (let [json (generate-string {:username "borkdude"
- :channel channel
- :text text})]
- (sh "curl" "-X" "POST" "-H" "Content-Type: application/json" "-d" json slack-hook-url)))
+
+(defn slack! [text]
+ (when slack-hook-url
+ (let [json (generate-string {:username "borkdude"
+ :channel channel
+ :text text})]
+ (sh "curl" "-X" "POST" "-H" "Content-Type: application/json" "-d" json slack-hook-url))))
+
+(def release-text (format "[%s - %s@%s]: https://%s-201467090-gh.circle-artifacts.com/0/release/babashka-%s-%s-amd64.zip"
+ (System/getenv "BABASHKA_PLATFORM")
+ (System/getenv "CIRCLE_BRANCH")
+ (System/getenv "CIRCLE_SHA1")
+ (System/getenv "CIRCLE_BUILD_NUM")
+ babashka-version
+ (System/getenv "BABASHKA_PLATFORM")))
+
+(slack! release-text)
+
+(def binary-size-text
+ (format "[%s - %s@%s] binary size: %s"
+ (System/getenv "BABASHKA_PLATFORM")
+ (System/getenv "CIRCLE_BRANCH")
+ (System/getenv "CIRCLE_SHA1")
+ (slurp (io/file "/tmp/bb_size/size"))))
+
+(slack! binary-size-text)
diff --git a/.circleci/script/release b/.circleci/script/release
index 2d32f514..5e6325bf 100755
--- a/.circleci/script/release
+++ b/.circleci/script/release
@@ -8,6 +8,8 @@ cp bb /tmp/release
VERSION=$(cat resources/BABASHKA_VERSION)
cd /tmp/release
+mkdir -p /tmp/bb_size
+./bb '(spit "/tmp/bb_size/size" (.length (io/file "bb")))'
## release binary as zip archive
diff --git a/.clj-kondo/config.edn b/.clj-kondo/config.edn
index e4db19e4..cba99996 100644
--- a/.clj-kondo/config.edn
+++ b/.clj-kondo/config.edn
@@ -4,4 +4,4 @@
babashka.impl.File/gen-wrapper-fn-2 clojure.core/def
babashka.impl.Pattern/gen-wrapper-fn-2 clojure.core/def
babashka.impl.Pattern/gen-constants clojure.core/declare}
- :linters {:unsorted-namespaces {:level :warning}}}
+ :linters {:unsorted-required-namespaces {:level :warning}}}
diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 00000000..eefa27e4
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,18 @@
+.circleci/
+.git/
+.clj-kondo/
+.github/
+doc/
+examples/
+logo/
+test-resources/
+test/
+.gitignore
+.carve_ignore
+.gitmodules
+appveyor.yml
+CHANGES.md
+deps.edn
+Dockerfile
+LICENSE
+README.md
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
index c7fbd994..2ff623c6 100644
--- a/.github/FUNDING.yml
+++ b/.github/FUNDING.yml
@@ -2,7 +2,7 @@
github: borkdude # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: borkdude
-open_collective: # Replace with a single Open Collective username
+open_collective: babashka
ko_fi: borkdude
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
diff --git a/.github/script/deploy b/.github/script/deploy
new file mode 100755
index 00000000..31c8a147
--- /dev/null
+++ b/.github/script/deploy
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+
+if [ -z "$GITHUB_HEAD_REF" ] && [ "${GITHUB_REF##*/}" = "master" ]
+then
+ lein deploy clojars
+fi
+
+exit 0;
diff --git a/.github/script/docker b/.github/script/docker
new file mode 100755
index 00000000..a0121d94
--- /dev/null
+++ b/.github/script/docker
@@ -0,0 +1,36 @@
+#!/usr/bin/env bash
+
+set -eo pipefail
+
+image_name="borkdude/babashka"
+image_tag=$(cat resources/BABASHKA_VERSION)
+latest_tag="latest"
+
+if [[ $image_tag =~ SNAPSHOT$ ]]
+then
+ echo "This is a snapshot version"
+ snapshot="true"
+else
+ echo "This is a non-snapshot version"
+ snapshot="false"
+fi
+
+if [ -z "$GITHUB_HEAD_REF" ] && [ "${GITHUB_REF##*/}" = "master" ]
+then
+ echo "Building Docker image $image_name:$image_tag"
+ echo "$DOCKERHUB_PASS" | docker login -u "$DOCKERHUB_USER" --password-stdin
+ docker build -t "$image_name" .
+ docker tag "$image_name:$latest_tag" "$image_name:$image_tag"
+ # we only update latest when it's not a SNAPSHOT version
+ if [ "false" = "$snapshot" ]; then
+ echo "Pushing image $image_name:$latest_tag"
+ docker push "$image_name:$latest_tag"
+ fi
+ # we update the version tag, even if it's a SNAPSHOT version
+ echo "Pushing image $image_name:$image_tag"
+ docker push "$image_name:$image_tag"
+else
+ echo "Not publishing Docker image"
+fi
+
+exit 0;
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 00000000..ae444cf3
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,345 @@
+name: build
+
+on: [push
+ , pull_request
+ ]
+
+jobs:
+
+ scratch:
+ runs-on: ubuntu-18.04
+ steps:
+ - name: Git checkout
+ uses: actions/checkout@v1
+ with:
+ fetch-depth: 1
+ submodules: 'true'
+
+ - name: Scratch
+ run: |
+ echo "Scratch"
+
+ jvm:
+ # ubuntu 18.04 comes with lein + java8 installed
+ runs-on: ubuntu-18.04
+ steps:
+ - name: Git checkout
+ uses: actions/checkout@v1
+ with:
+ fetch-depth: 1
+ submodules: 'true'
+
+ - name: Cache deps
+ uses: actions/cache@v1
+ id: cache-deps
+ with:
+ path: ~/.m2/repository
+ key: ${{ runner.os }}-maven-${{ hashFiles('project.clj') }}
+ restore-keys: |
+ ${{ runner.os }}-maven-
+
+ - name: Fetch deps
+ if: steps.cache-deps.outputs.cache-hit != 'true'
+ run: |
+ lein deps
+
+ - name: Run tests
+ run: |
+ script/test
+
+ - name: Test libraries
+ run: |
+ sudo script/install-clojure
+ script/run_lib_tests
+
+ - name: Build uberjar
+ run: |
+ lein with-profiles +reflection do run
+ lein do clean, uberjar
+
+ - name: Babashka version
+ id: babashka-version
+ run: |
+ BABASHKA_VERSION=$(cat resources/BABASHKA_VERSION)
+ echo "##[set-output name=version;]${BABASHKA_VERSION}"
+
+ - name: Reflection artifact
+ run: |
+ cp reflection.json babashka-${{ steps.babashka-version.outputs.version }}-reflection.json
+
+ - uses: actions/upload-artifact@v1
+ with:
+ name: jar
+ path: target/babashka-${{ steps.babashka-version.outputs.version }}-standalone.jar
+
+ - uses: actions/upload-artifact@v1
+ with:
+ name: reflection.json
+ path: babashka-${{ steps.babashka-version.outputs.version }}-reflection.json
+
+ linux:
+ needs: [jvm]
+ runs-on: ubuntu-18.04
+ steps:
+ - name: Git checkout
+ uses: actions/checkout@v1
+ with:
+ fetch-depth: 1
+ submodules: 'true'
+
+ - uses: actions/download-artifact@v1
+ with:
+ name: jar
+ path: .
+
+ - uses: actions/download-artifact@v1
+ with:
+ name: reflection.json
+ path: .
+
+ - name: Cache deps
+ uses: actions/cache@v1
+ id: cache-deps
+ with:
+ path: ~/.m2/repository
+ key: ${{ runner.os }}-maven-${{ hashFiles('project.clj') }}
+ restore-keys: |
+ ${{ runner.os }}-maven-
+
+ - name: Cache GraalVM
+ uses: actions/cache@v1
+ id: cache-graalvm
+ with:
+ path: ~/graalvm-ce-java8-19.3.1
+ key: ${{ runner.os }}-graalvm-19.3.1
+ restore-keys: |
+ ${{ runner.os }}-graalvm-19.3.1
+
+ - name: Download GraalVM
+ run: |
+ cd ~
+ if ! [ -d graalvm-ce-java8-19.3.1 ]; then
+ curl -O -sL https://github.com/graalvm/graalvm-ce-builds/releases/download/vm-19.3.1/graalvm-ce-java8-linux-amd64-19.3.1.tar.gz
+ tar xzf graalvm-ce-java8-linux-amd64-19.3.1.tar.gz
+ fi
+
+ - name: Babashka version
+ id: babashka-version
+ run: |
+ BABASHKA_VERSION=$(cat resources/BABASHKA_VERSION)
+ echo "##[set-output name=version;]${BABASHKA_VERSION}"
+
+ - name: Build Linux native image
+ run: |
+ export BABASHKA_JAR=babashka-${{ steps.babashka-version.outputs.version }}-standalone.jar
+ export BABASHKA_XMX="-J-Xmx6g"
+ export GRAALVM_HOME="$HOME/graalvm-ce-java8-19.3.1"
+ cp babashka-${{ steps.babashka-version.outputs.version }}-reflection.json reflection.json
+ script/compile
+
+ - name: Test binary
+ run: |
+ BABASHKA_TEST_ENV=native script/test
+
+ - name: Install clojure
+ run: |
+ sudo script/install-clojure /usr/local
+
+ - name: Test libraries
+ run: |
+ BABASHKA_TEST_ENV=native script/run_lib_tests
+
+ - uses: actions/upload-artifact@v1
+ with:
+ path: bb
+ name: babashka-${{ steps.babashka-version.outputs.version }}-linux-amd64.zip
+
+ linux-static:
+ needs: [jvm]
+ runs-on: ubuntu-16.04
+ steps:
+ - name: Git checkout
+ uses: actions/checkout@v1
+ with:
+ fetch-depth: 1
+ submodules: 'true'
+
+ - uses: actions/download-artifact@v1
+ with:
+ name: jar
+ path: .
+
+ - uses: actions/download-artifact@v1
+ with:
+ name: reflection.json
+ path: .
+
+ - name: Cache deps
+ uses: actions/cache@v1
+ id: cache-deps
+ with:
+ path: ~/.m2/repository
+ key: ${{ runner.os }}-maven-${{ hashFiles('project.clj') }}
+ restore-keys: |
+ ${{ runner.os }}-maven-
+
+ - name: Cache GraalVM
+ uses: actions/cache@v1
+ id: cache-graalvm
+ with:
+ path: ~/graalvm-ce-java8-19.3.1
+ key: ${{ runner.os }}-graalvm-19.3.1
+ restore-keys: |
+ ${{ runner.os }}-graalvm-19.3.1
+
+ - name: Download GraalVM
+ run: |
+ cd ~
+ if ! [ -d graalvm-ce-java8-19.3.1 ]; then
+ curl -O -sL https://github.com/graalvm/graalvm-ce-builds/releases/download/vm-19.3.1/graalvm-ce-java8-linux-amd64-19.3.1.tar.gz
+ tar xzf graalvm-ce-java8-linux-amd64-19.3.1.tar.gz
+ fi
+
+ - name: Babashka version
+ id: babashka-version
+ run: |
+ BABASHKA_VERSION=$(cat resources/BABASHKA_VERSION)
+ echo "##[set-output name=version;]${BABASHKA_VERSION}"
+
+ - name: Build Linux native image
+ run: |
+ export BABASHKA_JAR=babashka-${{ steps.babashka-version.outputs.version }}-standalone.jar
+ export BABASHKA_XMX="-J-Xmx6g"
+ export GRAALVM_HOME="$HOME/graalvm-ce-java8-19.3.1"
+ export BABASHKA_STATIC=true
+ cp babashka-${{ steps.babashka-version.outputs.version }}-reflection.json reflection.json
+ script/compile
+
+ - name: Test binary
+ run: |
+ ./bb '(+ 1 2 3)'
+ BABASHKA_TEST_ENV=native script/test
+
+ - name: Install clojure
+ run: |
+ sudo script/install-clojure
+
+ - name: Test libraries
+ run: |
+ BABASHKA_TEST_ENV=native script/run_lib_tests
+
+ - uses: actions/upload-artifact@v1
+ with:
+ path: bb
+ name: babashka-${{ steps.babashka-version.outputs.version }}-linux-static-amd64.zip
+
+ mac:
+ needs: [jvm]
+ runs-on: macOS-latest
+ steps:
+ - name: Git checkout
+ uses: actions/checkout@v1
+ with:
+ fetch-depth: 1
+ submodules: 'true'
+
+ - uses: actions/download-artifact@v1
+ with:
+ name: jar
+ path: .
+
+ - uses: actions/download-artifact@v1
+ with:
+ name: reflection.json
+ path: .
+
+ - name: Cache GraalVM
+ uses: actions/cache@v1
+ id: cache-graalvm
+ with:
+ path: ~/graalvm-ce-java8-19.3.1
+ key: ${{ runner.os }}-graalvm-19.3.1
+ restore-keys: |
+ ${{ runner.os }}-graalvm-19.3.1
+
+ - name: Download GraalVM
+ run: |
+ cd ~
+ if ! [ -d graalvm-ce-java8-19.3.1 ]; then
+ curl -O -sL https://github.com/graalvm/graalvm-ce-builds/releases/download/vm-19.3.1/graalvm-ce-java8-darwin-amd64-19.3.1.tar.gz
+ tar xzf graalvm-ce-java8-darwin-amd64-19.3.1.tar.gz
+ fi
+
+ - name: Babashka version
+ id: babashka-version
+ run: |
+ BABASHKA_VERSION=$(cat resources/BABASHKA_VERSION)
+ echo "##[set-output name=version;]${BABASHKA_VERSION}"
+
+ - name: Build macOS native image
+ run: |
+ export BABASHKA_JAR=babashka-${{ steps.babashka-version.outputs.version }}-standalone.jar
+ export BABASHKA_XMX="-J-Xmx6g"
+ export GRAALVM_HOME="$HOME/graalvm-ce-java8-19.3.1/Contents/Home"
+ cp babashka-${{ steps.babashka-version.outputs.version }}-reflection.json reflection.json
+ script/compile
+
+ - name: Test binary
+ run: |
+ sudo script/install-leiningen
+ BABASHKA_TEST_ENV=native script/test
+
+ - name: Test libraries
+ run: |
+ sudo script/install-clojure
+ BABASHKA_TEST_ENV=native script/run_lib_tests
+
+ - uses: actions/upload-artifact@v1
+ with:
+ path: bb
+ name: babashka-${{ steps.babashka-version.outputs.version }}-macos-amd64.zip
+
+ deploy:
+ needs: [jvm, linux, linux-static, mac]
+ if: github.event_name == 'push' && github.ref == 'refs/heads/master'
+ runs-on: ubuntu-18.04
+ steps:
+ - name: Git checkout
+ uses: actions/checkout@v1
+ with:
+ fetch-depth: 1
+ submodules: 'true'
+
+ - name: Cache deps
+ uses: actions/cache@v1
+ id: cache-deps
+ with:
+ path: ~/.m2/repository
+ key: ${{ runner.os }}-maven-${{ hashFiles('project.clj') }}
+ restore-keys: |
+ ${{ runner.os }}-maven-
+
+ - name: Deploy
+ env:
+ CLOJARS_USER: "${{ secrets.CLOJARS_USER }}"
+ CLOJARS_PASS: "${{ secrets.CLOJARS_PASS }}"
+ run: |
+ .github/script/deploy
+
+ docker:
+ needs: [jvm, linux, linux-static, mac]
+ if: github.event_name == 'push' && github.ref == 'refs/heads/master'
+ runs-on: ubuntu-18.04
+ steps:
+ - name: Git checkout
+ uses: actions/checkout@v1
+ with:
+ fetch-depth: 1
+ submodules: 'true'
+
+ - name: Docker build
+ env:
+ DOCKERHUB_USER: "${{ secrets.DOCKERHUB_USER }}"
+ DOCKERHUB_PASS: "${{ secrets.DOCKERHUB_PASS }}"
+ run: |
+ .github/script/docker
diff --git a/.gitignore b/.gitignore
index d8a7040d..1b98c17b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,4 +15,6 @@ pom.xml.asc
!java/src/babashka/impl/LockFix.class
!test-resources/babashka/src_for_classpath_test/foo.jar
.cpcache
-reflection.json
+*reflection.json
+/tmp
+/reports
diff --git a/.gitmodules b/.gitmodules
index 7c3bc0cf..6bf5f5fe 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -2,3 +2,6 @@
path = sci
url = https://github.com/borkdude/sci
branch = master
+[submodule "babashka.curl"]
+ path = babashka.curl
+ url = https://github.com/borkdude/babashka.curl
diff --git a/CHANGES.md b/CHANGES.md
index fff6f671..e3e48050 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -2,13 +2,18 @@
## Breaking changes
+## v0.0.79
+- [babashka.curl#9](https://github.com/borkdude/babashka.curl/issues/9):
+ BREAKING! Functions in `babashka.curl` like `get`, `post`, etc. now always
+ return a map with `:status`, `:body`, and `:headers`.
+
## v0.0.71
- #267 Change behavior of reader conditionals: the `:clj` branch is taken when
it occurs before a `:bb` branch.
## v0.0.44 - 0.0.45
- #173: Rename `*in*` to `*input*` (in the `user` namespace). The reason for
- this is that itt shadowed `clojure.core/*in*` when used unqualified.
+ this is that it shadowed `clojure.core/*in*` when used unqualified.
## v0.0.43
- #160: Add support for `java.lang.ProcessBuilder`. See docs. This replaces the
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 00000000..0a177eb9
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1 @@
+See [doc/dev.md](doc/dev.md).
diff --git a/Dockerfile b/Dockerfile
index 267c3304..97b44d1d 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,20 +1,25 @@
-FROM ubuntu AS BASE
+FROM clojure:lein-2.9.1 AS BASE
+ARG BABASHKA_XMX="-J-Xmx3g"
-RUN apt-get update
-RUN apt-get install -yy curl unzip build-essential zlib1g-dev
+RUN apt update
+RUN apt install --no-install-recommends -yy curl unzip build-essential zlib1g-dev
WORKDIR "/opt"
RUN curl -sLO https://github.com/graalvm/graalvm-ce-builds/releases/download/vm-19.3.1/graalvm-ce-java8-linux-amd64-19.3.1.tar.gz
RUN tar -xzf graalvm-ce-java8-linux-amd64-19.3.1.tar.gz
+
ENV GRAALVM_HOME="/opt/graalvm-ce-java8-19.3.1"
ENV JAVA_HOME="/opt/graalvm-ce-java8-19.3.1/bin"
ENV PATH="$PATH:$JAVA_HOME"
+ENV BABASHKA_STATIC="true"
+ENV BABASHKA_XMX=$BABASHKA_XMX
+
COPY . .
-RUN apt install -y sudo
-RUN ./.circleci/script/install-leiningen
RUN ./script/compile
-RUN cp bb /usr/local/bin
-FROM ubuntu:bionic
-COPY --from=BASE /usr/local/bin/bb /usr/local/bin
+FROM alpine:latest
+
+RUN apk add --no-cache curl
+RUN mkdir -p /usr/local/bin
+COPY --from=BASE /opt/bb /usr/local/bin/bb
CMD ["bb"]
diff --git a/README.md b/README.md
index 00c3e269..595e83c9 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,8 @@
[](https://circleci.com/gh/borkdude/babashka/tree/master)
-[](https://clojars.org/borkdude/babashka)
[](https://app.slack.com/client/T03RZGPFR/CLX41ASCS)
-
-
+[](https://opencollective.com/babashka) [](https://clojars.org/borkdude/babashka)
A Clojure [babushka](https://en.wikipedia.org/wiki/Headscarf) for the grey areas of Bash.
@@ -14,80 +12,106 @@ A Clojure [babushka](https://en.wikipedia.org/wiki/Headscarf) for the grey areas
@laheadle on Clojurians Slack
-## Quickstart
+## Introduction
-``` shellsession
-$ bash <(curl -s https://raw.githubusercontent.com/borkdude/babashka/master/install)
-$ ls | bb --time -i '(filter #(-> % io/file .isDirectory) *input*)'
-("doc" "resources" "sci" "script" "src" "target" "test")
-bb took 4ms.
-```
-
-## Rationale
-
-The sweet spot for babashka is executing Clojure snippets or scripts in the same
-space where you would use Bash.
+The main idea behind babashka is to leverage Clojure in places where you would
+be using bash otherwise.
As one user described it:
> I’m quite at home in Bash most of the time, but there’s a substantial grey area of things that are too complicated to be simple in bash, but too simple to be worth writing a clj/s script for. Babashka really seems to hit the sweet spot for those cases.
-Goals:
+### Goals
-* Fast startup / low latency. This is achieved by compiling to native using [GraalVM](https://github.com/oracle/graal).
-* Familiarity and portability. Keep migration barriers between bash and Clojure as low as possible by:
- - Gradually introducing Clojure expressions to existing bash scripts
- - Scripts written in babashka should also be able to run on the JVM without major changes.
-* Multi-threading support similar to Clojure on the JVM
-* Batteries included (clojure.tools.cli, core.async, ...)
+* Low latency Clojure scripting alternative to JVM Clojure.
+* Easy installation: grab the self-contained binary and run. No JVM needed.
+* Familiarity and portability:
+ - Scripts should be compatible with JVM Clojure as much as possible
+ - Scripts should be platform-independent as much as possible. Babashka offers
+ support for linux, macOS and Windows.
+* Allow interop with commonly used classes like `java.io.File` and `System`
+* Multi-threading support (`pmap`, `future`, `core.async`)
+* Batteries included (tools.cli, cheshire, ...)
+* Library support via popular tools like the `clojure` CLI
-Non-goals:
+### Non-goals
-* Performance
-* Provide a mixed Clojure/bash DSL (see portability).
+* Performance1
+* Provide a mixed Clojure/Bash DSL (see portability).
* Replace existing shells. Babashka is a tool you can use inside existing shells like bash and it is designed to play well with them. It does not aim to replace them.
-Babashka uses [sci](https://github.com/borkdude/sci) for interpreting Clojure. Sci
-implements a subset of Clojure and is not as performant as compiled code. If your script is taking more than a few seconds, Clojure on the JVM may be a better fit.
+1 Babashka uses [sci](https://github.com/borkdude/sci) for
+interpreting Clojure. Sci implements a suffiently large subset of
+Clojure. Interpreting code is in general not as performant as executing compiled
+code. If your script takes more than a few seconds to run, Clojure on the JVM
+may be a better fit, since the performance of Clojure on the JVM outweighs its
+startup time penalty. Read more about the differences with Clojure
+[here](#differences-with-clojure).
-Read more about the differences with Clojure [here](#differences-with-clojure).
-## Status
+### Talk
-Experimental. Breaking changes are expected to happen at this phase. Keep an eye
-on [CHANGES.md](CHANGES.md) for a list of breaking changes.
+To get an overview of babashka, you can watch this talk ([slides](https://speakerdeck.com/borkdude/babashka-and-the-small-clojure-interpreter-at-clojured-2020)):
-## Examples
+[](https://www.youtube.com/watch?v=Nw8aN-nrdEk)
+
+## Quickstart
``` shellsession
-$ ls | bb -i '*input*'
-["LICENSE" "README.md" "bb" "doc" "pom.xml" "project.clj" "resources" "script" "src" "target" "test"]
+$ curl -s https://raw.githubusercontent.com/borkdude/babashka/master/install -o install-babashka
+$ chmod +x install-babashka && ./install-babashka
+$ ls | bb --time -i '(filter #(-> % io/file .isDirectory) *input*)'
+("doc" "resources" "sci" "script" "src" "target" "test")
+bb took 4ms.
+```
-$ ls | bb -i '(count *input*)'
-12
+### Examples
+Read the output from a shell command as a lazy seq of strings:
+
+``` shell
+$ ls | bb -i '(take 2 *input*)'
+("CHANGES.md" "Dockerfile")
+```
+
+Read EDN from stdin and write the result to stdout:
+
+``` shell
$ bb '(vec (dedupe *input*))' <<< '[1 1 1 1 2]'
[1 2]
+```
-$ bb '(filterv :foo *input*)' <<< '[{:foo 1} {:bar 2}]'
-[{:foo 1}]
+Read more about input and output flags
+[here](https://github.com/borkdude/babashka/#input-and-output-flags).
-$ bb '(#(+ %1 %2 %3) 1 2 *input*)' <<< 3
-6
+Execute a script. E.g. print the current time in California using the
+`java.time` API:
-$ ls | bb -i '(filterv #(re-find #"README" %) *input*)'
-["README.md"]
+File `pst.clj`:
+``` clojure
+#!/usr/bin/env bb
-$ bb '(run! #(shell/sh "touch" (str "/tmp/test/" %)) (range 100))'
-$ ls /tmp/test | bb -i '*input*'
-["0" "1" "10" "11" "12" "13" "14" "15" "16" "17" "18" "19" "2" "20" "21" ...]
+(def now (java.time.ZonedDateTime/now))
+(def LA-timezone (java.time.ZoneId/of "America/Los_Angeles"))
+(def LA-time (.withZoneSameInstant now LA-timezone))
+(def pattern (java.time.format.DateTimeFormatter/ofPattern "HH:mm"))
+(println (.format LA-time pattern))
+```
-$ bb -O '(repeat "dude")' | bb --stream '(str *input* "rino")' | bb -I '(take 3 *input*)'
-("duderino" "duderino" "duderino")
+``` shell
+$ pst.clj
+05:17
```
More examples can be found in the [gallery](#gallery).
+## Status
+
+Functionality regarding `clojure.core` and `java.lang` can be considered stable
+and is unlikely to change. Changes may happen in other parts of babashka,
+although we will try our best to prevent them. Always check the release notes or
+[CHANGES.md](CHANGES.md) before upgrading.
+
## Installation
### Brew
@@ -130,7 +154,13 @@ $ bash <(curl -s https://raw.githubusercontent.com/borkdude/babashka/master/inst
### Download
-You may also download a binary from [Github](https://github.com/borkdude/babashka/releases).
+You may also download a binary from
+[Github](https://github.com/borkdude/babashka/releases). For linux there is a
+static binary available which can be used on Alpine.
+
+## Docker
+
+Check out the image on [Docker hub](https://hub.docker.com/r/borkdude/babashka/).
## Usage
@@ -138,7 +168,7 @@ You may also download a binary from [Github](https://github.com/borkdude/babashk
Usage: bb [ -i | -I ] [ -o | -O ] [ --stream ] [--verbose]
[ ( --classpath | -cp ) ] [ --uberscript ]
[ ( --main | -m ) | -e | -f |
- --repl | --socket-repl [:] ]
+ --repl | --socket-repl [:] | --nrepl-server [:] ]
[ arg* ]
Options:
@@ -160,6 +190,7 @@ Options:
-m, --main Call the -main function from namespace with args.
--repl Start REPL. Use rlwrap for history.
--socket-repl Start socket REPL. Specify port (e.g. 1666) or host and port separated by colon (e.g. 127.0.0.1:1666).
+ --nrepl-server Start nREPL server. Specify port (e.g. 1667) or host and port separated by colon (e.g. 127.0.0.1:1667).
--time Print execution time before exiting.
-- Stop parsing args and pass everything after -- to *command-line-args*
@@ -184,14 +215,16 @@ enumerated explicitly.
`make-parents`, `output-stream`, `reader`, `resource`, `writer`
- `clojure.main`: `repl`
- [`clojure.core.async`](https://clojure.github.io/core.async/) aliased as
- `async`. The `alt` and `go` macros are not available but `alts!!` does work as
- it is a function.
+ `async`.
- `clojure.stacktrace`
- `clojure.test`
- `clojure.pprint`: `pprint` (currently backed by [fipp](https://github.com/brandonbloom/fipp)'s `fipp.edn/pprint`)
- [`clojure.tools.cli`](https://github.com/clojure/tools.cli) aliased as `tools.cli`
- [`clojure.data.csv`](https://github.com/clojure/data.csv) aliased as `csv`
- [`cheshire.core`](https://github.com/dakrone/cheshire) aliased as `json`
+- [`cognitect.transit`](https://github.com/cognitect/transit-clj) aliased as `transit`
+- [`clj-yaml.core`](https://github.com/clj-commons/clj-yaml) alias as `yaml`
+- [`bencode.core`](https://github.com/nrepl/bencode) aliased as `bencode`: `read-bencode`, `write-bencode`
A selection of java classes are available, see `babashka/impl/classes.clj`.
@@ -311,6 +344,12 @@ $ bb '((fn [x] (println x) (when (not (signal/pipe-signal-received?)) (recur (in
The namespace `babashka.signal` is aliased as `signal` in the `user` namespace.
+#### babashka.curl
+
+The namespace `babashka.curl` is a tiny wrapper around curl. It's aliased as
+`curl` in the user namespace. See
+[babashka.curl](https://github.com/borkdude/babashka.curl).
+
## Running a file
Scripts may be executed from a file using `-f` or `--file`:
@@ -375,6 +414,12 @@ $ cat script.clj
("hello" "1" "2" "3")
```
+## [Running a REPL](doc/repl.md)
+
+Babashka offers a REPL, a socket REPL and an nREPL server. Look
+[here](doc/repl.md) for more information on how to use and integrate them with
+your editor.
+
## Preloads
The environment variable `BABASHKA_PRELOADS` allows to define code that will be
@@ -416,6 +461,28 @@ $ bb --classpath src --main my.namespace
Hello from my namespace!
```
+So if you have a larger script with a classic Clojure project layout like
+
+```shellsession
+$ tree -L 3
+├── deps.edn
+├── README
+├── src
+│ └── project_namespace
+│ ├── main.clj
+│ └── utilities.clj
+└── test
+ └── project_namespace
+ ├── test_main.clj
+ └── test_utilities.clj
+```
+Then you can tell Babashka to include both the `src` and `test`
+folders in the classpath and start a socket REPL by running:
+
+```shellsession
+$ bb --classpath src:test --socket-repl 1666
+```
+
Note that you can use the `clojure` tool to produce classpaths and download dependencies:
``` shellsession
@@ -441,6 +508,13 @@ $ bb "(my-gist-script/-main)"
Hello from gist script!
```
+When invoking `bb` with a main function, the expression `(System/getProperty
+"babashka.main")` will return the name of the main function.
+
+Also see the
+[babashka.classpath](https://github.com/borkdude/babashka/#babashkaclasspath)
+namespace which allows dynamically adding to the classpath.
+
### Deps.clj
The [`deps.clj`](https://github.com/borkdude/deps.clj/) script can be used to work with `deps.edn`-based projects:
@@ -469,6 +543,46 @@ Hello from gist script!
nil
```
+You can also use for example `deps.clj` to produce the classpath for a
+`babashka` REPL:
+
+```shellsession
+$ cat script/start-repl.sh
+#!/bin/sh -e
+git_root=$(git rev-parse --show-toplevel)
+export BABASHKA_CLASSPATH=$("$git_root"/script/deps.clj -Spath)
+bb --socket-repl 1666
+$ ./script/start-repl.sh
+Babashka socket REPL started at localhost:1666
+```
+
+Now, given that your `deps.edn` and source tree looks something like
+
+```shellsession
+$ cat deps.edn
+{:paths ["src" "test"]
+ :deps {}}
+$ tree -L 3
+├── deps.edn
+├── README
+├── script
+│ ├── deps.clj
+│ └── start-repl.sh
+├── src
+│ └── project_namespace
+│ ├── main.clj
+│ └── utilities.clj
+└── test
+ └── project_namespace
+ ├── test_main.clj
+ └── test_utilities.clj
+
+```
+
+you should now be able to `(require '[multi-machine-rsync.utilities :as util])`
+in your REPL and the source code in `/src/multi_machine_rsync/utilities.clj`
+will be evaluated and made available through the symbol `util`.
+
## Uberscript
The `--uberscript` option collects the expressions in
@@ -547,44 +661,6 @@ bb -cp "src:test:resources" \
(System/exit (+ fail error)))"
```
-## REPL
-
-Babashka supports both a REPL and socket REPL. To start the REPL, type:
-
-``` shell
-$ bb --repl
-```
-
-To get history with up and down arrows, use `rlwrap`:
-
-``` shell
-$ rlwrap bb --repl
-```
-
-To start the socket REPL you can do this:
-
-``` shellsession
-$ bb --socket-repl 1666
-Babashka socket REPL started at localhost:1666
-```
-
-Now you can connect with your favorite socket REPL client:
-
-``` shellsession
-$ rlwrap nc 127.0.0.1 1666
-Babashka v0.0.14 REPL.
-Use :repl/quit or :repl/exit to quit the REPL.
-Clojure rocks, Bash reaches.
-
-bb=> (+ 1 2 3)
-6
-bb=> :repl/quit
-$
-```
-
-A socket REPL client for Emacs is
-[inf-clojure](https://github.com/clojure-emacs/inf-clojure).
-
## Spawning and killing a process
Use the `java.lang.ProcessBuilder` class.
@@ -604,10 +680,9 @@ Also see this [example](examples/process_builder.clj).
## Async
-Apart from `future` and `pmap` for creating threads, you may use the `async`
-namespace, which maps to `clojure.core.async`, for asynchronous scripting. The
-following example shows how to get first available value from two different
-processes:
+In addition to `future`, `pmap`, `promise` and friends, you may use the
+`clojure.core.async` namespace for asynchronous scripting. The following example
+shows how to get first available value from two different processes:
``` clojure
bb '
@@ -620,30 +695,93 @@ bb '
process 2
```
+Note: the `go` macro is available for compatibility with JVM programs, but the
+implementation maps to `clojure.core.async/thread` and the single exclamation
+mark operations (`!`, etc.) map to the double exclamation mark operations
+(`!!`, etc.). It will not "park" threads, like on the JVM.
+
+## HTTP
+
+For making HTTP requests you can use:
+
+- [babashka.curl](https://github.com/borkdude/babashka.curl). This library is
+ included with babashka and aliased as `curl` in the user namespace.
+- `slurp` for simple `GET` requests
+- [clj-http-lite](https://github.com/borkdude/clj-http-lite) as a library.
+- `clojure.java.shell` or `java.lang.ProcessBuilder` for shelling out to your
+ favorite command line http client
+
+### HTTP over Unix sockets
+
+This can be useful for talking to Docker:
+
+``` clojure
+(require '[clojure.java.shell :refer [sh]])
+(require '[cheshire.core :as json])
+(-> (sh "curl" "--silent"
+ "--no-buffer" "--unix-socket"
+ "/var/run/docker.sock"
+ "http://localhost/images/json")
+ :out
+ (json/parse-string true)
+ first
+ :RepoTags) ;;=> ["borkdude/babashka:latest"]
+```
+
+## Shutdown hook
+
+Adding a shutdown hook allows you to execute some code before the script exits.
+
+``` clojure
+$ bb -e '(-> (Runtime/getRuntime) (.addShutdownHook (Thread. #(println "bye"))))'
+bye
+```
+
+This also works when the script is interrupted with ctrl-c.
+
+## Bencode
+
+Babashka comes with the [nrepl/bencode](https://github.com/nrepl/bencode)
+library which allows you to read and write bencode messages to a socket. A
+simple example which evaluates a Clojure expression on an nREPL server started
+with `lein repl`:
+
+``` clojure
+(ns nrepl-client
+ (:require [bencode.core :as b]))
+
+(defn nrepl-eval [port expr]
+ (let [s (java.net.Socket. "localhost" port)
+ out (.getOutputStream s)
+ in (java.io.PushbackInputStream. (.getInputStream s))
+ _ (b/write-bencode out {"op" "eval" "code" expr})
+ bytes (get (b/read-bencode in) "value")]
+ (String. bytes)))
+
+(nrepl-eval 52054 "(+ 1 2 3)") ;;=> "6"
+```
+
## Differences with Clojure
Babashka is implemented using the [Small Clojure
Interpreter](https://github.com/borkdude/sci). This means that a snippet or
script is not compiled to JVM bytecode, but executed form by form by a runtime
-which implements a subset of Clojure. Babashka is compiled to a native binary
-using [GraalVM](https://github.com/oracle/graal). It comes with a selection of
-built-in namespaces and functions from Clojure and other useful libraries. The
-data types (numbers, strings, persistent collections) are the
+which implements a sufficiently large subset of Clojure. Babashka is compiled to
+a native binary using [GraalVM](https://github.com/oracle/graal). It comes with
+a selection of built-in namespaces and functions from Clojure and other useful
+libraries. The data types (numbers, strings, persistent collections) are the
same. Multi-threading is supported (`pmap`, `future`).
Differences with Clojure:
-- A subset of Java classes are supported.
-
-- Only the `clojure.core`, `clojure.edn`, `clojue.java.io`,
- `clojure.java.shell`, `clojure.set`, `clojure.stacktrace`, `clojure.string`,
- `clojure.template`, `clojure.test` and `clojure.walk` namespaces are available
- from Clojure.
+- A pre-selected set of Java classes are supported. You cannot add Java classes
+ at runtime.
- Interpretation comes with overhead. Therefore tight loops are likely slower
- than in Clojure on the JVM.
+ than in Clojure on the JVM. In general interpretation yields slower programs
+ than compiled programs.
-- No support for unboxed types.
+- No `defprotocol`, `defrecord` and unboxed math.
## External resources
@@ -682,13 +820,13 @@ Ran 1 tests containing 0 assertions.
Requires `bb` >= v0.0.71. Latest coordinates checked with with bb:
``` clojure
-{:git/url "https://github.com/weavejester" :sha "a4e5fb5383f5c0d83cb2d005181a35b76d8a136d"}
+{:git/url "https://github.com/weavejester/medley" :sha "a4e5fb5383f5c0d83cb2d005181a35b76d8a136d"}
```
Example:
``` shell
-$ export BABASHKA_CLASSPATH=$(clojure -Spath -Sdeps '{:deps {medley {:git/url "https://github.com/weavejester" :sha "a4e5fb5383f5c0d83cb2d005181a35b76d8a136d"}}}')
+$ export BABASHKA_CLASSPATH=$(clojure -Spath -Sdeps '{:deps {medley {:git/url "https://github.com/weavejester/medley" :sha "a4e5fb5383f5c0d83cb2d005181a35b76d8a136d"}}}')
$ bb -e "(require '[medley.core :as m]) (m/index-by :id [{:id 1} {:id 2}])"
{1 {:id 1}, 2 {:id 2}}
@@ -749,26 +887,81 @@ export BABASHKA_CLASSPATH="$(clojure -Sdeps '{:deps {clojure-csv {:mvn/version "
Requires `bb` >= v0.0.71. Latest coordinates checked with with bb:
``` clojure
-{:git/url "https://github.com/lambdaisland/regal" :sha "8d300f8e15f43480801766b7762530b6d412c1e6"}
+{:git/url "https://github.com/lambdaisland/regal" :sha "d4e25e186f7b9705ebb3df6b21c90714d278efb7"}
```
Example:
``` shell
-$ export BABASHKA_CLASSPATH=$(clojure -Spath -Sdeps '{:deps {regal {:git/url "https://github.com/lambdaisland/regal" :sha "8d300f8e15f43480801766b7762530b6d412c1e6"}}}')
+$ export BABASHKA_CLASSPATH=$(clojure -Spath -Sdeps '{:deps {regal {:git/url "https://github.com/lambdaisland/regal" :sha "d4e25e186f7b9705ebb3df6b21c90714d278efb7"}}}')
$ bb -e "(require '[lambdaisland.regal :as regal]) (regal/regex [:* \"ab\"])"
#"(?:\Qab\E)*"
```
-#### [spartan.test](https://github.com/borkdude/spartan.test/)
+#### [4bb](https://github.com/porkostomus/4bb)
-A minimal test framework compatible with babashka. This library is deprecated
-since babashka v0.0.68 which has `clojure.test` built-in.
+4clojure as a babashka script!
+#### [cprop](https://github.com/tolitius/cprop/)
-### Blogs
+A clojure configuration libary. Latest test version: `"0.1.16"`.
+#### [comb](https://github.com/weavejester/comb)
+
+Simple templating system for Clojure. Latest tested version: `"0.1.1"`.
+
+``` clojure
+$ export BABASHKA_CLASSPATH=$(clojure -Spath -Sdeps '{:deps {comb {:mvn/version "0.1.1"}}}')
+$ rlwrap bb
+...
+user=> (require '[comb.template :as template])
+user=> (template/eval "<% (dotimes [x 3] %>foo<% ) %>")
+"foofoofoo"
+user=> (template/eval "Hello <%= name %>" {:name "Alice"})
+"Hello Alice"
+user=> (def hello (template/fn [name] "Hello <%= name %>"))
+user=> (hello "Alice")
+"Hello Alice"
+```
+
+#### [nubank/docopt](https://github.com/nubank/docopt.clj#babashka)
+
+Docopt implementation in Clojure, compatible with babashka.
+
+#### [babashka lambda layer](https://github.com/dainiusjocas/babashka-lambda-layer)
+
+Babashka Lambda runtime packaged as a Lambda layer.
+
+#### [Release on push Github action](https://github.com/rymndhng/release-on-push-action)
+
+Github Action to create a git tag + release when pushed to master. Written in
+babashka.
+
+#### [justone/bb-scripts](https://github.com/justone/bb-scripts)
+
+A collection of scripts developed by [@justone](https://github.com/justone).
+
+#### [nativity](https://github.com/MnRA/nativity)
+
+Turn babashka scripts into binaries using GraalVM `native-image`.
+
+#### [arrangement](https://github.com/greglook/clj-arrangement)
+
+A micro-library which provides a total-ordering comparator for Clojure
+values. Tested with version `1.2.0`.
+
+## Package babashka script as a AWS Lambda
+
+AWS Lambda runtime doesn't support signals, therefore babashka has to disable
+handling of the SIGPIPE. This can be done by setting
+`BABASHKA_DISABLE_PIPE_SIGNAL_HANDLER` to `true`.
+
+## Articles, podcasts and videos
+
+- [Implementing an nREPL server for babashka](https://youtu.be/0YmZYnwyHHc): impromptu presentation by Michiel Borkent at the online [Dutch Clojure Meetup](http://meetup.com/The-Dutch-Clojure-Meetup)
+- [ClojureScript podcast](https://soundcloud.com/user-959992602/s3-e5-babashka-with-michiel-borkent) with Jacek Schae interviewing Michiel Borkent
+- [Babashka talk at ClojureD](https://www.youtube.com/watch?v=Nw8aN-nrdEk) ([slides](https://speakerdeck.com/borkdude/babashka-and-the-small-clojure-interpreter-at-clojured-2020)) by Michiel Borkent
- [Babashka: a quick example](https://juxt.pro/blog/posts/babashka.html) by Malcolm Sparks
- [Clojure Start Time in 2019](https://stuartsierra.com/2019/12/21/clojure-start-time-in-2019) by Stuart Sierra
- [Advent of Random
@@ -837,13 +1030,18 @@ $ < /tmp/test.txt bb -io '(shuffle *input*)'
### Fetch latest Github release tag
-For converting JSON to EDN, see [jet](https://github.com/borkdude/jet).
+``` shell
+(require '[clojure.java.shell :refer [sh]]
+ '[cheshire.core :as json])
-``` shellsession
-$ curl -s https://api.github.com/repos/borkdude/babashka/tags |
-jet --from json --keywordize --to edn |
-bb '(-> *input* first :name (subs 1))'
-"0.0.4"
+(defn babashka-latest-version []
+ (-> (sh "curl" "https://api.github.com/repos/borkdude/babashka/tags")
+ :out
+ (json/parse-string true)
+ first
+ :name))
+
+(babashka-latest-version) ;;=> "v0.0.73"
```
### Generate deps.edn entry for a gitlib
@@ -938,6 +1136,8 @@ bb '(let [{:keys [dependencies source-paths resource-paths]} (apply hash-map (dr
jet --pretty > deps.edn
```
+A script with the same goal can be found [here](https://gist.github.com/swlkr/3f346c66410e5c60c59530c4413a248e#gistcomment-3232605).
+
### Print current time in California
See [examples/pst.clj](https://github.com/borkdude/babashka/blob/master/examples/pst.clj)
@@ -960,13 +1160,132 @@ clojure.core/ffirst
Same as (first (first x))
```
+### Cryptographic hash
+
+`sha1.clj`:
+``` clojure
+#!/usr/bin/env bb
+
+(defn sha1
+ [s]
+ (let [hashed (.digest (java.security.MessageDigest/getInstance "SHA-1")
+ (.getBytes s))
+ sw (java.io.StringWriter.)]
+ (binding [*out* sw]
+ (doseq [byte hashed]
+ (print (format "%02X" byte))))
+ (str sw)))
+
+(sha1 (first *command-line-args*))
+```
+
+``` shell
+$ sha1.clj babashka
+"0AB318BE3A646EEB1E592781CBFE4AE59701EDDF"
+```
+
+### Package script as Docker image
+
+`Dockerfile`:
+``` dockerfile
+FROM borkdude/babashka
+RUN echo $'\
+(println "Your command line args:" *command-line-args*)\
+'\
+>> script.clj
+
+ENTRYPOINT ["bb", "script.clj"]
+```
+
+``` shell
+$ docker build . -t script
+...
+$ docker run --rm script 1 2 3
+Your command line args: (1 2 3)
+```
+
+### Extract single file from zip
+
+``` clojure
+;; Given the following:
+
+;; $ echo 'contents' > file
+;; $ zip zipfile.zip file
+;; $ rm file
+
+;; we extract the single file from the zip archive using java.nio:
+
+(import '[java.nio.file Files FileSystems CopyOption])
+(let [zip-file (io/file "zipfile.zip")
+ file (io/file "file")
+ fs (FileSystems/newFileSystem (.toPath zip-file) nil)
+ file-in-zip (.getPath fs "file" (into-array String []))]
+ (Files/copy file-in-zip (.toPath file)
+ (into-array CopyOption [])))
+```
+
+### Note taking app
+
+See
+[examples/notes.clj](https://github.com/borkdude/babashka/blob/master/examples/notes.clj). This
+is a variation on the
+[http-server](https://github.com/borkdude/babashka/#tiny-http-server)
+example. If you get prompted with a login, use `admin`/`admin`.
+
+
+
+### which
+
+The `which` command re-implemented in Clojure. See
+[examples/which.clj](https://github.com/borkdude/babashka/blob/master/examples/which.clj).
+Prints the canonical file name.
+
+``` shell
+$ examples/which.clj rg
+/usr/local/Cellar/ripgrep/11.0.1/bin/rg
+```
+
## Thanks
- [adgoji](https://www.adgoji.com/) for financial support
+- [CircleCI](https://circleci.com/) for CI and additional support
+- [Nikita Prokopov](https://github.com/tonsky) for the logo
+- [contributors](https://github.com/borkdude/babashka/graphs/contributors) and
+ other users posting issues with bug reports and ideas
+
+## Contributors
+
+### Code Contributors
+
+This project exists thanks to all the people who contribute. [[Contribute](doc/dev.md)].
+
+
+### Financial Contributors
+
+Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/babashka/contribute)]
+
+#### Individuals
+
+
+
+#### Organizations
+
+Support this project with your organization. Your logo will show up here with a link to your website. [[Contribute](https://opencollective.com/babashka/contribute)]
+
+
+
+
+
+
+
+
+
+
+
## License
-Copyright © 2019 Michiel Borkent
+Copyright © 2019-2020 Michiel Borkent
Distributed under the EPL License. See LICENSE.
diff --git a/assets/notes-example.png b/assets/notes-example.png
new file mode 100644
index 00000000..4434ef35
Binary files /dev/null and b/assets/notes-example.png differ
diff --git a/babashka.curl b/babashka.curl
new file mode 160000
index 00000000..a9e9fe83
--- /dev/null
+++ b/babashka.curl
@@ -0,0 +1 @@
+Subproject commit a9e9fe83d56b020071c1a3bbeb4656e53c8a988d
diff --git a/deps.edn b/deps.edn
index 2c28c015..1d43466f 100644
--- a/deps.edn
+++ b/deps.edn
@@ -1,7 +1,7 @@
-{:paths ["src" "sci/src" "resources" "sci/resources"],
- :deps {org.clojure/clojure {:mvn/version "1.10.1"},
+{:paths ["src" "sci/src" "babashka.curl/src" "resources" "sci/resources"],
+ :deps {org.clojure/clojure {:mvn/version "1.10.2-alpha1"},
org.clojure/tools.reader {:mvn/version "1.3.2"},
- borkdude/edamame {:mvn/version "0.0.10"},
+ borkdude/edamame {:mvn/version "0.0.11-alpha.9"},
borkdude/graal.locking {:mvn/version "0.0.2"},
borkdude/sci.impl.reflector {:mvn/version "0.0.1"}
org.clojure/core.async {:mvn/version "1.0.567"},
@@ -9,7 +9,9 @@
org.clojure/data.csv {:mvn/version "1.0.0"},
cheshire {:mvn/version "5.10.0"}
org.clojure/data.xml {:mvn/version "0.2.0-alpha6"}
- fipp {:mvn/version "0.6.22"}}
+ fipp {:mvn/version "0.6.22"}
+ clj-commons/clj-yaml {:mvn/version "0.7.1"}
+ com.cognitect/transit-clj {:mvn/version "1.0.324"}}
:aliases {:main
{:main-opts ["-m" "babashka.main"]}
:profile
diff --git a/doc/dev.md b/doc/dev.md
index a5c1b8da..2ffae17f 100644
--- a/doc/dev.md
+++ b/doc/dev.md
@@ -1,5 +1,9 @@
# Developing Babashka
+You need [lein](https://leiningen.org/) for running JVM tests and/or producing uberjars. For building binaries you need GraalVM. Currently we use java8-19.3.1.
+
+## Clone repository
+
To work on Babashka itself make sure Git submodules are checked out.
``` shellsession
@@ -12,8 +16,6 @@ To update later on:
$ git submodule update --recursive
```
-You need [Leiningen](https://leiningen.org/), and for building binaries you need GraalVM.
-
## REPL
`lein repl` will get you a standard REPL/nREPL connection. To work on tests use `lein with-profiles +test repl`.
@@ -47,16 +49,40 @@ To build this project, set `$GRAALVM_HOME` to the GraalVM distribution directory
Then run:
- script/compile
+ $ script/compile
+
+To tweak maximum heap size:
+
+```
+$ BABASHKA_XMX="-J-Xmx4g" script/compile
+```
## Binary size
Keep notes here about how adding libraries and classes to Babashka affects the binary size.
+We're registering the size of the macOS binary (as built on CircleCI).
-We're only registering the size of the macOS binary (as built on CircleCI).
+2020/03/29 Added clj-yaml for parsing and generating yaml.
+45196996 - 42626884 = 2570kb added.
-2020/01/08, ..., 38.7mb / 11.3mb zipped
-Added: `clojure.data.xml`. Growth: 1.8mb / 0.4mb zipped.
+2020/03/28 Added java.nio.file.FileSystem(s) to support extracting zip files
+42562284 - 42021244 = 541kb added.
+
+2020/03/22 Added java.io.FileReader
+42025276 - 42008876 = 16kb added.
+
+2020/03/20 Added transit write, writer, read, reader
+42004796 - 41025212 = 980kb added (305kb zipped).
+
+2020/03/19 Added java.lang.NumberFormatException, java.lang.RuntimeException,
+java.util.MissingResourceException and java.util.Properties to support
+[cprop](https://github.com/tolitius/cprop/).
+41025180 - 40729908 = 295kb added.
+
+2020/02/21
+Added java.time.temporal.ChronoUnit
+40651596 - 40598260 = 53kb added.
+>>>>>>> master
2020/02/19, e43727955a2cdabd2bb0189c20dd7f9a18156fc9
Added fipp.edn/pprint
diff --git a/doc/repl.md b/doc/repl.md
new file mode 100644
index 00000000..2cdb836e
--- /dev/null
+++ b/doc/repl.md
@@ -0,0 +1,128 @@
+# Running a REPL
+
+Babashka supports running a REPL, a socket REPL and an nREPL server.
+
+## REPL
+
+To start the REPL, type:
+
+``` shell
+$ bb --repl
+```
+
+To get history with up and down arrows, use `rlwrap`:
+
+``` shell
+$ rlwrap bb --repl
+```
+
+## Socket REPL
+
+To start the socket REPL you can do this:
+
+``` shell
+$ bb --socket-repl 1666
+Babashka socket REPL started at localhost:1666
+```
+
+Now you can connect with your favorite socket REPL client:
+
+``` shell
+$ rlwrap nc 127.0.0.1 1666
+Babashka v0.0.14 REPL.
+Use :repl/quit or :repl/exit to quit the REPL.
+Clojure rocks, Bash reaches.
+
+bb=> (+ 1 2 3)
+6
+bb=> :repl/quit
+$
+```
+
+Editor plugins and tools known to work with a babashka socket REPL:
+
+- Emacs: [inf-clojure](https://github.com/clojure-emacs/inf-clojure):
+
+ To connect:
+
+ `M-x inf-clojure-connect localhost 1666`
+
+ Before evaluating from a Clojure buffer:
+
+ `M-x inf-clojure-minor-mode`
+
+- Atom: [Chlorine](https://github.com/mauricioszabo/atom-chlorine)
+- Vim: [vim-iced](https://github.com/liquidz/vim-iced)
+- IntelliJ IDEA: [Cursive](https://cursive-ide.com/)
+
+ Note: you will have to use a workaround via
+ [tubular](https://github.com/mfikes/tubular). For more info, look
+ [here](https://cursive-ide.com/userguide/repl.html#repl-types).
+
+
+## nREPL
+
+To start an nREPL server:
+
+``` shell
+$ bb --nrepl-server 1667
+```
+
+Then connect with your favorite nREPL client:
+
+``` clojure
+$ lein repl :connect 1667
+Connecting to nREPL at 127.0.0.1:1667
+user=> (+ 1 2 3)
+6
+user=>
+```
+
+Editor plugins and tools known to work with the babashka nREPL server:
+
+ - Emacs: [CIDER](https://docs.cider.mx/cider-nrepl/)
+ - `lein repl :connect`
+ - VSCode: [Calva](http://calva.io/)
+ - Atom: [Chlorine](https://github.com/mauricioszabo/atom-chlorine)
+ - (Neo)Vim: [vim-iced](https://github.com/liquidz/vim-iced), [conjure](https://github.com/Olical/conjure), [fireplace](https://github.com/tpope/vim-fireplace)
+
+The babashka nREPL server does not write an `.nrepl-port` file at startup, but
+you can easily write a script that launches the server and writes the file:
+
+``` clojure
+#!/usr/bin/env bb
+
+(import [java.net ServerSocket]
+ [java.io File]
+ [java.lang ProcessBuilder$Redirect])
+
+(require '[babashka.wait :as wait])
+
+(let [nrepl-port (with-open [sock (ServerSocket. 0)] (.getLocalPort sock))
+ pb (doto (ProcessBuilder. (into ["bb" "--nrepl-server" (str nrepl-port)]
+ *command-line-args*))
+ (.redirectOutput ProcessBuilder$Redirect/INHERIT))
+ proc (.start pb)]
+ (wait/wait-for-port "localhost" nrepl-port)
+ (spit ".nrepl-port" nrepl-port)
+ (.deleteOnExit (File. ".nrepl-port"))
+ (.waitFor proc))
+```
+
+### Debugging the nREPL server
+
+To debug the nREPL server from the binary you can run:
+
+``` shell
+$ BABASHKA_DEV=true bb --nrepl-server 1667
+```
+
+This will print all the incoming messages.
+
+To debug the nREPL server from source:
+
+``` clojure
+$ git clone https://github.com/borkdude/babashka --recursive
+$ cd babashka
+$ BABASHKA_DEV=true clojure -A:main --nrepl-server 1667
+```
diff --git a/examples/notes.clj b/examples/notes.clj
new file mode 100755
index 00000000..c23769ef
--- /dev/null
+++ b/examples/notes.clj
@@ -0,0 +1,147 @@
+#!/usr/bin/env bb
+
+(import (java.net ServerSocket))
+(require '[clojure.java.io :as io]
+ '[clojure.string :as str])
+
+(def debug? true)
+(def user "admin")
+(def password "admin")
+(def base64 (-> (.getEncoder java.util.Base64)
+ (.encodeToString (.getBytes (str user ":" password)))))
+
+(def notes-file (io/file (System/getProperty "user.home") ".notes" "notes.txt"))
+(def file-lock (Object.))
+
+(defn write-note! [note]
+ (locking file-lock
+ (io/make-parents notes-file)
+ (spit notes-file (str note "\n") :append true)))
+
+;; hiccup-like
+(defn html [v]
+ (cond (vector? v)
+ (let [tag (first v)
+ attrs (second v)
+ attrs (when (map? attrs) attrs)
+ elts (if attrs (nnext v) (next v))
+ tag-name (name tag)]
+ (format "<%s%s>%s%s>\n" tag-name (html attrs) (html elts) tag-name))
+ (map? v)
+ (str/join ""
+ (map (fn [[k v]]
+ (format " %s=\"%s\"" (name k) v)) v))
+ (seq? v)
+ (str/join " " (map html v))
+ :else (str v)))
+
+(defn write-response [out session-id status headers content]
+ (let [cookie-header (str "Set-Cookie: notes-id=" session-id)
+ headers (str/join "\r\n" (conj headers cookie-header))
+ response (str "HTTP/1.1 " status "\r\n"
+ (str headers "\r\n")
+ "Content-Length: " (if content (count content)
+ 0)
+ "\r\n\r\n"
+ (when content
+ (str content)))]
+ (when debug? (println response))
+ (binding [*out* out]
+ (print response)
+ (flush))))
+
+;; the home page
+(defn home-response [out session-id]
+ (let [body (str
+ "\n"
+ (html
+ [:html
+ [:head
+ [:title "Notes"]]
+ [:body
+ [:h1 "Notes"]
+ [:pre (when (.exists notes-file)
+ (slurp notes-file))]
+ [:form {:action "/" :method "post"}
+ [:input {:type "text" :name "note"}]
+ [:input {:type "submit" :value "Submit"}]]]]))]
+ (write-response out session-id "200 OK" nil body)))
+
+(defn basic-auth-response [out session-id]
+ (write-response out session-id
+ "401 Unauthorized"
+ ["WWW-Authenticate: Basic realm=\"notes\""]
+ nil))
+
+(def known-sessions
+ (atom #{}))
+
+(defn new-session! []
+ (let [uuid (str (java.util.UUID/randomUUID))]
+ (swap! known-sessions conj uuid)
+ uuid))
+
+(defn get-session-id [headers]
+ (if-let [cookie-header (first (filter #(str/starts-with? % "Cookie: ") headers))]
+ (let [parts (str/split cookie-header #"; ")]
+ (if-let [notes-id (first (filter #(str/starts-with? % "notes-id") parts))]
+ (str/replace notes-id "notes-id=" "")
+ (new-session!)))
+ (new-session!)))
+
+(defn basic-auth-header [headers]
+ (some #(str/starts-with? % "Basic-Auth: ") headers))
+
+(def authenticated-sessions
+ (atom #{}))
+
+(defn authenticate! [session-id headers]
+ (or (contains? @authenticated-sessions session-id)
+ (when (some #(= % (str "Authorization: Basic " base64)) headers)
+ (swap! authenticated-sessions conj session-id)
+ true)))
+
+;; run the server
+(with-open [server-socket (let [s (new ServerSocket 8080)]
+ (println "Server started on port 8080.")
+ s)]
+ (loop []
+ (let [client-socket (.accept server-socket)]
+ (future
+ (with-open [conn client-socket]
+ (try
+ (let [out (io/writer (.getOutputStream conn))
+ is (.getInputStream conn)
+ in (io/reader is)
+ [_req & headers :as response]
+ (loop [headers []]
+ (let [line (.readLine in)]
+ (if (str/blank? line)
+ headers
+ (recur (conj headers line)))))
+ session-id (get-session-id headers)
+ form-data (let [sb (StringBuilder.)]
+ (loop []
+ (when (.ready in)
+ (.append sb (char (.read in)))
+ (recur)))
+ (-> (str sb)
+ (java.net.URLDecoder/decode)))
+ _ (when debug? (println (str/join "\n" response)))
+ _ (when-not (str/blank? form-data)
+ (when debug? (println form-data))
+ (let [note (str/replace form-data "note=" "")]
+ (write-note! note)))
+ _ (when debug? (println))]
+ (cond
+ ;; if we didn't see this session before, we want the user to re-authenticate
+ (not (contains? @known-sessions session-id))
+ (let [uuid (new-session!)]
+ (basic-auth-response out uuid))
+ (not (authenticate! session-id headers))
+ (basic-auth-response out session-id)
+ :else (home-response out session-id)))
+ (catch Throwable t
+ (binding [*err* *out*]
+ (println t)))))))
+ (recur)))
diff --git a/examples/which.clj b/examples/which.clj
new file mode 100755
index 00000000..8bc43cee
--- /dev/null
+++ b/examples/which.clj
@@ -0,0 +1,17 @@
+#!/usr/bin/env bb
+
+(require '[clojure.java.io :as io])
+
+(defn which [executable]
+ (let [path (System/getenv "PATH")
+ paths (.split path (System/getProperty "path.separator"))]
+ (loop [paths paths]
+ (when-first [p paths]
+ (let [f (io/file p executable)]
+ (if (and (.isFile f)
+ (.canExecute f))
+ (.getCanonicalPath f)
+ (recur (rest paths))))))))
+
+(when-let [executable (first *command-line-args*)]
+ (println (which executable)))
diff --git a/project.clj b/project.clj
index 8bd811f8..37f95090 100644
--- a/project.clj
+++ b/project.clj
@@ -7,11 +7,13 @@
:url "https://github.com/borkdude/babashka"}
:license {:name "Eclipse Public License 1.0"
:url "http://opensource.org/licenses/eclipse-1.0.php"}
- :source-paths ["src" "sci/src"]
+ :source-paths ["src" "sci/src" "babashka.curl/src"]
+ ;; for debugging Reflector.java code:
+ ;; :java-source-paths ["sci/reflector/src-java"]
:resource-paths ["resources" "sci/resources"]
- :dependencies [[org.clojure/clojure "1.10.1"]
+ :dependencies [[org.clojure/clojure "1.10.2-alpha1"]
[org.clojure/tools.reader "1.3.2"]
- [borkdude/edamame "0.0.10"]
+ [borkdude/edamame "0.0.11-alpha.9"]
[borkdude/graal.locking "0.0.2"]
[borkdude/sci.impl.reflector "0.0.1"]
[org.clojure/core.async "1.0.567"]
@@ -19,7 +21,9 @@
[org.clojure/data.csv "1.0.0"]
[org.clojure/data.xml "0.2.0-alpha6"]
[cheshire "5.10.0"]
- [fipp "0.6.22"]]
+ [fipp "0.6.22"]
+ [clj-commons/clj-yaml "0.7.1"]
+ [com.cognitect/transit-clj "1.0.324"]]
:profiles {:test {:dependencies [[clj-commons/conch "0.9.2"]
[com.clojure-goes-fast/clj-async-profiler "0.4.0"]]}
:uberjar {:global-vars {*assert* false}
diff --git a/resources/BABASHKA_RELEASED_VERSION b/resources/BABASHKA_RELEASED_VERSION
index cf9f1739..4c4317b7 100644
--- a/resources/BABASHKA_RELEASED_VERSION
+++ b/resources/BABASHKA_RELEASED_VERSION
@@ -1 +1 @@
-0.0.71
\ No newline at end of file
+0.0.86
\ No newline at end of file
diff --git a/resources/BABASHKA_VERSION b/resources/BABASHKA_VERSION
index 75b9c2a8..baa5f3ce 100644
--- a/resources/BABASHKA_VERSION
+++ b/resources/BABASHKA_VERSION
@@ -1 +1 @@
-0.0.72-SNAPSHOT
\ No newline at end of file
+0.0.87-SNAPSHOT
\ No newline at end of file
diff --git a/resources/CutOffCoreServicesDependencies.java b/resources/CutOffCoreServicesDependencies.java
new file mode 100644
index 00000000..50f316b1
--- /dev/null
+++ b/resources/CutOffCoreServicesDependencies.java
@@ -0,0 +1,19 @@
+import org.graalvm.nativeimage.Platform;
+import org.graalvm.nativeimage.Platforms;
+import com.oracle.svm.core.annotate.TargetClass;
+import com.oracle.svm.core.annotate.Delete;
+
+public final class CutOffCoreServicesDependencies {
+}
+
+// @Platforms(Platform.DARWIN.class)
+// @TargetClass(className = "sun.net.spi.DefaultProxySelector")
+// @Delete
+// final class Target_sun_net_spi_DefaultProxySelector {
+// }
+
+@Platforms(Platform.DARWIN.class)
+@TargetClass(className = "apple.security.KeychainStore")
+@Delete
+final class Target_apple_security_KeychainStore {
+}
diff --git a/sci b/sci
index eebb4566..ead5dd7c 160000
--- a/sci
+++ b/sci
@@ -1 +1 @@
-Subproject commit eebb456628beb2ac0d1e31c2be46ee0683b9ee7a
+Subproject commit ead5dd7c25e0e38cb6244077ec9e57e00665cde9
diff --git a/script/compile b/script/compile
index fbb59dc7..69ed5424 100755
--- a/script/compile
+++ b/script/compile
@@ -2,48 +2,58 @@
set -eo pipefail
+if [ -z "$BABASHKA_XMX" ]; then
+ export BABASHKA_XMX="-J-Xmx3g"
+fi
+
if [ -z "$GRAALVM_HOME" ]; then
echo "Please set GRAALVM_HOME"
exit 1
fi
-if [ -z "$BABASHKA_XMX" ]; then
- export BABASHKA_XMX="-J-Xmx3g"
-fi
-
-"$GRAALVM_HOME/bin/gu" install native-image || true
+$GRAALVM_HOME/bin/gu install native-image
BABASHKA_VERSION=$(cat resources/BABASHKA_VERSION)
-# # We also need to AOT sci, else something didn't work in the Mac build on CircleCI
-# # See https://github.com/oracle/graal/issues/1613
-# ( cd /tmp; git clone https://github.com/borkdude/sci 2> /dev/null || true )
-# mkdir -p src/sci
-# cp -R /tmp/sci/src/* src
-
export JAVA_HOME=$GRAALVM_HOME
-lein with-profiles +reflection do run
-lein do clean, uberjar
+SVM_JAR=$(find "$GRAALVM_HOME" | grep svm.jar)
+$GRAALVM_HOME/bin/javac -cp "$SVM_JAR" resources/CutOffCoreServicesDependencies.java
-$GRAALVM_HOME/bin/native-image \
- -jar target/babashka-$BABASHKA_VERSION-standalone.jar \
- -H:Name=bb \
- -H:+ReportExceptionStackTraces \
- -J-Dclojure.spec.skip-macros=true \
- -J-Dclojure.compiler.direct-linking=true \
- "-H:IncludeResources=BABASHKA_VERSION" \
- "-H:IncludeResources=SCI_VERSION" \
- -H:ReflectionConfigurationFiles=reflection.json \
- --initialize-at-run-time=java.lang.Math\$RandomNumberGeneratorHolder \
- --initialize-at-build-time \
- -H:Log=registerResource: \
- -H:EnableURLProtocols=http,https \
- --enable-all-security-services \
- -H:+JNI \
- --verbose \
- --no-fallback \
- --no-server \
- "$BABASHKA_XMX"
+if [ -z "$BABASHKA_JAR" ]; then
+ lein with-profiles +reflection do run
+ lein do clean, uberjar
+ BABASHKA_JAR=${BABASHKA_JAR:-"target/babashka-$BABASHKA_VERSION-standalone.jar"}
+fi
-lein clean
+BABASHKA_BINARY=${BABASHKA_BINARY:-"bb"}
+
+args=( -jar $BABASHKA_JAR \
+ -H:Name=$BABASHKA_BINARY \
+ -H:+ReportExceptionStackTraces \
+ -J-Dclojure.spec.skip-macros=true \
+ -J-Dclojure.compiler.direct-linking=true \
+ "-H:IncludeResources=BABASHKA_VERSION" \
+ "-H:IncludeResources=SCI_VERSION" \
+ -H:ReflectionConfigurationFiles=reflection.json \
+ --initialize-at-run-time=java.lang.Math\$RandomNumberGeneratorHolder \
+ --initialize-at-build-time \
+ -H:Log=registerResource: \
+ -H:EnableURLProtocols=http,https \
+ --enable-all-security-services \
+ -H:+JNI \
+ --verbose \
+ --no-fallback \
+ --no-server \
+ --report-unsupported-elements-at-runtime \
+ "$BABASHKA_XMX" )
+
+if [ "$BABASHKA_STATIC" = "true" ]; then
+ args+=("--static")
+fi
+
+$GRAALVM_HOME/bin/native-image "${args[@]}"
+
+if [ ! -z "$(command -v lein)" ]; then
+ lein clean
+fi
diff --git a/.circleci/script/install-clojure b/script/install-clojure
similarity index 95%
rename from .circleci/script/install-clojure
rename to script/install-clojure
index b4cb4a55..f4781ba1 100755
--- a/.circleci/script/install-clojure
+++ b/script/install-clojure
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
-install_dir=${1:-/tmp/clojure}
+install_dir=${1:-/usr/local}
mkdir -p "$install_dir"
cd /tmp
curl -O -sL https://download.clojure.org/install/clojure-tools-1.10.1.447.tar.gz
diff --git a/script/install-leiningen b/script/install-leiningen
new file mode 100755
index 00000000..92a62a41
--- /dev/null
+++ b/script/install-leiningen
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+
+curl https://raw.githubusercontent.com/technomancy/leiningen/2.9.1/bin/lein > lein
+mkdir -p /usr/local/bin/
+mv lein /usr/local/bin/lein
+chmod a+x /usr/local/bin/lein
+lein self-install
diff --git a/script/lib_tests/arrangement_test b/script/lib_tests/arrangement_test
new file mode 100755
index 00000000..fa93438f
--- /dev/null
+++ b/script/lib_tests/arrangement_test
@@ -0,0 +1,13 @@
+#!/usr/bin/env bash
+
+set -eo pipefail
+
+export BABASHKA_CLASSPATH=$(clojure -Sdeps '{:deps {mvxcvi/arrangement {:mvn/version "1.2.0"}}}' -Spath)
+
+if [ "$BABASHKA_TEST_ENV" = "native" ]; then
+ BB_CMD="./bb"
+else
+ BB_CMD="lein bb"
+fi
+
+$BB_CMD -e "(require '[arrangement.core :as order]) (sort order/rank ['a false 2 :b nil 3.14159 \"c\" true \d [3 2] #{:one :two} [3 1 2] #{:three}])"
diff --git a/script/lib_tests/babashka_curl_test b/script/lib_tests/babashka_curl_test
new file mode 100755
index 00000000..84f60151
--- /dev/null
+++ b/script/lib_tests/babashka_curl_test
@@ -0,0 +1,33 @@
+#!/usr/bin/env bash
+
+set -eo pipefail
+
+export BABASHKA_CLASSPATH=$(clojure -Sdeps '{:deps {babasha.curl {:local/root "babashka.curl"}}}' -Spath)
+
+if [ "$BABASHKA_TEST_ENV" = "native" ]; then
+ BB_CMD="./bb"
+else
+ BB_CMD="lein bb"
+fi
+
+$BB_CMD -e "
+(require '[babashka.curl :as curl] :reload-all)
+
+(prn (:status (curl/get \"https://www.clojure.org\")))
+
+(prn (:status (curl/get \"https://postman-echo.com/get?foo1=bar1&foo2=bar2\")))
+
+(prn (:status (curl/post \"https://postman-echo.com/post\")))
+
+(prn (:status (curl/post \"https://postman-echo.com/post\"
+ {:body (json/generate-string {:a 1})
+ :headers {\"X-Hasura-Role\" \"admin\"}
+ :content-type :json
+ :accept :json})))
+
+(prn (:status (curl/put \"https://postman-echo.com/put\"
+ {:body (json/generate-string {:a 1})
+ :headers {\"X-Hasura-Role\" \"admin\"}
+ :content-type :json
+ :accept :json})))
+"
diff --git a/script/lib_tests/clj_yaml_test b/script/lib_tests/clj_yaml_test
new file mode 100755
index 00000000..a20a5279
--- /dev/null
+++ b/script/lib_tests/clj_yaml_test
@@ -0,0 +1,19 @@
+#!/usr/bin/env bash
+
+set -eo pipefail
+
+if [ "$BABASHKA_TEST_ENV" = "native" ]; then
+ BB_CMD="./bb"
+else
+ BB_CMD="lein bb"
+fi
+
+$BB_CMD -cp test-resources/lib_tests -e "
+(require '[clojure.java.io :as io])
+(require '[clj-yaml.core-test])
+(require '[clojure.test :as t])
+(let [{:keys [:test :pass :fail :error]} (t/run-tests 'clj-yaml.core-test)]
+ (when-not (pos? test)
+ (System/exit 1))
+ (System/exit (+ fail error)))
+"
diff --git a/script/lib_tests/clojure_data_csv_test b/script/lib_tests/clojure_data_csv_test
new file mode 100755
index 00000000..c229ab92
--- /dev/null
+++ b/script/lib_tests/clojure_data_csv_test
@@ -0,0 +1,19 @@
+#!/usr/bin/env bash
+
+set -eo pipefail
+
+if [ "$BABASHKA_TEST_ENV" = "native" ]; then
+ BB_CMD="./bb"
+else
+ BB_CMD="lein bb"
+fi
+
+$BB_CMD -cp test-resources/lib_tests -e "
+(require '[clojure.java.io :as io])
+(require '[clojure.data.csv-test])
+(require '[clojure.test :as t])
+(let [{:keys [:test :pass :fail :error]} (t/run-tests 'clojure.data.csv-test)]
+ (when-not (pos? test)
+ (System/exit 1))
+ (System/exit (+ fail error)))
+"
diff --git a/script/lib_tests/comb_test b/script/lib_tests/comb_test
new file mode 100755
index 00000000..3e6e88df
--- /dev/null
+++ b/script/lib_tests/comb_test
@@ -0,0 +1,21 @@
+#!/usr/bin/env bash
+
+set -eo pipefail
+
+export BABASHKA_CLASSPATH=$(clojure -Spath -Sdeps '{:deps {comb {:mvn/version "0.1.1"}}}')
+
+if [ "$BABASHKA_TEST_ENV" = "native" ]; then
+ BB_CMD="./bb"
+else
+ BB_CMD="lein bb"
+fi
+
+
+$BB_CMD '
+(ns foo (:require [comb.template :as template]))
+(prn (template/eval "<% (dotimes [x 3] %>foo<% ) %>"))
+(prn (template/eval "Hello <%= name %>" {:name "Alice"}))
+(def hello
+ (template/fn [name] "Hello <%= name %>"))
+(prn (hello "Alice"))
+'
diff --git a/script/lib_tests/cprop_test b/script/lib_tests/cprop_test
new file mode 100755
index 00000000..71ef87bd
--- /dev/null
+++ b/script/lib_tests/cprop_test
@@ -0,0 +1,18 @@
+#!/usr/bin/env bash
+
+set -eo pipefail
+
+export BABASHKA_CLASSPATH=$(clojure -Spath -Sdeps '{:deps {cprop {:mvn/version "0.1.16"}}}')
+
+if [ "$BABASHKA_TEST_ENV" = "native" ]; then
+ BB_CMD="./bb"
+else
+ BB_CMD="lein bb"
+fi
+
+
+CPROP_ENV="hello" $BB_CMD "
+(require '[cprop.core :refer [load-config]])
+(require '[cprop.source :refer [from-system-props from-env]])
+(println (:cprop-env (from-env)))
+"
diff --git a/script/lib_tests/regal_test b/script/lib_tests/regal_test
index 76dcca5a..4073c9e6 100755
--- a/script/lib_tests/regal_test
+++ b/script/lib_tests/regal_test
@@ -2,7 +2,7 @@
set -eo pipefail
-export BABASHKA_CLASSPATH="$(clojure -Sdeps '{:deps {regal {:git/url "https://github.com/lambdaisland/regal" :sha "8d300f8e15f43480801766b7762530b6d412c1e6"}}}' -Spath)"
+export BABASHKA_CLASSPATH="$(clojure -Sdeps '{:deps {regal {:git/url "https://github.com/lambdaisland/regal" :sha "b059fdb06d5586a9a04c27e7b011c467ad8546db"}}}' -Spath)"
if [ "$BABASHKA_TEST_ENV" = "native" ]; then
BB_CMD="./bb"
@@ -10,4 +10,13 @@ else
BB_CMD="lein bb"
fi
-$BB_CMD "(require '[lambdaisland.regal :as re]) (re/regex [:range \a \z])"
+$BB_CMD "
+(require '[lambdaisland.regal :as regal])
+(def r [:cat
+ [:+ [:class [\a \z]]]
+ \"=\"
+ [:+ [:not \=]]])
+
+(prn (regal/regex r))
+(prn (re-matches (regal/regex r) \"foo=bar\"))
+"
diff --git a/script/reflection.clj b/script/reflection.clj
new file mode 100755
index 00000000..315872c0
--- /dev/null
+++ b/script/reflection.clj
@@ -0,0 +1,9 @@
+#!/usr/bin/env bb
+
+(require '[clojure.java.io :as io]
+ '[clojure.java.shell :refer [sh]]
+ '[clojure.string :as str])
+
+(def version (str/trim (slurp (io/file "resources" "BABASHKA_VERSION"))))
+(sh "lein" "with-profiles" "+reflection" "run")
+(io/copy (io/file "reflection.json") (io/file (str "babashka-" version "-reflection.json")))
diff --git a/script/run_lib_tests b/script/run_lib_tests
index 8e0695a9..b50b845d 100755
--- a/script/run_lib_tests
+++ b/script/run_lib_tests
@@ -8,3 +8,9 @@ script/lib_tests/spartan_spec_test
script/lib_tests/clojure_csv_test
script/lib_tests/regal_test
script/lib_tests/medley_test
+script/lib_tests/babashka_curl_test
+script/lib_tests/cprop_test
+script/lib_tests/comb_test
+script/lib_tests/arrangement_test
+script/lib_tests/clj_yaml_test
+script/lib_tests/clojure_data_csv_test
diff --git a/script/test b/script/test
index 58f31d5f..d6c8473b 100755
--- a/script/test
+++ b/script/test
@@ -3,13 +3,16 @@
set -eo pipefail
BABASHKA_PRELOADS=""
BABASHKA_CLASSPATH=""
+echo "running tests part 1"
lein test "$@"
BABASHKA_PRELOADS='(defn __bb__foo [] "foo") (defn __bb__bar [] "bar")'
BABASHKA_PRELOADS_TEST=true
+echo "running tests part 2"
lein test :only babashka.main-test/preloads-test
BABASHKA_PRELOADS="(require '[env-ns])"
BABASHKA_CLASSPATH_TEST=true
BABASHKA_CLASSPATH="test-resources/babashka/src_for_classpath_test/env"
+echo "running tests part 3"
lein test :only babashka.classpath-test/classpath-env-test
diff --git a/src/babashka/impl/async.clj b/src/babashka/impl/async.clj
index 9cfa421a..6d2cf7a0 100644
--- a/src/babashka/impl/async.clj
+++ b/src/babashka/impl/async.clj
@@ -1,18 +1,50 @@
(ns babashka.impl.async
{:no-doc true}
(:require [clojure.core.async :as async]
- [clojure.core.async.impl.protocols :as protocols]))
+ [clojure.core.async.impl.protocols :as protocols]
+ [sci.impl.vars :as vars]))
+
+(def ^java.util.concurrent.Executor executor @#'async/thread-macro-executor)
+
+(defn thread-call
+ "Executes f in another thread, returning immediately to the calling
+ thread. Returns a channel which will receive the result of calling
+ f when completed, then close."
+ [f]
+ (let [c (async/chan 1)]
+ (let [binds (vars/get-thread-binding-frame)]
+ (.execute executor
+ (fn []
+ (vars/reset-thread-binding-frame binds)
+ (try
+ (let [ret (f)]
+ (when-not (nil? ret)
+ (async/>!! c ret)))
+ (finally
+ (async/close! c))))))
+ c))
(defn thread
[_ _ & body]
`(~'clojure.core.async/thread-call (fn [] ~@body)))
+(defn alt!!
+ "Like alt!, except as if by alts!!, will block until completed, and
+ not intended for use in (go ...) blocks."
+ [_ _ & clauses]
+ (async/do-alt 'clojure.core.async/alts!! clauses))
+
+(defn go-loop
+ [_ _ bindings & body]
+ (list 'clojure.core.async/thread (list* 'loop bindings body)))
+
(def async-namespace
{'!! async/>!!
'admix async/admix
'alts! async/alts!
'alts!! async/alts!!
+ 'alt!! (with-meta alt!! {:sci/macro true})
'buffer async/buffer
'chan async/chan
'close! async/close!
@@ -53,7 +85,7 @@
'take! async/take!
'tap async/tap
'thread (with-meta thread {:sci/macro true})
- 'thread-call async/thread-call
+ 'thread-call thread-call
'timeout async/timeout
'to-chan async/to-chan
'toggle async/toggle
@@ -65,7 +97,13 @@
'unsub async/unsub
'unsub-all async/unsub-all
'untap async/untap
- 'untap-all async/untap-all})
+ 'untap-all async/untap-all
+ ;; polyfill
+ 'go (with-meta thread {:sci/macro true})
+ '! async/>!!
+ 'alt! (with-meta alt!! {:sci/macro true})
+ 'go-loop (with-meta go-loop {:sci/macro true})})
(def async-protocols-namespace
{'ReadPort protocols/ReadPort})
diff --git a/src/babashka/impl/bencode.clj b/src/babashka/impl/bencode.clj
new file mode 100644
index 00000000..3d452eb6
--- /dev/null
+++ b/src/babashka/impl/bencode.clj
@@ -0,0 +1,11 @@
+(ns babashka.impl.bencode
+ {:no-doc true}
+ (:require [babashka.impl.bencode.core :as bencode]
+ [sci.impl.namespaces :refer [copy-var]]
+ [sci.impl.vars :as vars]))
+
+(def tns (vars/->SciNamespace 'bencode.core nil))
+
+(def bencode-namespace
+ {'read-bencode (copy-var bencode/read-bencode tns)
+ 'write-bencode (copy-var bencode/write-bencode tns)})
diff --git a/src/babashka/impl/bencode/core.clj b/src/babashka/impl/bencode/core.clj
new file mode 100644
index 00000000..20e24e5b
--- /dev/null
+++ b/src/babashka/impl/bencode/core.clj
@@ -0,0 +1,420 @@
+(ns babashka.impl.bencode.core
+ "A netstring and bencode implementation for Clojure."
+ {:author "Meikel Brandmeyer"
+ :no-doc true}
+ (:require [clojure.java.io :as io])
+ (:import [java.io ByteArrayOutputStream
+ EOFException
+ InputStream
+ IOException
+ OutputStream
+ PushbackInputStream]))
+
+;; Copyright (c) Meikel Brandmeyer. All rights reserved.
+;; The use and distribution terms for this software are covered by the
+;; Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php)
+;; which can be found in the file epl-v10.html at the root of this distribution.
+;; By using this software in any fashion, you are agreeing to be bound by
+;; the terms of this license.
+;; You must not remove this notice, or any other, from this software.
+
+;; # Motivation
+;;
+;; In each and every application, which contacts peer processes via some
+;; communication channel, the handling of the communication channel is
+;; obviously a central part of the application. Unfortunately introduces
+;; handling of buffers of varying sizes often bugs in form of buffer
+;; overflows and similar.
+;;
+;; A strong factor in this situation is of course the protocol which goes
+;; over the wire. Depending on its design it might be difficult to estimate
+;; the size of the input up front. This introduces more handling of message
+;; buffers to accomodate for inputs of varying sizes. This is particularly
+;; difficult in languages like C, where there is no bounds checking of array
+;; accesses and where errors might go unnoticed for considerable amount of
+;; time.
+;;
+;; To address these issues D. Bernstein developed the so called
+;; [netstrings][net]. They are especially designed to allow easy construction
+;; of the message buffers, easy and robust parsing.
+;;
+;; BitTorrent extended this to the [bencode][bc] protocol which also
+;; includes ways to encode numbers and collections like lists or maps.
+;;
+;; *wire* is based on these ideas.
+;;
+;; [net]: http://cr.yp.to/proto/netstrings.txt
+;; [bc]: http://wiki.theory.org/BitTorrentSpecification#Bencoding
+;;
+;; # Netstrings
+;;
+;; Now let's start with the basic netstrings. They consist of a byte count,
+;; followed a colon and the binary data and a trailing comma. Examples:
+;;
+;; 13:Hello, World!,
+;; 10:Guten Tag!,
+;; 0:,
+;;
+;; The initial byte count allows to efficiently allocate a sufficiently
+;; sized message buffer. The trailing comma serves as a hint to detect
+;; incorrect netstrings.
+;;
+;; ## Low-level reading
+;;
+;; We will need some low-level reading helpers to read the bytes from
+;; the input stream. These are `read-byte` as well as `read-bytes`. They
+;; are split out, because doing such a simple task as reading a byte is
+;; mild catastrophe in Java. So it would add some clutter to the algorithm
+;; `read-netstring`.
+;;
+;; On the other hand they might be also useful elsewhere.
+;;
+;; To remove some magic numbers from the code below.
+
+(set! *warn-on-reflection* true)
+
+(def #^{:const true} i 105)
+(def #^{:const true} l 108)
+(def #^{:const true} d 100)
+(def #^{:const true} comma 44)
+(def #^{:const true} minus 45)
+
+;; These two are only used boxed. So we keep them extra here.
+
+(def e 101)
+(def colon 58)
+
+(defn #^{:private true} read-byte
+ #^long [#^InputStream input]
+ (let [c (.read input)]
+ (when (neg? c)
+ (throw (EOFException. "Invalid netstring. Unexpected end of input.")))
+ ;; Here we have a quirk for example. `.read` returns -1 on end of
+ ;; input. However the Java `Byte` has only a range from -128 to 127.
+ ;; How does the fit together?
+ ;;
+ ;; The whole thing is shifted. `.read` actually returns an int
+ ;; between zero and 255. Everything below the value 128 stands
+ ;; for itself. But larger values are actually negative byte values.
+ ;;
+ ;; So we have to do some translation here. `Byte/byteValue` would
+ ;; do that for us, but we want to avoid boxing here.
+ (if (< 127 c) (- c 256) c)))
+
+(defn #^{:private true :tag "[B"} read-bytes
+ #^Object [#^InputStream input n]
+ (let [content (byte-array n)]
+ (loop [offset (int 0)
+ len (int n)]
+ (let [result (.read input content offset len)]
+ (when (neg? result)
+ (throw
+ (EOFException.
+ "Invalid netstring. Less data available than expected.")))
+ (when (not= result len)
+ (recur (+ offset result) (- len result)))))
+ content))
+
+;; `read-long` is used for reading integers from the stream as well
+;; as the byte count prefixes of byte strings. The delimiter is \:
+;; for byte count prefixes and \e for integers.
+
+(defn #^{:private true} read-long
+ #^long [#^InputStream input delim]
+ (loop [n (long 0)]
+ ;; We read repeatedly a byte from the input…
+ (let [b (read-byte input)]
+ ;; …and stop at the delimiter.
+ (cond
+ (= b minus) (- (read-long input delim))
+ (= b delim) n
+ :else (recur (+ (* n (long 10)) (- (long b) (long 48))))))))
+
+;; ## Reading a netstring
+;;
+;; Let's dive straight into reading a netstring from an `InputStream`.
+;;
+;; For convenience we split the function into two subfunctions. The
+;; public `read-netstring` is the normal entry point, which also checks
+;; for the trailing comma after reading the payload data with the
+;; private `read-netstring*`.
+;;
+;; The reason we need the less strict `read-netstring*` is that with
+;; bencode we don't have a trailing comma. So a check would not be
+;; beneficial here.
+;;
+;; However the consumer doesn't have to care. `read-netstring` as
+;; well as `read-bencode` provide the public entry points, which do
+;; the right thing. Although they both may reference the `read-netstring*`
+;; underneath.
+;;
+;; With this in mind we define the inner helper function first.
+
+(declare #^"[B" string>payload
+ #^String stringpayload` and `stringpayload
+ [#^String s]
+ (.getBytes s "UTF-8"))
+
+(defn #^{:private true :tag String} stringpayload (str (alength content))))
+ (.write (int colon))
+ (.write content)))
+
+(defn write-netstring
+ "Write the given binary data to the output stream in form of a classic
+ netstring."
+ [#^OutputStream output content]
+ (doto output
+ (write-netstring* content)
+ (.write (int comma))))
+
+;; # Bencode
+;;
+;; However most of the time we don't want to send simple blobs of data
+;; back and forth. The data sent between the communication peers usually
+;; have some structure, which has to be carried along the way to the
+;; other side. Here [bencode][bc] come into play.
+;;
+;; Bencode defines additionally to netstrings easily parseable structures
+;; for lists, maps and numbers. It allows to communicate information
+;; about the data structure to the peer on the other side.
+;;
+;; ## Tokens
+;;
+;; The data is encoded in tokens in bencode. There are several types of
+;; tokens:
+;;
+;; * A netstring without trailing comma for string data.
+;; * A tag specifiyng the type of the following tokens.
+;; The tag may be one of these:
+;; * `\i` to encode integers.
+;; * `\l` to encode lists of items.
+;; * `\d` to encode maps of item pairs.
+;; * `\e` to end the a previously started tag.
+;;
+;; ## Reading bencode
+;;
+;; Reading bencode encoded data is basically parsing a stream of tokens
+;; from the input. Hence we need a read-token helper which allows to
+;; retrieve the next token.
+
+(defn #^{:private true} read-token
+ [#^PushbackInputStream input]
+ (let [ch (read-byte input)]
+ (cond
+ (= (long e) ch) nil
+ (= i ch) :integer
+ (= l ch) :list
+ (= d ch) :map
+ :else (do
+ (.unread input (int ch))
+ (read-netstring* input)))))
+
+;; To read the bencode encoded data we walk a long the sequence of tokens
+;; and act according to the found tags.
+
+(declare read-integer read-list read-map)
+
+(defn read-bencode
+ "Read bencode token from the input stream."
+ [input]
+ (let [token (read-token input)]
+ (case token
+ :integer (read-integer input)
+ :list (read-list input)
+ :map (read-map input)
+ token)))
+
+;; Of course integers and the collection types are have to treated specially.
+;;
+;; Integers for example consist of a sequence of decimal digits.
+
+(defn #^{:private true} read-integer
+ [input]
+ (read-long input e))
+
+;; *Note:* integers are an ugly special case, which cannot be
+;; handled with `read-token` or `read-netstring*`.
+;;
+;; Lists are just a sequence of other tokens.
+
+(declare token-seq)
+
+(defn #^{:private true} read-list
+ [input]
+ (vec (token-seq input)))
+
+;; Maps are sequences of key/value pairs. The keys are always
+;; decoded into strings. The values are kept as is.
+
+(defn #^{:private true} read-map
+ [input]
+ (->> (token-seq input)
+ (into {} (comp (partition-all 2)
+ (map (fn [[k v]]
+ [(string> #(read-bencode input)
+ repeatedly
+ (take-while identity)))
+
+;; ## Writing bencode
+;;
+;; Writing bencode is similar easy as reading it. The main entry point
+;; takes a string, map, sequence or integer and writes it according to
+;; the rules to the given OutputStream.
+
+(defmulti write-bencode
+ "Write the given thing to the output stream. “Thing” means here a
+ string, map, sequence or integer. Alternatively an ByteArray may
+ be provided whose contents are written as a bytestring. Similar
+ the contents of a given InputStream are written as a byte string.
+ Named things (symbols or keywords) are written in the form
+ 'namespace/name'."
+ (fn [_output thing]
+ (cond
+ (bytes? thing) :bytes
+ (instance? InputStream thing) :input-stream
+ (integer? thing) :integer
+ (string? thing) :string
+ (symbol? thing) :named
+ (keyword? thing) :named
+ (map? thing) :map
+ (or (nil? thing) (coll? thing) (.isArray (class thing))) :list
+ :else (type thing))))
+
+(defmethod write-bencode :default
+ [output x]
+ (throw (IllegalArgumentException. (str "Cannot write value of type " (class x)))))
+
+;; The following methods should be pretty straight-forward.
+;;
+;; The easiest case is of course when we already have a byte array.
+;; We can simply pass it on to the underlying machinery.
+
+(defmethod write-bencode :bytes
+ [output bytes]
+ (write-netstring* output bytes))
+
+;; For strings we simply write the string as a netstring without
+;; trailing comma after encoding the string as UTF-8 bytes.
+
+(defmethod write-bencode :string
+ [output string]
+ (write-netstring* output (string>payload string)))
+
+;; Streaming does not really work, since we need to know the
+;; number of bytes to write upfront. So we read in everything
+;; for InputStreams and pass on the byte array.
+
+(defmethod write-bencode :input-stream
+ [output stream]
+ (let [bytes (ByteArrayOutputStream.)]
+ (io/copy stream bytes)
+ (write-netstring* output (.toByteArray bytes))))
+
+;; Integers are again the ugly special case.
+
+(defmethod write-bencode :integer
+ [#^OutputStream output n]
+ (doto output
+ (.write (int i))
+ (.write (string>payload (str n)))
+ (.write (int e))))
+
+;; Symbols and keywords are converted to a string of the
+;; form 'namespace/name' or just 'name' in case its not
+;; qualified. We do not add colons for keywords since the
+;; other side might not have the notion of keywords.
+
+(defmethod write-bencode :named
+ [output thing]
+ (let [nspace (namespace thing)
+ name (name thing)]
+ (->> (str (when nspace (str nspace "/")) name)
+ string>payload
+ (write-netstring* output))))
+
+;; Lists as well as maps work recursively to print their elements.
+
+(defmethod write-bencode :list
+ [#^OutputStream output lst]
+ (.write output (int l))
+ (doseq [elt lst]
+ (write-bencode output elt))
+ (.write output (int e)))
+
+;; However, maps are a bit special because their keys are sorted
+;; lexicographically based on their byte string represantation.
+
+(declare lexicographically)
+
+(defmethod write-bencode :map
+ [#^OutputStream output m]
+ (let [translation (into {} (map (juxt string>payload identity) (keys m)))
+ key-strings (sort lexicographically (keys translation))
+ >value (comp m translation)]
+ (.write output (int d))
+ (doseq [k key-strings]
+ (write-netstring* output k)
+ (write-bencode output (>value k)))
+ (.write output (int e))))
+
+;; However, since byte arrays are not `Comparable` we need a custom
+;; comparator which we can feed to `sort`.
+
+(defn #^{:private true} lexicographically
+ [#^"[B" a #^"[B" b]
+ (let [alen (alength a)
+ blen (alength b)
+ len (min alen blen)]
+ (loop [i 0]
+ (if (== i len)
+ (- alen blen)
+ (let [x (- (int (aget a i)) (int (aget b i)))]
+ (if (zero? x)
+ (recur (inc i))
+ x))))))
diff --git a/src/babashka/impl/classes.clj b/src/babashka/impl/classes.clj
index c6696613..43308368 100644
--- a/src/babashka/impl/classes.clj
+++ b/src/babashka/impl/classes.clj
@@ -1,19 +1,11 @@
(ns babashka.impl.classes
{:no-doc true}
(:require
- [cheshire.core :as json]
- #_[clojure.string :as str]))
-
-;; (def os-name (str/lower-case (System/getProperty "os.name")))
-;; (def os (cond (str/includes? os-name "mac") :mac
-;; (or (str/includes? os-name "nix")
-;; (str/includes? os-name "nux")) :linux
-;; (str/includes? os-name "win") :windows))
-;; (def unix-like? (or (identical? os :linux)
-;; (identical? os :mac)))
+ [cheshire.core :as json]))
(def classes
- '{:all [java.io.BufferedReader
+ `{:all [clojure.lang.ExceptionInfo
+ java.io.BufferedReader
java.io.BufferedWriter
java.io.ByteArrayInputStream
java.io.ByteArrayOutputStream
@@ -21,19 +13,27 @@
java.io.InputStream
java.io.IOException
java.io.OutputStream
+ java.io.FileReader
+ java.io.PushbackInputStream
java.io.Reader
+ java.io.SequenceInputStream
java.io.StringReader
java.io.StringWriter
java.io.Writer
java.lang.ArithmeticException
java.lang.AssertionError
java.lang.Boolean
+ java.lang.Byte
+ java.lang.Comparable
java.lang.Class
java.lang.Double
java.lang.Exception
java.lang.Integer
java.lang.Long
+ java.lang.NumberFormatException
java.lang.Math
+ java.lang.Runtime
+ java.lang.RuntimeException
java.util.concurrent.LinkedBlockingQueue
java.lang.Object
java.lang.String
@@ -44,15 +44,21 @@
java.lang.ProcessBuilder
java.lang.ProcessBuilder$Redirect
java.math.BigInteger
- java.net.URI
+ java.net.DatagramSocket
+ java.net.DatagramPacket
java.net.HttpURLConnection
+ java.net.InetAddress
java.net.ServerSocket
java.net.Socket
java.net.UnknownHostException
+ java.net.URI
+ ;; java.net.URL, see below
java.net.URLEncoder
java.net.URLDecoder
java.nio.file.CopyOption
java.nio.file.FileAlreadyExistsException
+ java.nio.file.FileSystem
+ java.nio.file.FileSystems
java.nio.file.Files
java.nio.file.LinkOption
java.nio.file.NoSuchFileException
@@ -83,18 +89,24 @@
java.time.ZonedDateTime
java.time.ZoneId
java.time.ZoneOffset
+ java.time.temporal.ChronoUnit
java.time.temporal.TemporalAccessor
java.util.regex.Pattern
java.util.Base64
java.util.Base64$Decoder
java.util.Base64$Encoder
java.util.Date
+ java.util.MissingResourceException
+ java.util.Properties
java.util.UUID
java.util.concurrent.TimeUnit
java.util.zip.InflaterInputStream
java.util.zip.DeflaterInputStream
java.util.zip.GZIPInputStream
- java.util.zip.GZIPOutputStream]
+ java.util.zip.GZIPOutputStream
+ org.yaml.snakeyaml.error.YAMLException
+ ~(symbol "[B")
+ ]
:constructors [clojure.lang.Delay
clojure.lang.MapEntry
clojure.lang.LineNumberingPushbackReader
@@ -104,8 +116,7 @@
:methods [borkdude.graal.LockFix ;; support for locking
]
:fields [clojure.lang.PersistentQueue]
- :instance-checks [clojure.lang.ExceptionInfo
- clojure.lang.IObj
+ :instance-checks [clojure.lang.IObj
clojure.lang.IEditableCollection]
:custom {clojure.lang.LineNumberingPushbackReader {:allPublicConstructors true
:allPublicMethods true}
@@ -197,7 +208,13 @@
(instance? java.io.ByteArrayOutputStream v)
java.io.ByteArrayOutputStream
(instance? java.security.MessageDigest v)
- java.security.MessageDigest)))))
+ java.security.MessageDigest
+ (instance? java.io.InputStream v)
+ java.io.InputStream
+ (instance? java.io.OutputStream v)
+ java.io.OutputStream
+ (instance? java.nio.file.FileSystem v)
+ java.nio.file.FileSystem)))))
(def class-map (gen-class-map))
diff --git a/src/babashka/impl/clojure/core.clj b/src/babashka/impl/clojure/core.clj
index bb7502af..c7e9452a 100644
--- a/src/babashka/impl/clojure/core.clj
+++ b/src/babashka/impl/clojure/core.clj
@@ -1,7 +1,9 @@
(ns babashka.impl.clojure.core
{:no-doc true}
- (:refer-clojure :exclude [future])
- (:require [borkdude.graal.locking :as locking]))
+ (:refer-clojure :exclude [future read read-string])
+ (:require [borkdude.graal.locking :as locking]
+ [sci.core :as sci]
+ [sci.impl.namespaces :refer [copy-core-var]]))
(defn locking* [form bindings v f & args]
(apply @#'locking/locking form bindings v f args))
@@ -16,17 +18,17 @@
ret#))
(def core-extras
- {'file-seq file-seq
- 'agent agent
- 'instance? instance? ;; TODO: move to sci
- 'send send
- 'send-off send-off
- 'promise promise
- 'deliver deliver
+ {'file-seq (copy-core-var file-seq)
+ 'agent (copy-core-var agent)
+ 'send (copy-core-var send)
+ 'send-off (copy-core-var send-off)
+ 'promise (copy-core-var promise)
+ 'deliver (copy-core-var deliver)
'locking (with-meta locking* {:sci/macro true})
- 'shutdown-agents shutdown-agents
- 'slurp slurp
- 'spit spit
+ 'shutdown-agents (copy-core-var shutdown-agents)
+ 'slurp (copy-core-var slurp)
+ 'spit (copy-core-var spit)
'time (with-meta time* {:sci/macro true})
- 'Throwable->map Throwable->map
- 'compare-and-set! compare-and-set!})
+ 'Throwable->map (copy-core-var Throwable->map)
+ 'compare-and-set! (copy-core-var compare-and-set!)
+ '*data-readers* (sci/new-dynamic-var '*data-readers* nil)})
diff --git a/src/babashka/impl/clojure/core/server.clj b/src/babashka/impl/clojure/core/server.clj
index 113934a0..3de2f97e 100644
--- a/src/babashka/impl/clojure/core/server.clj
+++ b/src/babashka/impl/clojure/core/server.clj
@@ -1,5 +1,5 @@
-;; Modified / stripped version of clojure.core.server for use with babashka on
-;; GraalVM.
+;; Modified / stripped version of clojure.core.server for use with babashka on
+;; GraalVM.
;;
;; Copyright (c) Rich Hickey. All rights reserved.
;; The use and distribution terms for this software are covered by the
diff --git a/src/babashka/impl/clojure/main.clj b/src/babashka/impl/clojure/main.clj
index 3d8a9f2f..db26af93 100644
--- a/src/babashka/impl/clojure/main.clj
+++ b/src/babashka/impl/clojure/main.clj
@@ -48,6 +48,11 @@
*e nil]
~@body))
+(def ^{:doc "A sequence of lib specs that are applied to `require`
+by default when a new command-line REPL is started."} repl-requires
+ '[[clojure.repl :refer (dir doc)]
+ [clojure.pprint :refer (pprint)]])
+
(defn repl
"Generic, reusable, read-eval-print loop. By default, reads from *in*,
writes to *out*, and prints exception summaries to *err*. If you use the
diff --git a/src/babashka/impl/clojure/pprint.clj b/src/babashka/impl/clojure/pprint.clj
new file mode 100644
index 00000000..3d2a3695
--- /dev/null
+++ b/src/babashka/impl/clojure/pprint.clj
@@ -0,0 +1,12 @@
+(ns babashka.impl.clojure.pprint
+ {:no-doc true}
+ (:require [fipp.edn :as fipp]))
+
+(defn pprint
+ ([edn]
+ (fipp/pprint edn))
+ ([edn writer]
+ (fipp/pprint edn {:writer writer})))
+
+(def pprint-namespace
+ {'pprint pprint})
diff --git a/src/babashka/impl/clojure/stacktrace.clj b/src/babashka/impl/clojure/stacktrace.clj
index 96fd9f32..07f19ef1 100644
--- a/src/babashka/impl/clojure/stacktrace.clj
+++ b/src/babashka/impl/clojure/stacktrace.clj
@@ -1,88 +1,16 @@
-;; Copyright (c) Rich Hickey. All rights reserved.
-;; The use and distribution terms for this software are covered by the
-;; Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php)
-;; which can be found in the file epl-v10.html at the root of this distribution.
-;; By using this software in any fashion, you are agreeing to be bound by
-;; the terms of this license.
-;; You must not remove this notice, or any other, from this software.
+(ns babashka.impl.clojure.stacktrace
+ {:no-doc true}
+ (:require [clojure.stacktrace :as stacktrace]
+ [sci.core :as sci]))
-;;; stacktrace.clj: print Clojure-centric stack traces
-
-;; by Stuart Sierra
-;; January 6, 2009
-
-(ns ^{:doc "Print stack traces oriented towards Clojure, not Java."
- :author "Stuart Sierra"
- :no-doc true}
- babashka.impl.clojure.stacktrace)
-
-(set! *warn-on-reflection* true)
-
-(defn root-cause
- "Returns the last 'cause' Throwable in a chain of Throwables."
- {:added "1.1"}
- [^Throwable tr]
- (if-let [cause (.getCause tr)]
- (recur cause)
- tr))
-
-(defn print-trace-element
- "Prints a Clojure-oriented view of one element in a stack trace."
- {:added "1.1"}
- [^StackTraceElement e]
- (let [class (.getClassName e)
- method (.getMethodName e)]
- (let [match (re-matches #"^([A-Za-z0-9_.-]+)\$(\w+)__\d+$" (str class))]
- (if (and match (= "invoke" method))
- (apply printf "%s/%s" (rest match))
- (printf "%s.%s" class method))))
- (printf " (%s:%d)" (or (.getFileName e) "") (.getLineNumber e)))
-
-(defn print-throwable
- "Prints the class and message of a Throwable. Prints the ex-data map
- if present."
- {:added "1.1"}
- [^Throwable tr]
- (printf "%s: %s" (.getName (class tr)) (.getMessage tr))
- (when-let [info (ex-data tr)]
- (newline)
- (pr info)))
-
-(defn print-stack-trace
- "Prints a Clojure-oriented stack trace of tr, a Throwable.
- Prints a maximum of n stack frames (default: unlimited).
- Does not print chained exceptions (causes)."
- {:added "1.1"}
- ([tr] (print-stack-trace tr nil))
- ([^Throwable tr n]
- (let [st (.getStackTrace tr)]
- (print-throwable tr)
- (newline)
- (print " at ")
- (if-let [e (first st)]
- (print-trace-element e)
- (print "[empty stack trace]"))
- (newline)
- (doseq [e (if (nil? n)
- (rest st)
- (take (dec n) (rest st)))]
- (print " ")
- (print-trace-element e)
- (newline)))))
-
-(defn print-cause-trace
- "Like print-stack-trace but prints chained exceptions (causes)."
- {:added "1.1"}
- ([tr] (print-cause-trace tr nil))
- ([^Throwable tr n]
- (print-stack-trace tr n)
- (when-let [cause (.getCause tr)]
- (print "Caused by: " )
- (recur cause n))))
+(defmacro wrap-out [f]
+ `(fn [& ~'args]
+ (binding [*out* @sci/out]
+ (apply ~f ~'args))))
(def stacktrace-namespace
- {'root-cause root-cause
- 'print-trace-element print-trace-element
- 'print-throwable print-throwable
- 'print-stack-trace print-stack-trace
- 'print-cause-trace print-cause-trace})
+ {'root-cause stacktrace/root-cause
+ 'print-trace-element (wrap-out stacktrace/print-trace-element)
+ 'print-throwable (wrap-out stacktrace/print-throwable)
+ 'print-stack-trace (wrap-out stacktrace/print-stack-trace)
+ 'print-cause-trace (wrap-out stacktrace/print-cause-trace)})
diff --git a/src/babashka/impl/clojure/test.clj b/src/babashka/impl/clojure/test.clj
index 60fcec78..97406f5d 100644
--- a/src/babashka/impl/clojure/test.clj
+++ b/src/babashka/impl/clojure/test.clj
@@ -1,10 +1,10 @@
- ; Copyright (c) Rich Hickey. All rights reserved.
- ; The use and distribution terms for this software are covered by the
- ; Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php)
- ; which can be found in the file epl-v10.html at the root of this distribution.
- ; By using this software in any fashion, you are agreeing to be bound by
- ; the terms of this license.
- ; You must not remove this notice, or any other, from this software.
+;; Copyright (c) Rich Hickey. All rights reserved.
+;; The use and distribution terms for this software are covered by the
+;; Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php)
+;; which can be found in the file epl-v10.html at the root of this distribution.
+;; By using this software in any fashion, you are agreeing to be bound by
+;; the terms of this license.
+;; You must not remove this notice, or any other, from this software.
;;; test.clj: test framework for Clojure
@@ -232,9 +232,8 @@
For additional event types, see the examples in the code.
"}
babashka.impl.clojure.test
- (:require [babashka.impl.clojure.stacktrace :as stack]
- [babashka.impl.common :refer [ctx]]
- [clojure.string :as str]
+ (:require [babashka.impl.common :refer [ctx]]
+ [clojure.stacktrace :as stack]
[clojure.template :as temp]
[sci.core :as sci]
[sci.impl.analyzer :as ana]
@@ -430,9 +429,9 @@
result# (apply ~pred values#)]
(if result#
(clojure.test/do-report {:type :pass, :message ~msg,
- :expected '~form, :actual (cons ~pred values#)})
+ :expected '~form, :actual (cons ~pred values#)})
(clojure.test/do-report {:type :fail, :message ~msg,
- :expected '~form, :actual (list '~'not (cons '~pred values#))}))
+ :expected '~form, :actual (list '~'not (cons '~pred values#))}))
result#)))
(defn assert-any
@@ -443,9 +442,9 @@
`(let [value# ~form]
(if value#
(clojure.test/do-report {:type :pass, :message ~msg,
- :expected '~form, :actual value#})
+ :expected '~form, :actual value#})
(clojure.test/do-report {:type :fail, :message ~msg,
- :expected '~form, :actual value#}))
+ :expected '~form, :actual value#}))
value#))
@@ -479,9 +478,9 @@
(let [result# (instance? klass# object#)]
(if result#
(clojure.test/do-report {:type :pass, :message ~msg,
- :expected '~form, :actual (class object#)})
+ :expected '~form, :actual (class object#)})
(clojure.test/do-report {:type :fail, :message ~msg,
- :expected '~form, :actual (class object#)}))
+ :expected '~form, :actual (class object#)}))
result#)))
(defmethod assert-expr 'thrown? [msg form]
@@ -492,10 +491,10 @@
body (nthnext form 2)]
`(try ~@body
(clojure.test/do-report {:type :fail, :message ~msg,
- :expected '~form, :actual nil})
+ :expected '~form, :actual nil})
(catch ~klass e#
(clojure.test/do-report {:type :pass, :message ~msg,
- :expected '~form, :actual e#})
+ :expected '~form, :actual e#})
e#))))
(defmethod assert-expr 'thrown-with-msg? [msg form]
@@ -512,7 +511,7 @@
(let [m# (.getMessage e#)]
(if (re-find ~re m#)
(clojure.test/do-report {:type :pass, :message ~msg,
- :expected '~form, :actual e#})
+ :expected '~form, :actual e#})
(clojure.test/do-report {:type :fail, :message ~msg,
:expected '~form, :actual e#})))
e#))))
diff --git a/src/babashka/impl/curl.clj b/src/babashka/impl/curl.clj
new file mode 100644
index 00000000..cf24bc60
--- /dev/null
+++ b/src/babashka/impl/curl.clj
@@ -0,0 +1,16 @@
+(ns babashka.impl.curl
+ {:no-doc true}
+ (:require [babashka.curl :as curl]
+ [sci.impl.namespaces :refer [copy-var]]
+ [sci.impl.vars :as vars]))
+
+(def tns (vars/->SciNamespace 'babashka.curl nil))
+
+(def curl-namespace
+ {'request (copy-var curl/request tns)
+ 'get (copy-var curl/get tns)
+ 'patch (copy-var curl/patch tns)
+ 'post (copy-var curl/post tns)
+ 'put (copy-var curl/put tns)
+ 'head (copy-var curl/head tns)
+ 'curl-command (copy-var curl/curl-command tns)})
diff --git a/src/babashka/impl/nrepl_server.clj b/src/babashka/impl/nrepl_server.clj
new file mode 100644
index 00000000..7e1fbf4d
--- /dev/null
+++ b/src/babashka/impl/nrepl_server.clj
@@ -0,0 +1,198 @@
+(ns babashka.impl.nrepl-server
+ {:no-doc true}
+ (:refer-clojure :exclude [send future binding])
+ (:require [babashka.impl.bencode.core :refer [read-bencode]]
+ [babashka.impl.nrepl-server.utils :refer [dev? response-for send send-exception
+ replying-print-writer]]
+ [clojure.string :as str]
+ [clojure.tools.reader.reader-types :as r]
+ [sci.core :as sci]
+ [sci.impl.interpreter :refer [eval-string* eval-form]]
+ [sci.impl.parser :as p]
+ [sci.impl.utils :as sci-utils]
+ [sci.impl.vars :as vars])
+ (:import [java.io InputStream PushbackInputStream EOFException BufferedOutputStream]
+ [java.net ServerSocket]))
+
+(set! *warn-on-reflection* true)
+
+(defn eval-msg [ctx o msg]
+ (try
+ (let [code-str (get msg :code)
+ reader (r/indexing-push-back-reader (r/string-push-back-reader code-str))
+ ns-str (get msg :ns)
+ sci-ns (when ns-str (sci-utils/namespace-object (:env ctx) (symbol ns-str) true nil))]
+ (when @dev? (println "current ns" (vars/current-ns-name)))
+ (sci/with-bindings (cond-> {}
+ sci-ns (assoc vars/current-ns sci-ns))
+ (loop []
+ (let [pw (replying-print-writer o msg)
+ form (p/parse-next ctx reader)
+ value (if (identical? :edamame.impl.parser/eof form) ::nil
+ (sci/with-bindings {sci/out pw}
+ (eval-form ctx form)))
+ env (:env ctx)]
+ (swap! env update-in [:namespaces 'clojure.core]
+ (fn [core]
+ (assoc core
+ '*1 value
+ '*2 (get core '*1)
+ '*3 (get core '*2))))
+ (send o (response-for msg (cond-> {"ns" (vars/current-ns-name)}
+ (not (identical? value ::nil)) (assoc "value" (pr-str value)))))
+ (when (not (identical? ::nil value))
+ (recur)))))
+ (send o (response-for msg {"status" #{"done"}})))
+ (catch Exception ex
+ (swap! (:env ctx) update-in [:namespaces 'clojure.core]
+ assoc '*e ex)
+ (send-exception o msg ex))))
+
+(defn fully-qualified-syms [ctx ns-sym]
+ (let [syms (eval-string* ctx (format "(keys (ns-map '%s))" ns-sym))
+ sym-strs (map #(str "`" %) syms)
+ sym-expr (str "[" (str/join " " sym-strs) "]")
+ syms (eval-string* ctx sym-expr)]
+ syms))
+
+(defn match [_alias->ns ns->alias query [sym-ns sym-name qualifier]]
+ (let [pat (re-pattern query)]
+ (or (when (and (identical? :unqualified qualifier) (re-find pat sym-name))
+ [sym-ns sym-name])
+ (when sym-ns
+ (or (when (re-find pat (str (get ns->alias (symbol sym-ns)) "/" sym-name))
+ [sym-ns (str (get ns->alias (symbol sym-ns)) "/" sym-name)])
+ (when (re-find pat (str sym-ns "/" sym-name))
+ [sym-ns (str sym-ns "/" sym-name)]))))))
+
+(defn complete [ctx o msg]
+ (try
+ (let [ns-str (get msg :ns)
+ sci-ns (when ns-str
+ (sci-utils/namespace-object (:env ctx) (symbol ns-str) nil false))]
+ (sci/binding [vars/current-ns (or sci-ns @vars/current-ns)]
+ (let [query (:symbol msg)
+ from-current-ns (fully-qualified-syms ctx (eval-string* ctx "(ns-name *ns*)"))
+ from-current-ns (map (fn [sym]
+ [(namespace sym) (name sym) :unqualified])
+ from-current-ns)
+ alias->ns (eval-string* ctx "(let [m (ns-aliases *ns*)] (zipmap (keys m) (map ns-name (vals m))))")
+ ns->alias (zipmap (vals alias->ns) (keys alias->ns))
+ from-aliased-nss (doall (mapcat
+ (fn [alias]
+ (let [ns (get alias->ns alias)
+ syms (eval-string* ctx (format "(keys (ns-publics '%s))" ns))]
+ (map (fn [sym]
+ [(str ns) (str sym) :qualified])
+ syms)))
+ (keys alias->ns)))
+ svs (concat from-current-ns from-aliased-nss)
+ completions (keep (fn [entry]
+ (match alias->ns ns->alias query entry))
+ svs)
+ completions (mapv (fn [[namespace name]]
+ {"candidate" (str name) "ns" (str namespace) #_"type" #_"function"})
+ completions)]
+ (when @dev? (prn "completions" completions))
+ (send o (response-for msg {"completions" completions
+ "status" #{"done"}})))))
+ (catch Throwable e
+ (println e)
+ (send o (response-for msg {"completions" []
+ "status" #{"done"}})))))
+
+(defn close-session [ctx msg _is os]
+ (let [session (:session msg)]
+ (swap! (:sessions ctx) disj session))
+ (send os (response-for msg {"status" #{"done" "session-closed"}})))
+
+(defn ls-sessions [ctx msg os]
+ (let [sessions @(:sessions ctx)]
+ (send os (response-for msg {"sessions" sessions
+ "status" #{"done"}}))))
+
+(defn read-msg [msg]
+ (-> (zipmap (map keyword (keys msg))
+ (map #(if (bytes? %)
+ (String. (bytes %))
+ %) (vals msg)))
+ (update :op keyword)))
+
+(defn session-loop [ctx ^InputStream is os id]
+ (when @dev? (println "Reading!" id (.available is)))
+ (when-let [msg (try (read-bencode is)
+ (catch EOFException _
+ (println "Client closed connection.")))]
+ (let [msg (read-msg msg)]
+ (when @dev? (prn "Received" msg))
+ (case (get msg :op)
+ :clone (do
+ (when @dev? (println "Cloning!"))
+ (let [id (str (java.util.UUID/randomUUID))]
+ (swap! (:sessions ctx) (fnil conj #{}) id)
+ (send os (response-for msg {"new-session" id "status" #{"done"}}))
+ (recur ctx is os id)))
+ :close (do (close-session ctx msg is os)
+ (recur ctx is os id))
+ :eval (do
+ (eval-msg ctx os msg)
+ (recur ctx is os id))
+ :load-file (let [file (:file msg)
+ msg (assoc msg :code file)]
+ (eval-msg ctx os msg)
+ (recur ctx is os id))
+ :complete (do
+ (complete ctx os msg)
+ (recur ctx is os id))
+ :describe
+ (do (send os (response-for msg {"status" #{"done"}
+ "ops" (zipmap #{"clone" "close" "eval" "load-file"
+ "complete" "describe" "ls-sessions"}
+ (repeat {}))}))
+ (recur ctx is os id))
+ :ls-sessions (do (ls-sessions ctx msg os)
+ (recur ctx is os id))
+ ;; fallback
+ (do (when @dev?
+ (println "Unhandled message" msg))
+ (send os (response-for msg {"status" #{"error" "unknown-op" "done"}}))
+ (recur ctx is os id))))))
+
+(defn listen [ctx ^ServerSocket listener]
+ (when @dev? (println "Listening"))
+ (let [client-socket (.accept listener)
+ in (.getInputStream client-socket)
+ in (PushbackInputStream. in)
+ out (.getOutputStream client-socket)
+ out (BufferedOutputStream. out)]
+ (when @dev? (println "Connected."))
+ (sci/future
+ (sci/binding
+ ;; allow *ns* to be set! inside future
+ [vars/current-ns (vars/->SciNamespace 'user nil)
+ sci/print-length @sci/print-length]
+ (session-loop ctx in out "pre-init")))
+ (recur ctx listener)))
+
+(def server (atom nil))
+
+(defn stop-server! []
+ (when-let [s @server]
+ (.close ^ServerSocket s)
+ (reset! server nil)))
+
+(defn start-server! [ctx host+port]
+ (vreset! dev? (= "true" (System/getenv "BABASHKA_DEV")))
+ (let [ctx (assoc ctx :sessions (atom #{}))
+ parts (str/split host+port #":")
+ [address port] (if (= 1 (count parts))
+ [nil (Integer. ^String (first parts))]
+ [(java.net.InetAddress/getByName (first parts))
+ (Integer. ^String (second parts))])
+ host+port (if-not address (str "localhost:" port)
+ host+port)
+ socket-server (new ServerSocket port 0 address)]
+ (println "Started nREPL server at" host+port)
+ (println "For more info visit https://github.com/borkdude/babashka/blob/master/doc/repl.md#nrepl.")
+ (reset! server socket-server)
+ (listen ctx socket-server)))
diff --git a/src/babashka/impl/nrepl_server/utils.clj b/src/babashka/impl/nrepl_server/utils.clj
new file mode 100644
index 00000000..46dca89f
--- /dev/null
+++ b/src/babashka/impl/nrepl_server/utils.clj
@@ -0,0 +1,63 @@
+(ns babashka.impl.nrepl-server.utils
+ {:no-doc true}
+ (:refer-clojure :exclude [send])
+ (:require [babashka.impl.bencode.core :refer [write-bencode]])
+ (:import [java.io Writer PrintWriter StringWriter OutputStream BufferedWriter]))
+
+(set! *warn-on-reflection* true)
+
+(def dev? (volatile! nil))
+
+(defn response-for [old-msg msg]
+ (let [session (get old-msg :session "none")
+ id (get old-msg :id "unknown")]
+ (assoc msg "session" session "id" id)))
+
+(defn send [^OutputStream os msg]
+ ;;(when @dev? (prn "Sending" msg))
+ (write-bencode os msg)
+ (.flush os))
+
+(defn send-exception [os msg ^Throwable ex]
+ (let [ex-map (Throwable->map ex)
+ ex-name (-> ex-map :via first :type)
+ cause (:cause ex-map)]
+ (when @dev? (prn "sending exception" ex-map))
+ (send os (response-for msg {"err" (str ex-name ": " cause "\n")}))
+ (send os (response-for msg {"ex" (str "class " ex-name)
+ "root-ex" (str "class " ex-name)
+ "status" #{"eval-error"}}))
+ (send os (response-for msg {"status" #{"done"}}))))
+
+;; from https://github.com/nrepl/nrepl/blob/1cc9baae631703c184894559a2232275dc50dff6/src/clojure/nrepl/middleware/print.clj#L63
+(defn- to-char-array
+ ^chars
+ [x]
+ (cond
+ (string? x) (.toCharArray ^String x)
+ (integer? x) (char-array [(char x)])
+ :else x))
+
+;; from https://github.com/nrepl/nrepl/blob/1cc9baae631703c184894559a2232275dc50dff6/src/clojure/nrepl/middleware/print.clj#L99
+(defn replying-print-writer
+ "Returns a `java.io.PrintWriter` suitable for binding as `*out*` or `*err*`. All
+ of the content written to that `PrintWriter` will be sent as messages on the
+ transport of `msg`, keyed by `key`."
+ ^java.io.PrintWriter
+ [o msg]
+ (-> (proxy [Writer] []
+ (write
+ ([x]
+ (let [cbuf (to-char-array x)]
+ (.write ^Writer this cbuf (int 0) (count cbuf))))
+ ([x off len]
+ (let [cbuf (to-char-array x)
+ text (str (doto (StringWriter.)
+ (.write cbuf ^int off ^int len)))]
+ (when (pos? (count text))
+ (when @dev? (println "out str:" text))
+ (send o (response-for msg {"out" text}))))))
+ (flush [])
+ (close []))
+ (BufferedWriter. 1024)
+ (PrintWriter. true)))
diff --git a/src/babashka/impl/pipe_signal_handler.clj b/src/babashka/impl/pipe_signal_handler.clj
index aee99151..e1bf83b2 100644
--- a/src/babashka/impl/pipe_signal_handler.clj
+++ b/src/babashka/impl/pipe_signal_handler.clj
@@ -9,8 +9,9 @@
(identical? :PIPE @pipe-state))
(defn handle-pipe! []
- (Signal/handle
- (Signal. "PIPE")
- (reify SignalHandler
- (handle [_ _]
- (vreset! pipe-state :PIPE)))))
+ (when-not (= "true" (System/getenv "BABASHKA_DISABLE_PIPE_SIGNAL_HANDLER"))
+ (Signal/handle
+ (Signal. "PIPE")
+ (reify SignalHandler
+ (handle [_ _]
+ (vreset! pipe-state :PIPE))))))
diff --git a/src/babashka/impl/repl.clj b/src/babashka/impl/repl.clj
index 12fb91a6..9e2674c8 100644
--- a/src/babashka/impl/repl.clj
+++ b/src/babashka/impl/repl.clj
@@ -5,17 +5,21 @@
[clojure.java.io :as io]
[clojure.string :as str]
[clojure.tools.reader.reader-types :as r]
- [sci.impl.interpreter :refer [eval-form]]
- [sci.impl.parser :as parser]
- [sci.impl.vars :as vars]
[sci.core :as sci]
- [sci.impl.io :as sio]))
+ [sci.impl.interpreter :refer [eval-form]]
+ [sci.impl.io :as sio]
+ [sci.impl.parser :as parser]
+ [sci.impl.vars :as vars]))
+
+(set! *warn-on-reflection* true)
(defn repl-caught
"Default :caught hook for repl"
- [e]
+ [^Throwable e]
(sci/with-bindings {sci/out @sci/err}
- (sio/println (.getMessage ^Exception e))
+ (sio/println (str (.. e getClass getName)
+ (when-let [m (.getMessage e)]
+ (str ": " m)) ))
(sio/flush)))
(defn repl
@@ -31,19 +35,15 @@
(sio/println "Use :repl/quit or :repl/exit to quit the REPL.")
(sio/println "Clojure rocks, Bash reaches.")
(sio/println)
- (eval-form sci-ctx '(require '[clojure.repl :refer [dir doc]]))))
+ (eval-form sci-ctx '(use 'clojure.repl))))
:read (or read
(fn [_request-prompt request-exit]
- ;; (prn "PEEK" @sci/in (r/peek-char @sci/in))
- ;; (prn "PEEK" @sci/in (r/peek-char @sci/in)) this works fine
- (if (r/peek-char in) ;; if this is nil, we reached EOF
- (let [v (parser/parse-next sci-ctx in)]
- (if (or (identical? :repl/quit v)
- (identical? :repl/exit v)
- (identical? :edamame.impl.parser/eof v))
- request-exit
- v))
- request-exit)))
+ (let [v (parser/parse-next sci-ctx in)]
+ (if (or (identical? :repl/quit v)
+ (identical? :repl/exit v)
+ (identical? :edamame.impl.parser/eof v))
+ request-exit
+ v))))
:eval (or eval
(fn [expr]
(let [ret (eval-form (update sci-ctx
diff --git a/src/babashka/impl/sigint_handler.clj b/src/babashka/impl/sigint_handler.clj
new file mode 100644
index 00000000..9708bc59
--- /dev/null
+++ b/src/babashka/impl/sigint_handler.clj
@@ -0,0 +1,14 @@
+(ns babashka.impl.sigint-handler
+ {:no-doc true}
+ (:import [sun.misc Signal]
+ [sun.misc SignalHandler]))
+
+(set! *warn-on-reflection* true)
+
+(defn handle-sigint! []
+ (Signal/handle
+ (Signal. "INT")
+ (reify SignalHandler
+ (handle [_ _]
+ ;; This is needed to run shutdown hooks on interrupt, System/exit triggers those
+ (System/exit 0)))))
diff --git a/src/babashka/impl/transit.clj b/src/babashka/impl/transit.clj
new file mode 100644
index 00000000..73eb7026
--- /dev/null
+++ b/src/babashka/impl/transit.clj
@@ -0,0 +1,13 @@
+(ns babashka.impl.transit
+ (:require [cognitect.transit :as transit]
+ [sci.impl.namespaces :refer [copy-var]]
+ [sci.impl.vars :as vars]))
+
+
+(def tns (vars/->SciNamespace 'cognitect.transit nil))
+
+(def transit-namespace
+ {'write (copy-var transit/write tns)
+ 'writer (copy-var transit/writer tns)
+ 'read (copy-var transit/read tns)
+ 'reader (copy-var transit/reader tns)})
diff --git a/src/babashka/impl/yaml.clj b/src/babashka/impl/yaml.clj
new file mode 100644
index 00000000..33ad7a4f
--- /dev/null
+++ b/src/babashka/impl/yaml.clj
@@ -0,0 +1,13 @@
+(ns babashka.impl.yaml
+ {:no-doc true}
+ (:require [clj-yaml.core :as yaml]
+ [sci.impl.namespaces :refer [copy-var]]
+ [sci.impl.vars :as vars]))
+
+(def yns (vars/->SciNamespace 'clj-yaml.core nil))
+
+(def yaml-namespace
+ {'mark (copy-var yaml/mark yns)
+ 'unmark (copy-var yaml/unmark yns)
+ 'generate-string (copy-var yaml/generate-string yns)
+ 'parse-string (copy-var yaml/parse-string yns)})
diff --git a/src/babashka/main.clj b/src/babashka/main.clj
index 77834a02..2faf9c00 100644
--- a/src/babashka/main.clj
+++ b/src/babashka/main.clj
@@ -2,31 +2,39 @@
{:no-doc true}
(:require
[babashka.impl.async :refer [async-namespace async-protocols-namespace]]
+ [babashka.impl.bencode :refer [bencode-namespace]]
[babashka.impl.cheshire :refer [cheshire-core-namespace]]
[babashka.impl.classes :as classes]
[babashka.impl.classpath :as cp]
[babashka.impl.clojure.core :refer [core-extras]]
[babashka.impl.clojure.java.io :refer [io-namespace]]
[babashka.impl.clojure.java.shell :refer [shell-namespace]]
- [babashka.impl.clojure.main :refer [demunge]]
- [babashka.impl.clojure.stacktrace :refer [stacktrace-namespace print-stack-trace]]
+ [babashka.impl.clojure.main :as clojure-main :refer [demunge]]
+ [babashka.impl.clojure.pprint :refer [pprint-namespace]]
+ [babashka.impl.clojure.stacktrace :refer [stacktrace-namespace]]
[babashka.impl.common :as common]
[babashka.impl.csv :as csv]
+ [babashka.impl.curl :refer [curl-namespace]]
+ [babashka.impl.nrepl-server :as nrepl-server]
[babashka.impl.pipe-signal-handler :refer [handle-pipe! pipe-signal-received?]]
[babashka.impl.repl :as repl]
+ [babashka.impl.sigint-handler :as sigint-handler]
[babashka.impl.socket-repl :as socket-repl]
[babashka.impl.test :as t]
[babashka.impl.tools.cli :refer [tools-cli-namespace]]
[babashka.impl.xml :as xml]
+ [babashka.impl.transit :refer [transit-namespace]]
+ [babashka.impl.yaml :refer [yaml-namespace]]
[babashka.wait :as wait]
[clojure.edn :as edn]
[clojure.java.io :as io]
+ [clojure.stacktrace :refer [print-stack-trace]]
[clojure.string :as str]
- [fipp.edn :as fipp]
[sci.addons :as addons]
[sci.core :as sci]
[sci.impl.interpreter :refer [eval-string*]]
[sci.impl.opts :as sci-opts]
+ [sci.impl.types :as sci-types]
[sci.impl.unrestrict :refer [*unrestricted*]]
[sci.impl.vars :as vars])
(:gen-class))
@@ -110,7 +118,14 @@
(let [options (next options)]
(recur (next options)
(assoc opts-map
- :socket-repl (first options))))
+ :socket-repl (or (first options)
+ "1666"))))
+ ("--nrepl-server")
+ (let [options (next options)]
+ (recur (next options)
+ (assoc opts-map
+ :nrepl (or (first options)
+ "1667"))))
("--eval", "-e")
(let [options (next options)]
(recur (next options)
@@ -154,7 +169,7 @@
(def usage-string "Usage: bb [ -i | -I ] [ -o | -O ] [ --stream ] [--verbose]
[ ( --classpath | -cp ) ] [ --uberscript ]
[ ( --main | -m ) | -e | -f |
- --repl | --socket-repl [:] ]
+ --repl | --socket-repl [:] | --nrepl-server [:] ]
[ arg* ]")
(defn print-usage []
(println usage-string))
@@ -184,6 +199,7 @@
-m, --main Call the -main function from namespace with args.
--repl Start REPL. Use rlwrap for history.
--socket-repl Start socket REPL. Specify port (e.g. 1666) or host and port separated by colon (e.g. 127.0.0.1:1666).
+ --nrepl-server Start nREPL server. Specify port (e.g. 1667) or host and port separated by colon (e.g. 127.0.0.1:1667).
--time Print execution time before exiting.
-- Stop parsing args and pass everything after -- to *command-line-args*
@@ -198,26 +214,27 @@ Everything after that is bound to *command-line-args*."))
(str/replace x #"^#!.*" ""))
(throw (Exception. (str "File does not exist: " file))))))
-(defn read-edn []
- (edn/read {;;:readers *data-readers*
- :eof ::EOF} *in*))
-
(def reflection-var (sci/new-dynamic-var '*warn-on-reflection* false))
(defn load-file* [sci-ctx f]
(let [f (io/file f)
- s (slurp f)]
+ s (slurp f)
+ prev-ns @vars/current-ns]
(sci/with-bindings {vars/current-file (.getCanonicalPath f)}
- (eval-string* sci-ctx s))))
-
-(defn eval* [sci-ctx form]
- (eval-string* sci-ctx (pr-str form)))
+ (try
+ (eval-string* sci-ctx s)
+ (finally (sci-types/setVal vars/current-ns prev-ns))))))
(defn start-socket-repl! [address ctx]
(socket-repl/start-repl! address ctx)
;; hang until SIGINT
@(promise))
+(defn start-nrepl! [address ctx]
+ (nrepl-server/start-server! ctx address)
+ ;; hang until SIGINT
+ #_@(promise))
+
(defn exit [n]
(throw (ex-info "" {:bb/exit-code n})))
@@ -231,7 +248,11 @@ Everything after that is bound to *command-line-args*."))
async clojure.core.async
csv clojure.data.csv
json cheshire.core
- xml clojure.data.xml})
+ xml clojure.data.xml
+ yaml clj-yaml.core
+ curl babashka.curl
+ transit cognitect.transit
+ bencode bencode.core})
(def cp-state (atom nil))
@@ -257,12 +278,17 @@ Everything after that is bound to *command-line-args*."))
'clojure.data.csv csv/csv-namespace
'cheshire.core cheshire-core-namespace
'clojure.stacktrace stacktrace-namespace
- 'clojure.main {'demunge demunge}
+ 'clojure.main {'demunge demunge
+ 'repl-requires clojure-main/repl-requires}
'clojure.repl {'demunge demunge}
'clojure.test t/clojure-test-namespace
'babashka.classpath {'add-classpath add-classpath*}
- 'clojure.pprint {'pprint fipp/pprint}
- 'clojure.data.xml xml/xml-namespace})
+ 'clojure.data.xml xml/xml-namespace
+ 'clj-yaml.core yaml-namespace
+ 'clojure.pprint pprint-namespace
+ 'babashka.curl curl-namespace
+ 'cognitect.transit transit-namespace
+ 'bencode.core bencode-namespace})
(def bindings
{'java.lang.System/exit exit ;; override exit, so we have more control
@@ -284,6 +310,7 @@ Everything after that is bound to *command-line-args*."))
(defn main
[& args]
(handle-pipe!)
+ (sigint-handler/handle-sigint!)
#_(binding [*out* *err*]
(prn "M" (meta (get bindings 'future))))
(binding [*unrestricted* true]
@@ -293,16 +320,18 @@ Everything after that is bound to *command-line-args*."))
{:keys [:version :shell-in :edn-in :shell-out :edn-out
:help? :file :command-line-args
:expressions :stream? :time?
- :repl :socket-repl
+ :repl :socket-repl :nrepl
:verbose? :classpath
:main :uberscript] :as _opts}
(parse-opts args)
+ _ (when main (System/setProperty "babashka.main" main))
read-next (fn [*in*]
(if (pipe-signal-received?)
::EOF
(if stream?
(if shell-in (or (read-line) ::EOF)
- (read-edn))
+ (edn/read {;;:readers *data-readers*
+ :eof ::EOF} *in*))
(delay (cond shell-in
(shell-seq *in*)
edn-in
@@ -337,6 +366,7 @@ Everything after that is bound to *command-line-args*."))
:imports '{ArithmeticException java.lang.ArithmeticException
AssertionError java.lang.AssertionError
Boolean java.lang.Boolean
+ Byte java.lang.Byte
Class java.lang.Class
Double java.lang.Double
Exception java.lang.Exception
@@ -345,7 +375,10 @@ Everything after that is bound to *command-line-args*."))
File java.io.File
Long java.lang.Long
Math java.lang.Math
+ NumberFormatException java.lang.NumberFormatException
Object java.lang.Object
+ Runtime java.lang.Runtime
+ RuntimeException java.lang.RuntimeException
ProcessBuilder java.lang.ProcessBuilder
String java.lang.String
StringBuilder java.lang.StringBuilder
@@ -357,17 +390,20 @@ Everything after that is bound to *command-line-args*."))
ctx (addons/future ctx)
sci-ctx (sci-opts/init ctx)
_ (vreset! common/ctx sci-ctx)
+ input-var (sci/new-dynamic-var '*input* nil)
_ (swap! (:env sci-ctx)
(fn [env]
- (update-in env [:namespaces 'clojure.core] assoc
- 'eval #(eval* sci-ctx %)
- 'load-file #(load-file* sci-ctx %))))
- _ (swap! (:env sci-ctx)
- (fn [env]
- (assoc-in env [:namespaces 'clojure.main 'repl]
- (fn [& opts]
- (let [opts (apply hash-map opts)]
- (repl/start-repl! sci-ctx opts))))))
+ (update env :namespaces
+ (fn [namespaces] [:namespaces 'clojure.main 'repl]
+ (-> namespaces
+ (assoc-in ['clojure.core 'load-file] #(load-file* sci-ctx %))
+ (assoc-in ['clojure.main 'repl]
+ (fn [& opts]
+ (let [opts (apply hash-map opts)]
+ (repl/start-repl! sci-ctx opts))))
+ (assoc-in ['user (with-meta '*input*
+ (when-not stream?
+ {:sci.impl/deref! true}))] input-var))))))
preloads (some-> (System/getenv "BABASHKA_PRELOADS") (str/trim))
[expressions exit-code]
(cond expressions [expressions nil]
@@ -395,17 +431,16 @@ Everything after that is bound to *command-line-args*."))
[(print-help) 0]
repl [(repl/start-repl! sci-ctx) 0]
socket-repl [(start-socket-repl! socket-repl sci-ctx) 0]
+ nrepl [(start-nrepl! nrepl sci-ctx) 0]
(not (str/blank? expression))
(try
- (loop [in (read-next *in*)]
- (let [_ (swap! env update-in [:namespaces 'user]
- assoc (with-meta '*input*
- (when-not stream?
- {:sci.impl/deref! true}))
- (sci/new-dynamic-var '*input* in))]
+ (loop []
+ (let [in (read-next *in*)]
(if (identical? ::EOF in)
[nil 0] ;; done streaming
- (let [res [(let [res (eval-string* sci-ctx expression)]
+ (let [res [(let [res
+ (sci/binding [input-var in]
+ (eval-string* sci-ctx expression))]
(when (some? res)
(if-let [pr-f (cond shell-out println
edn-out prn)]
@@ -416,7 +451,7 @@ Everything after that is bound to *command-line-args*."))
(pr-f res))
(prn res)))) 0]]
(if stream?
- (recur (read-next *in*))
+ (recur)
res)))))
(catch Throwable e
(error-handler* e verbose?)))
@@ -440,7 +475,8 @@ Everything after that is bound to *command-line-args*."))
(defn -main
[& args]
(if-let [dev-opts (System/getenv "BABASHKA_DEV")]
- (let [{:keys [:n]} (edn/read-string dev-opts)
+ (let [{:keys [:n]} (if (= "true" dev-opts) {:n 1}
+ (edn/read-string dev-opts))
last-iteration (dec n)]
(dotimes [i n]
(if (< i last-iteration)
diff --git a/src/babashka/wait.clj b/src/babashka/wait.clj
index af2cc9d4..f4bf6999 100644
--- a/src/babashka/wait.clj
+++ b/src/babashka/wait.clj
@@ -17,8 +17,8 @@
opts)
t0 (System/currentTimeMillis)]
(loop []
- (let [v (try (with-open [_ (Socket. host port)]
- (- (System/currentTimeMillis) t0))
+ (let [v (try (.close (Socket. host port))
+ (- (System/currentTimeMillis) t0)
(catch ConnectException _e
(let [took (- (System/currentTimeMillis) t0)]
(if (and timeout (>= took timeout))
diff --git a/test-resources/babashka/src_for_classpath_test/my/main2.clj b/test-resources/babashka/src_for_classpath_test/my/main2.clj
new file mode 100644
index 00000000..49a3c7f3
--- /dev/null
+++ b/test-resources/babashka/src_for_classpath_test/my/main2.clj
@@ -0,0 +1,4 @@
+(ns my.main2)
+
+(defn -main [& _args]
+ (System/getProperty "babashka.main"))
diff --git a/test-resources/babashka/statsd.clj b/test-resources/babashka/statsd.clj
new file mode 100644
index 00000000..959fc3bf
--- /dev/null
+++ b/test-resources/babashka/statsd.clj
@@ -0,0 +1,49 @@
+(ns statsd-client
+ "a simple StatsD client written in Clojure
+
+ Usage:
+ statsd-client/increment 'foo
+ statsd-client/decrement 'foo
+ statsd-client/increment 'foo 1
+ statsd-client/decrement 'foo 1
+ statsd-client/gauge 'foo 1
+ statsd-client/timing 'foo 1
+ "
+ (:import (java.net InetAddress DatagramPacket DatagramSocket)))
+
+(def server-address "127.0.0.1")
+(def server-port 8125)
+
+; UDP helper functions
+(defn make-socket
+ ([] (new DatagramSocket))
+ ([port] (new DatagramSocket port)))
+
+(defn send-data [send-socket ip port data]
+ (let [ipaddress (InetAddress/getByName ip),
+ send-packet (new DatagramPacket (.getBytes data) (.length data) ipaddress port)]
+ (.send send-socket send-packet)))
+
+(defn make-send [ip port]
+ (let [send-socket (make-socket)]
+ (fn [data] (send-data send-socket ip port data))))
+
+(def send-msg (make-send server-address server-port))
+
+; statsd client functions
+(defn increment
+ ([metric] (increment metric 1))
+ ([metric value]
+ (send-msg (str metric ":" value "|c"))))
+
+(defn decrement
+ ([metric] (increment metric -1))
+ ([metric value]
+ (send-msg (str metric ":" value "|c"))))
+
+(defn timing [metric value]
+ (send-msg (str metric ":" value "|ms")))
+
+(defn gauge [metric value]
+ (send-msg (str metric ":" value "|g")))
+
diff --git a/test-resources/babashka/transit.clj b/test-resources/babashka/transit.clj
new file mode 100644
index 00000000..3d3bda6a
--- /dev/null
+++ b/test-resources/babashka/transit.clj
@@ -0,0 +1,18 @@
+(require '[cognitect.transit :as transit])
+(import [java.io ByteArrayInputStream ByteArrayOutputStream])
+
+;; Write data to a stream
+(def out (ByteArrayOutputStream. 4096))
+(def writer (transit/writer out :json))
+(transit/write writer "foo")
+(transit/write writer {:a [1 2]})
+
+;; Take a peek at the JSON
+(.toString out)
+;; => "{\"~#'\":\"foo\"} [\"^ \",\"~:a\",[1,2]]"
+
+;; Read data from a stream
+(def in (ByteArrayInputStream. (.toByteArray out)))
+(def reader (transit/reader in :json))
+(prn (transit/read reader)) ;; => "foo"
+(prn (transit/read reader)) ;; => {:a [1 2]}
diff --git a/test-resources/lib_tests/clj_yaml/core_test.clj b/test-resources/lib_tests/clj_yaml/core_test.clj
new file mode 100644
index 00000000..0e75a4d6
--- /dev/null
+++ b/test-resources/lib_tests/clj_yaml/core_test.clj
@@ -0,0 +1,203 @@
+(ns clj-yaml.core-test
+ (:require [clojure.test :refer (deftest testing is)]
+ [clojure.string :as string]
+ [clj-yaml.core :refer [parse-string unmark generate-string]])
+ (:import [java.util Date]))
+
+(def nested-hash-yaml
+ "root:\n childa: a\n childb: \n grandchild: \n greatgrandchild: bar\n")
+
+(def list-yaml
+ "--- # Favorite Movies\n- Casablanca\n- North by Northwest\n- The Man Who Wasn't There")
+
+(def hashes-lists-yaml "
+items:
+ - part_no: A4786
+ descrip: Water Bucket (Filled)
+ price: 1.47
+ quantity: 4
+
+ - part_no: E1628
+ descrip: High Heeled \"Ruby\" Slippers
+ price: 100.27
+ quantity: 1
+ owners:
+ - Dorthy
+ - Wicked Witch of the East
+")
+
+(def inline-list-yaml
+"--- # Shopping list
+[milk, pumpkin pie, eggs, juice]
+")
+
+(def inline-hash-yaml
+ "{name: John Smith, age: 33}")
+
+(def list-of-hashes-yaml "
+- {name: John Smith, age: 33}
+- name: Mary Smith
+ age: 27
+")
+
+(def hashes-of-lists-yaml "
+men: [John Smith, Bill Jones]
+women:
+ - Mary Smith
+ - Susan Williams
+")
+
+(def typed-data-yaml "
+the-bin: !!binary 0101")
+
+(def io-file-typed-data-yaml "
+!!java.io.File")
+
+(def set-yaml "
+--- !!set
+? Mark McGwire
+? Sammy Sosa
+? Ken Griff")
+
+(deftest parse-hash
+ (let [parsed (parse-string "foo: bar")]
+ (is (= "bar" (parsed :foo)))))
+
+(deftest parse-hash-with-numeric-key
+ (let [parsed (parse-string "123: 456")]
+ (is (= 456 (parsed 123)))))
+
+(deftest parse-hash-with-complex-key
+ (let [parsed (parse-string "[1, 2]: 3")]
+ (is (= 3 (parsed [1, 2])))))
+
+(deftest parse-nested-hash
+ (let [parsed (parse-string nested-hash-yaml)]
+ (is (= "a" ((parsed :root) :childa)))
+ (is (= "bar" ((((parsed :root) :childb) :grandchild) :greatgrandchild)))))
+
+(deftest parse-list
+ (let [parsed (parse-string list-yaml)]
+ (is (= "Casablanca" (first parsed)))
+ (is (= "North by Northwest" (nth parsed 1)))
+ (is (= "The Man Who Wasn't There" (nth parsed 2)))))
+
+(deftest parse-nested-hash-and-list
+ (let [parsed (parse-string hashes-lists-yaml)]
+ (is (= "A4786" ((first (parsed :items)) :part_no)))
+ (is (= "Dorthy" (first ((nth (parsed :items) 1) :owners))))))
+
+(deftest parse-inline-list
+ (let [parsed (parse-string inline-list-yaml)]
+ (is (= "milk" (first parsed)))
+ (is (= "pumpkin pie" (nth parsed 1)))
+ (is (= "eggs" (nth parsed 2)))
+ (is (= "juice" (last parsed)))))
+
+(deftest parse-inline-hash
+ (let [parsed (parse-string inline-hash-yaml)]
+ (is (= "John Smith" (parsed :name)))
+ (is (= 33 (parsed :age)))))
+
+(deftest parse-list-of-hashes
+ (let [parsed (parse-string list-of-hashes-yaml)]
+ (is (= "John Smith" ((first parsed) :name)))
+ (is (= 33 ((first parsed) :age)))
+ (is (= "Mary Smith" ((nth parsed 1) :name)))
+ (is (= 27 ((nth parsed 1) :age)))))
+
+(deftest hashes-of-lists
+ (let [parsed (parse-string hashes-of-lists-yaml)]
+ (is (= "John Smith" (first (parsed :men))))
+ (is (= "Bill Jones" (last (parsed :men))))
+ (is (= "Mary Smith" (first (parsed :women))))
+ (is (= "Susan Williams" (last (parsed :women))))))
+
+(deftest h-set
+ (is (= #{"Mark McGwire" "Ken Griff" "Sammy Sosa"}
+ (parse-string set-yaml))))
+
+(deftest typed-data
+ (let [parsed (parse-string typed-data-yaml)]
+ (is (= (Class/forName "[B") (type (:the-bin parsed))))))
+
+(deftest disallow-arbitrary-typed-data
+ (is (thrown? org.yaml.snakeyaml.error.YAMLException
+ (parse-string io-file-typed-data-yaml))))
+
+(deftest keywordized
+ (is (= "items"
+ (-> hashes-lists-yaml
+ (parse-string :keywords false)
+ ffirst))))
+
+(deftest not-keywordized-in-lists
+ (is (every? string?
+ (-> "[{b: c, c: d}]"
+ (parse-string :keywords false)
+ first
+ keys))))
+
+(deftest marking-source-position-works
+ (let [parsed (parse-string inline-list-yaml :mark true)]
+ ;; The list starts at the beginning of line 1.
+ (is (= 1 (-> parsed :start :line)))
+ (is (= 0 (-> parsed :start :column)))
+ ;; The first item starts at the second character of line 1.
+ (is (= 1 (-> parsed unmark first :start :line)))
+ (is (= 1 (-> parsed unmark first :start :column)))
+ ;; The first item ends at the fifth character of line 1.
+ (is (= 1 (-> parsed unmark first :end :line)))
+ (is (= 5 (-> parsed unmark first :end :column)))))
+
+(deftest text-wrapping
+ (let [data
+ {:description
+ "Big-picture diagram showing how our top-level systems and stakeholders interact"}]
+ (testing "long lines of text should not be wrapped"
+ ;; clj-yaml 0.5.6 used SnakeYAML 1.13 which by default did *not* split long lines.
+ ;; clj-yaml 0.6.0 upgraded to SnakeYAML 1.23 which by default *did* split long lines.
+ ;; This test ensures that generate-string uses the older behavior by default, for the sake
+ ;; of stability, i.e. backwards compatibility.
+ (is
+ (= "{description: Big-picture diagram showing how our top-level systems and stakeholders interact}\n"
+ (generate-string data))))))
+
+(deftest dump-opts
+ (let [data [{:age 33 :name "jon"} {:age 44 :name "boo"}]]
+ (is (= "- age: 33\n name: jon\n- age: 44\n name: boo\n"
+ (generate-string data :dumper-options {:flow-style :block})))
+ (is (= "[{age: 33, name: jon}, {age: 44, name: boo}]\n"
+ (generate-string data :dumper-options {:flow-style :flow})))))
+
+;; TODO: this test is failing in GraalVM
+;; Could be related to https://github.com/oracle/graal/issues/2234
+#_(deftest parse-time
+ (testing "clj-time parses timestamps with more than millisecond precision correctly."
+ (let [timestamp "2001-11-23 15:02:31.123456 -04:00"
+ expected 1006542151123]
+ (is (= (.getTime ^Date (parse-string timestamp)) expected)))))
+
+(deftest maps-are-ordered
+ (let [parsed (parse-string hashes-lists-yaml)
+ [first second] (:items parsed)]
+ (is (= (keys first) '(:part_no :descrip :price :quantity)))
+ (is (= (keys second)'(:part_no :descrip :price :quantity :owners)))))
+
+
+(deftest nulls-are-fine
+ (testing "nil does not blow up"
+ (let [res (parse-string "- f:")]
+ (is (= [{:f nil}] res))
+ (is (str res)))))
+
+(deftest emoji-can-be-parsed
+ (let [yaml "{emoji: 💣}"]
+ (is (= yaml (-> yaml
+ (generate-string)
+ (parse-string)
+ (string/trim)))))
+
+ (testing "emoji in comments are OK too"
+ (let [yaml "# 💣 emoji in a comment\n42"]
+ (is (= 42 (parse-string yaml))))))
diff --git a/test-resources/lib_tests/clojure/data/csv_test.clj b/test-resources/lib_tests/clojure/data/csv_test.clj
new file mode 100644
index 00000000..8dfa7dfb
--- /dev/null
+++ b/test-resources/lib_tests/clojure/data/csv_test.clj
@@ -0,0 +1,79 @@
+(ns clojure.data.csv-test
+ (:use
+ [clojure.test :only (deftest is)]
+ [clojure.data.csv :only (read-csv write-csv)])
+ (:import
+ [java.io Reader StringReader StringWriter EOFException]))
+
+(def ^{:private true} simple
+ "Year,Make,Model
+1997,Ford,E350
+2000,Mercury,Cougar
+")
+
+(def ^{:private true} simple-alt-sep
+ "Year;Make;Model
+1997;Ford;E350
+2000;Mercury;Cougar
+")
+
+(def ^{:private true} complicated
+ "1997,Ford,E350,\"ac, abs, moon\",3000.00
+1999,Chevy,\"Venture \"\"Extended Edition\"\"\",\"\",4900.00
+1999,Chevy,\"Venture \"\"Extended Edition, Very Large\"\"\",\"\",5000.00
+1996,Jeep,Grand Cherokee,\"MUST SELL!
+air, moon roof, loaded\",4799.00")
+
+(deftest reading
+ (let [csv (read-csv simple)]
+ (is (= (count csv) 3))
+ (is (= (count (first csv)) 3))
+ (is (= (first csv) ["Year" "Make" "Model"]))
+ (is (= (last csv) ["2000" "Mercury" "Cougar"])))
+ (let [csv (read-csv simple-alt-sep :separator \;)]
+ (is (= (count csv) 3))
+ (is (= (count (first csv)) 3))
+ (is (= (first csv) ["Year" "Make" "Model"]))
+ (is (= (last csv) ["2000" "Mercury" "Cougar"])))
+ (let [csv (read-csv complicated)]
+ (is (= (count csv) 4))
+ (is (= (count (first csv)) 5))
+ (is (= (first csv)
+ ["1997" "Ford" "E350" "ac, abs, moon" "3000.00"]))
+ (is (= (last csv)
+ ["1996" "Jeep" "Grand Cherokee", "MUST SELL!\nair, moon roof, loaded" "4799.00"]))))
+
+
+(deftest reading-and-writing
+ (let [string-writer (StringWriter.)]
+ (->> simple read-csv (write-csv string-writer))
+ (is (= simple
+ (str string-writer)))))
+
+(deftest throw-if-quoted-on-eof
+ (let [s "ab,\"de,gh\nij,kl,mn"]
+ (try
+ (doall (read-csv s))
+ (is false "No exception thrown")
+ (catch Exception e
+ (is (or (instance? java.io.EOFException e)
+ (and (instance? RuntimeException e)
+ (instance? java.io.EOFException (.getCause e)))))))))
+
+(deftest parse-line-endings
+ (let [csv (read-csv "Year,Make,Model\n1997,Ford,E350")]
+ (is (= 2 (count csv)))
+ (is (= ["Year" "Make" "Model"] (first csv)))
+ (is (= ["1997" "Ford" "E350"] (second csv))))
+ (let [csv (read-csv "Year,Make,Model\r\n1997,Ford,E350")]
+ (is (= 2 (count csv)))
+ (is (= ["Year" "Make" "Model"] (first csv)))
+ (is (= ["1997" "Ford" "E350"] (second csv))))
+ (let [csv (read-csv "Year,Make,Model\r1997,Ford,E350")]
+ (is (= 2 (count csv)))
+ (is (= ["Year" "Make" "Model"] (first csv)))
+ (is (= ["1997" "Ford" "E350"] (second csv))))
+ (let [csv (read-csv "Year,Make,\"Model\"\r1997,Ford,E350")]
+ (is (= 2 (count csv)))
+ (is (= ["Year" "Make" "Model"] (first csv)))
+ (is (= ["1997" "Ford" "E350"] (second csv)))))
diff --git a/test/babashka/async_test.clj b/test/babashka/async_test.clj
new file mode 100644
index 00000000..783e7788
--- /dev/null
+++ b/test/babashka/async_test.clj
@@ -0,0 +1,35 @@
+(ns babashka.async-test
+ (:require
+ [babashka.test-utils :as test-utils]
+ [clojure.edn :as edn]
+ [clojure.test :as t :refer [deftest is]]))
+
+(deftest alts!!-test
+ (is (= "process 2\n" (test-utils/bb nil "
+ (defn async-command [& args]
+ (async/thread (apply shell/sh \"bash\" \"-c\" args)))
+
+ (-> (async/alts!! [(async-command \"sleep 2 && echo process 1\")
+ (async-command \"sleep 1 && echo process 2\")])
+ first :out str/trim println)"))))
+
+(deftest go-test
+ (is (number? (edn/read-string (test-utils/bb nil "
+(defn calculation-go []
+ (async/go
+ ;; wait for some stuff
+ (rand-int 1000)))
+
+(defn get-result-go []
+ (async/go
+ (->>
+ (repeatedly 10 calculation-go)
+ (map async/ response :args :foo)))
-")))))
+")))
+ (catch Exception e
+ (str/includes? (str e) "timed out")))))
diff --git a/test/babashka/impl/nrepl_server_test.clj b/test/babashka/impl/nrepl_server_test.clj
new file mode 100644
index 00000000..52613754
--- /dev/null
+++ b/test/babashka/impl/nrepl_server_test.clj
@@ -0,0 +1,203 @@
+(ns babashka.impl.nrepl-server-test
+ (:require
+ [babashka.impl.bencode.core :as bencode]
+ [babashka.impl.nrepl-server :refer [start-server! stop-server!]]
+ [babashka.main :as main]
+ [babashka.test-utils :as tu]
+ [babashka.wait :as wait]
+ [clojure.test :as t :refer [deftest is testing]]
+ [sci.impl.opts :refer [init]])
+ (:import [java.lang ProcessBuilder$Redirect]))
+
+(def debug? false)
+
+(set! *warn-on-reflection* true)
+
+(defn bytes->str [x]
+ (if (bytes? x) (String. (bytes x))
+ (str x)))
+
+(defn read-msg [msg]
+ (let [res (zipmap (map keyword (keys msg))
+ (map #(if (bytes? %)
+ (String. (bytes %))
+ %)
+ (vals msg)))
+ res (if-let [status (:status res)]
+ (assoc res :status (mapv bytes->str status))
+ res)
+ res (if-let [status (:sessions res)]
+ (assoc res :sessions (mapv bytes->str status))
+ res)]
+ res))
+
+(defn read-reply [in session id]
+ (loop []
+ (let [msg (read-msg (bencode/read-bencode in))]
+ (if (and (= (:session msg) session)
+ (= (:id msg) id))
+ (do
+ (when debug? (prn "received" msg))
+ msg)
+ (do
+ (when debug? (prn "skipping over msg" msg))
+ (recur))))))
+
+(defn nrepl-test []
+ (with-open [socket (java.net.Socket. "127.0.0.1" 1667)
+ in (.getInputStream socket)
+ in (java.io.PushbackInputStream. in)
+ os (.getOutputStream socket)]
+ (bencode/write-bencode os {"op" "clone"})
+ (let [session (:new-session (read-msg (bencode/read-bencode in)))
+ id (atom 0)
+ new-id! #(swap! id inc)]
+ (testing "session"
+ (is session))
+ (testing "eval"
+ (bencode/write-bencode os {"op" "eval" "code" "(+ 1 2 3)" "session" session "id" (new-id!)})
+ (let [msg (read-reply in session @id)
+ id (:id msg)
+ value (:value msg)]
+ (is (= 1 id))
+ (is (= value "6")))
+ (testing "creating a namespace and evaluating something in it"
+ (bencode/write-bencode os {"op" "eval"
+ "code" "(ns ns0) (defn foo [] :foo0) (ns ns1) (defn foo [] :foo1)"
+ "session" session
+ "id" (new-id!)})
+ (read-reply in session @id)
+ (testing "not providing the ns key evaluates in the last defined namespace"
+ (bencode/write-bencode os {"op" "eval" "code" "(foo)" "session" session "id" (new-id!)})
+ (is (= ":foo1" (:value (read-reply in session @id)))))
+ (testing "explicitly providing the ns key evaluates in that namespace"
+ (bencode/write-bencode os {"op" "eval"
+ "code" "(foo)"
+ "session" session
+ "id" (new-id!)
+ "ns" "ns0"})
+ (is (= ":foo0" (:value (read-reply in session @id)))))
+ (testing "providing an ns value of a non-existing namespace creates the namespace"
+ (bencode/write-bencode os {"op" "eval"
+ "code" "(ns-name *ns*)"
+ "session" session
+ "id" (new-id!)
+ "ns" "unicorn"})
+ (let [reply (read-reply in session @id)]
+ (is (= "unicorn" (:value reply))))))
+ (testing "multiple top level expressions results in two value replies"
+ (bencode/write-bencode os {"op" "eval"
+ "code" "(+ 1 2 3) (+ 1 2 3)"
+ "session" session
+ "id" (new-id!)})
+ (let [reply-1 (read-reply in session @id)
+ reply-2 (read-reply in session @id)]
+ (is (= "6" (:value reply-1) (:value reply-2))))))
+ (testing "load-file"
+ (bencode/write-bencode os {"op" "load-file" "file" "(ns foo) (defn foo [] :foo)" "session" session "id" (new-id!)})
+ (read-reply in session @id)
+ (bencode/write-bencode os {"op" "eval" "code" "(foo)" "ns" "foo" "session" session "id" (new-id!)})
+ (is (= ":foo" (:value (read-reply in session @id)))))
+ (testing "complete"
+ (testing "completions for fo"
+ (bencode/write-bencode os {"op" "complete"
+ "symbol" "fo"
+ "session" session
+ "id" (new-id!)
+ "ns" "foo"})
+ (let [reply (read-reply in session @id)
+ completions (:completions reply)
+ completions (mapv read-msg completions)
+ completions (into #{} (map (juxt :ns :candidate)) completions)]
+ (is (contains? completions ["foo" "foo"]))
+ (is (contains? completions ["clojure.core" "format"]))))
+ (testing "completions for quux should be empty"
+ (bencode/write-bencode os {"op" "complete"
+ "symbol" "quux"
+ "session" session "id" (new-id!)
+ "ns" "foo"})
+ (let [reply (read-reply in session @id)
+ completions (:completions reply)]
+ (is (empty? completions)))
+ (testing "unless quux is an alias"
+ (bencode/write-bencode os {"op" "eval" "code" "(require '[cheshire.core :as quux])" "session" session "id" (new-id!)})
+ (read-reply in session @id)
+ (bencode/write-bencode os {"op" "complete" "symbol" "quux" "session" session "id" (new-id!)})
+ (let [reply (read-reply in session @id)
+ completions (:completions reply)
+ completions (mapv read-msg completions)
+ completions (into #{} (map (juxt :ns :candidate)) completions)]
+ (is (contains? completions ["cheshire.core" "quux/generate-string"])))))
+ (testing "completions for clojure.test"
+ (bencode/write-bencode os {"op" "eval" "code" "(require '[clojure.test :as test])" "session" session "id" (new-id!)})
+ (read-reply in session @id)
+ (bencode/write-bencode os {"op" "complete" "symbol" "test" "session" session "id" (new-id!)})
+ (let [reply (read-reply in session @id)
+ completions (:completions reply)
+ completions (mapv read-msg completions)
+ completions (into #{} (map (juxt :ns :candidate)) completions)]
+ (is (contains? completions ["clojure.test" "test/deftest"])))))
+ (testing "close + ls-sessions"
+ (bencode/write-bencode os {"op" "ls-sessions" "session" session "id" (new-id!)})
+ (let [reply (read-reply in session @id)
+ sessions (set (:sessions reply))]
+ (is (contains? sessions session))
+ (let [new-sessions (loop [i 0
+ sessions #{}]
+ (bencode/write-bencode os {"op" "clone" "session" session "id" (new-id!)})
+ (let [new-session (:new-session (read-reply in session @id))
+ sessions (conj sessions new-session)]
+ (if (= i 4)
+ sessions
+ (recur (inc i) sessions))))]
+ (bencode/write-bencode os {"op" "ls-sessions" "session" session "id" (new-id!)})
+ (let [reply (read-reply in session @id)
+ sessions (set (:sessions reply))]
+ (is (= 6 (count sessions)))
+ (is (contains? sessions session))
+ (is (= new-sessions (disj sessions session)))
+ (testing "close"
+ (doseq [close-session (disj sessions session)]
+ (bencode/write-bencode os {"op" "close" "session" close-session "id" (new-id!)})
+ (let [reply (read-reply in close-session @id)]
+ (is (contains? (set (:status reply)) "session-closed")))))
+ (testing "session not listen in ls-sessions after close"
+ (bencode/write-bencode os {"op" "ls-sessions" "session" session "id" (new-id!)})
+ (let [reply (read-reply in session @id)
+ sessions (set (:sessions reply))]
+ (is (contains? sessions session))
+ (is (not (some #(contains? sessions %) new-sessions)))))))))
+ (testing "output"
+ (bencode/write-bencode os {"op" "eval" "code" "(dotimes [i 3] (println \"Hello\"))"
+ "session" session "id" (new-id!)})
+ (dotimes [_ 3]
+ (let [reply (read-reply in session @id)]
+ (is (= "Hello\n" (:out reply)))))))))
+
+(deftest nrepl-server-test
+ (let [proc-state (atom nil)]
+ (try
+ (if tu/jvm?
+ (future
+ (start-server!
+ (init {:namespaces main/namespaces
+ :features #{:bb}}) "0.0.0.0:1667"))
+ (let [pb (ProcessBuilder. ["./bb" "--nrepl-server" "0.0.0.0:1667"])
+ _ (.redirectError pb ProcessBuilder$Redirect/INHERIT)
+ ;; _ (.redirectOutput pb ProcessBuilder$Redirect/INHERIT)
+ ;; env (.environment pb)
+ ;; _ (.put env "BABASHKA_DEV" "true")
+ proc (.start pb)]
+ (reset! proc-state proc)))
+ (babashka.wait/wait-for-port "localhost" 1667)
+ (nrepl-test)
+ (finally
+ (if tu/jvm?
+ (stop-server!)
+ (when-let [proc @proc-state]
+ (.destroy ^Process proc)))))))
+
+;;;; Scratch
+
+(comment
+ )
diff --git a/test/babashka/impl/repl_test.clj b/test/babashka/impl/repl_test.clj
index 29a932c8..b914207b 100644
--- a/test/babashka/impl/repl_test.clj
+++ b/test/babashka/impl/repl_test.clj
@@ -3,8 +3,8 @@
[babashka.impl.repl :refer [start-repl!]]
[clojure.string :as str]
[clojure.test :as t :refer [deftest is]]
- [sci.impl.opts :refer [init]]
[sci.core :as sci]
+ [sci.impl.opts :refer [init]]
[sci.impl.vars :as vars]))
(set! *warn-on-reflection* true)
@@ -24,6 +24,15 @@
(sci/with-in-str (str expr "\n:repl/quit")
(repl!))) expected)))
+(defn assert-repl-error [expr expected]
+ (is (str/includes?
+ (let [sw (java.io.StringWriter.)]
+ (sci/binding [sci/out (java.io.StringWriter.)
+ sci/err sw]
+ (sci/with-in-str (str expr "\n:repl/quit")
+ (repl!)))
+ (str sw)) expected)))
+
(deftest repl-test
(assert-repl "1" "1")
(assert-repl "[1 2 3]" "[1 2 3]")
@@ -34,7 +43,8 @@
(assert-repl "1\n(inc *1)" "2")
(assert-repl "1\n(dec *1)(+ *2 *2)" "2")
(assert-repl "1\n(dec *1)(+ *2 *2)" "2")
- (assert-repl "*command-line-args*" "[\"a\" \"b\" \"c\"]"))
+ (assert-repl "*command-line-args*" "[\"a\" \"b\" \"c\"]")
+ (assert-repl-error "(+ 1 nil)" "NullPointerException"))
;;;; Scratch
diff --git a/test/babashka/impl/socket_repl_test.clj b/test/babashka/impl/socket_repl_test.clj
index 8bad7823..18abb4e4 100644
--- a/test/babashka/impl/socket_repl_test.clj
+++ b/test/babashka/impl/socket_repl_test.clj
@@ -2,10 +2,10 @@
(:require
[babashka.impl.socket-repl :refer [start-repl! stop-repl!]]
[babashka.test-utils :as tu]
+ [clojure.java.io :as io]
[clojure.java.shell :refer [sh]]
[clojure.string :as str]
[clojure.test :as t :refer [deftest is testing]]
- [clojure.java.io :as io]
[sci.impl.opts :refer [init]]))
(set! *warn-on-reflection* true)
diff --git a/test/babashka/java_time_test.clj b/test/babashka/java_time_test.clj
index 65b22709..c66fb8e4 100644
--- a/test/babashka/java_time_test.clj
+++ b/test/babashka/java_time_test.clj
@@ -16,4 +16,8 @@
(is (= "18-12-2019 16:01:41"
(bb '(.format
(java.time.LocalDateTime/parse "2019-12-18T16:01:41.485")
- (java.time.format.DateTimeFormatter/ofPattern "dd-MM-yyyy HH:mm:ss"))))))
+ (java.time.format.DateTimeFormatter/ofPattern "dd-MM-yyyy HH:mm:ss")))))
+ (is (number? (bb "
+(let [x (java.time.LocalDateTime/parse \"2019-12-18T16:01:41.485\")
+ y (java.time.LocalDateTime/now)]
+ (.between java.time.temporal.ChronoUnit/MINUTES x y))"))))
diff --git a/test/babashka/main_test.clj b/test/babashka/main_test.clj
index 6d5c3587..e9b43336 100644
--- a/test/babashka/main_test.clj
+++ b/test/babashka/main_test.clj
@@ -9,6 +9,10 @@
[clojure.test :as test :refer [deftest is testing]]
[sci.core :as sci]))
+(defmethod clojure.test/report :begin-test-var [m]
+ (println "===" (-> m :var meta :name))
+ (println))
+
(defn bb [input & args]
(edn/read-string (apply test-utils/bb (when (some? input) (str input)) (map str args))))
@@ -29,7 +33,7 @@
(deftest main-test
(testing "-io behaves as identity"
- (= "foo\nbar\n" (test-utils/bb "foo\nbar\n" "-io" "*input*")))
+ (is (= "foo\nbar\n" (test-utils/bb "foo\nbar\n" "-io" "*input*"))))
(testing "if and when"
(is (= 1 (bb 0 '(if (zero? *input*) 1 2))))
(is (= 2 (bb 1 '(if (zero? *input*) 1 2))))
@@ -118,9 +122,39 @@
(deftest load-file-test
(let [tmp (java.io.File/createTempFile "script" ".clj")]
- (spit tmp "(defn foo [x y] (+ x y)) (defn bar [x y] (* x y))")
- (is (= "120\n" (test-utils/bb nil (format "(load-file \"%s\") (bar (foo 10 30) 3)"
- (.getPath tmp)))))))
+ (.deleteOnExit tmp)
+ (spit tmp "(ns foo) (defn foo [x y] (+ x y)) (defn bar [x y] (* x y))")
+ (is (= "120\n" (test-utils/bb nil (format "(load-file \"%s\") (foo/bar (foo/foo 10 30) 3)"
+ (.getPath tmp)))))
+ (testing "namespace is restored after load file"
+ (is (= 'start-ns
+ (bb nil (format "(ns start-ns) (load-file \"%s\") (ns-name *ns*)"
+ (.getPath tmp))))))))
+
+(deftest repl-source-test
+ (let [tmp (java.io.File/createTempFile "lib" ".clj")
+ name (str/replace (.getName tmp) ".clj" "")
+ dir (.getParent tmp)]
+ (.deleteOnExit tmp)
+ (testing "print source from loaded file"
+ (spit tmp (format "
+(ns %s)
+
+(defn foo [x y]
+ (+ x y))" name))
+ (is (= "(defn foo [x y]\n (+ x y))\n"
+ (bb nil (format "
+(load-file \"%s\")
+(require '[clojure.repl :refer [source]])
+(with-out-str (source %s/foo))"
+ (.getPath tmp)
+ name)))))
+ (testing "print source from file on classpath"
+ (is (= "(defn foo [x y]\n (+ x y))\n"
+ (bb nil
+ "-cp" dir
+ "-e" (format "(require '[clojure.repl :refer [source]] '[%s])" name)
+ "-e" (format "(with-out-str (source %s/foo))" name)))))))
(deftest eval-test
(is (= "120\n" (test-utils/bb nil "(eval '(do (defn foo [x y] (+ x y))
@@ -160,7 +194,7 @@
(deftest future-test
(is (= 6 (bb nil "@(future (+ 1 2 3))"))))
-
+
(deftest promise-test
(is (= :timeout (bb nil "(deref (promise) 1 :timeout)")))
(is (= :ok (bb nil "(let [x (promise)]
@@ -220,23 +254,26 @@
{:default :timed-out :timeout 100}))"
temp-dir-path))))))
-(deftest async-test
- (is (= "process 2\n" (test-utils/bb nil "
- (defn async-command [& args]
- (async/thread (apply shell/sh \"bash\" \"-c\" args)))
-
- (-> (async/alts!! [(async-command \"sleep 2 && echo process 1\")
- (async-command \"sleep 1 && echo process 2\")])
- first :out str/trim println)"))))
-
(deftest tools-cli-test
(is (= {:result 8080} (bb nil "test/babashka/scripts/tools.cli.bb"))))
(deftest try-catch-test
- (is (zero? (bb nil "(try (/ 1 0) (catch ArithmeticException _ 0))"))))
+ (is (zero? (bb nil "(try (/ 1 0) (catch ArithmeticException _ 0))")))
+ (is (= :got-it (bb nil "
+(defn foo []
+ (throw (java.util.MissingResourceException. \"o noe!\" \"\" \"\")))
+
+(defn bar
+ []
+ (try (foo)
+ (catch java.util.MissingResourceException _
+ :got-it)))
+(bar)
+"))))
(deftest reader-conditionals-test
(is (= :hello (bb nil "#?(:bb :hello :default :bye)")))
+ (is (= :hello (bb nil "#? (:bb :hello :default :bye)")))
(is (= :hello (bb nil "#?(:clj :hello :bb :bye)")))
(is (= [1 2] (bb nil "[1 2 #?@(:bb [] :clj [1])]"))))
@@ -355,8 +392,52 @@
(alter-var-root #'clojure.core/inc (constantly inc2))
res)")))))
+(deftest pprint-test
+ (testing "writer"
+ (is (string? (bb nil "(let [sw (java.io.StringWriter.)] (clojure.pprint/pprint (range 10) sw) (str sw))")))))
+
+(deftest read-string-test
+ (testing "namespaced keyword via alias"
+ (is (= :clojure.string/foo
+ (bb nil "(ns foo (:require [clojure.string :as str])) (read-string \"::str/foo\")")))))
+
+(deftest available-stream-test
+ (is (= 0 (bb nil "(.available System/in)"))))
+
+(deftest file-reader-test
+ (when (str/includes? (str/lower-case (System/getProperty "os.name")) "linux")
+ (let [v (bb nil "(slurp (io/reader (java.io.FileReader. \"/proc/loadavg\")))")]
+ (prn "output:" v)
+ (is v))))
+
+(deftest download-and-extract-test
+ (is (try (= 6 (bb nil (io/file "test" "babashka" "scripts" "download_and_extract_zip.bb")))
+ (catch Exception e
+ (is (str/includes? (str e) "timed out"))))))
+
+(deftest get-message-on-exception-info-test
+ (is "foo" (bb nil "(try (throw (ex-info \"foo\" {})) (catch Exception e (.getMessage e)))")))
+
+(deftest pushback-reader-test
+ (is (= "foo" (bb nil "
+(require '[clojure.java.io :as io])
+(let [pb (java.io.PushbackInputStream. (java.io.ByteArrayInputStream. (.getBytes \"foo\")))]
+ (.unread pb (.read pb))
+ (slurp pb))"))))
+
+(deftest delete-on-exit-test
+ (when test-utils/native?
+ (let [f (java.io.File/createTempFile "foo" "bar")
+ p (.getPath f)]
+ (bb nil (format "(.deleteOnExit (io/file \"%s\"))" p))
+ (is (false? (.exists f))))))
+
+(deftest yaml-test
+ (is (str/starts-with?
+ (bb nil "(yaml/generate-string [{:name \"John Smith\", :age 33} {:name \"Mary Smith\", :age 27}])")
+ "-")))
+
;;;; Scratch
(comment
- (dotimes [_ 10] (wait-for-port-test))
- )
+ (dotimes [_ 10] (wait-for-port-test)))
diff --git a/test/babashka/scripts/download_and_extract_zip.bb b/test/babashka/scripts/download_and_extract_zip.bb
new file mode 100644
index 00000000..befe9258
--- /dev/null
+++ b/test/babashka/scripts/download_and_extract_zip.bb
@@ -0,0 +1,30 @@
+(require '[clojure.java.io :as io] '[clojure.java.shell :refer [sh]] '[clojure.string :as str])
+(import '[java.net URL HttpURLConnection])
+
+(set! *warn-on-reflection* true)
+
+(let [os-name (System/getProperty "os.name")
+ os-name (str/lower-case os-name)
+ os (cond (str/includes? os-name "linux") "linux"
+ (str/includes? os-name "mac") "macos"
+ (str/includes? os-name "win") "windows")
+ tmp-dir (System/getProperty "java.io.tmpdir")
+ zip-file (io/file tmp-dir "bb-0.0.78.zip")
+ source (URL. (format "https://github.com/borkdude/babashka/releases/download/v0.0.78/babashka-0.0.78-%s-amd64.zip" os))
+ conn ^HttpURLConnection (.openConnection ^URL source)
+ _ (.setConnectTimeout conn 2000)
+ _ (.setReadTimeout conn 2000)]
+ (.connect conn)
+ (with-open [is (.getInputStream conn)]
+ (io/copy is zip-file))
+ (let [bb-file (io/file tmp-dir "bb-extracted")
+ fs (java.nio.file.FileSystems/newFileSystem (.toPath zip-file) nil)
+ to-extract (.getPath fs "bb" (into-array String []))]
+ (java.nio.file.Files/copy to-extract (.toPath bb-file)
+ ^"[Ljava.nio.file.CopyOption;"
+ (into-array java.nio.file.CopyOption []))
+ (.setExecutable bb-file true)
+ (let [out (:out (sh (.getPath bb-file) "(+ 1 2 3)"))]
+ (.delete bb-file)
+ (.delete zip-file)
+ (println out))))
diff --git a/test/babashka/scripts/interrupt_handler.bb b/test/babashka/scripts/interrupt_handler.bb
new file mode 100644
index 00000000..02291139
--- /dev/null
+++ b/test/babashka/scripts/interrupt_handler.bb
@@ -0,0 +1,6 @@
+(require '[babashka.signal :as signal])
+
+(signal/add-interrupt-handler! :quit (fn [k] (println "bye1" k)))
+(signal/add-interrupt-handler! :quit2 (fn [k] (println "bye2" k)))
+
+(System/exit 0)
diff --git a/test/babashka/shutdown_hook_test.clj b/test/babashka/shutdown_hook_test.clj
new file mode 100644
index 00000000..71995c97
--- /dev/null
+++ b/test/babashka/shutdown_hook_test.clj
@@ -0,0 +1,24 @@
+(ns babashka.shutdown-hook-test
+ {:no-doc true}
+ (:import [java.nio.charset Charset])
+ (:require [babashka.test-utils :as tu]
+ [clojure.java.io :as io]
+ [clojure.test :refer [deftest is]]))
+
+(defn- stream-to-string
+ ([in] (stream-to-string in (.name (Charset/defaultCharset))))
+ ([in enc]
+ (with-open [bout (java.io.StringWriter.)]
+ (io/copy in bout :encoding enc)
+ (.toString bout))))
+
+(deftest shutdown-hook-test
+ (let [script "(-> (Runtime/getRuntime) (.addShutdownHook (Thread. #(println \"bye\"))))"
+ pb (ProcessBuilder. (if tu/jvm?
+ ["lein" "bb" "-e" script]
+ ["./bb" "-e" script]))
+ process (.start pb)
+ output (.getInputStream process)]
+ (when-let [s (not-empty (stream-to-string (.getErrorStream process)))]
+ (prn "ERROR:" s))
+ (is (= "bye\n" (stream-to-string output)))))
diff --git a/test/babashka/transit_test.clj b/test/babashka/transit_test.clj
new file mode 100644
index 00000000..86523771
--- /dev/null
+++ b/test/babashka/transit_test.clj
@@ -0,0 +1,13 @@
+(ns babashka.transit-test
+ (:require
+ [babashka.test-utils :as test-utils]
+ [clojure.java.io :as io]
+ [clojure.test :as t :refer [deftest is]]))
+
+(defn bb [& args]
+ (apply test-utils/bb nil (map str args)))
+
+(deftest transit-test
+ (is (= "\"foo\"\n{:a [1 2]}\n"
+ (bb (format "(load-file \"%s\")"
+ (.getPath (io/file "test-resources" "babashka" "transit.clj")))))))
diff --git a/test/babashka/udp_test.clj b/test/babashka/udp_test.clj
new file mode 100644
index 00000000..5c0f3730
--- /dev/null
+++ b/test/babashka/udp_test.clj
@@ -0,0 +1,25 @@
+(ns babashka.udp-test
+ (:require [babashka.test-utils :as tu]
+ [clojure.test :refer [deftest is]])
+ (:import [java.io StringWriter]
+ [java.net DatagramPacket DatagramSocket]))
+
+(set! *warn-on-reflection* true)
+
+(deftest udp-test
+ (let [server (DatagramSocket. 8125)
+ sw (StringWriter.)
+ fut (future
+ (let [buf (byte-array 1024)
+ packet (DatagramPacket. buf 1024)
+ _ (.receive server packet)
+ non-zero-bytes (filter #(not (zero? %)) (.getData packet))
+ non-zero-bytes (byte-array non-zero-bytes)]
+ (binding [*out* sw]
+ (println (String. non-zero-bytes "UTF-8")))))]
+ (while (not (realized? fut))
+ (tu/bb nil
+ "-e" "(load-file (io/file \"test-resources\" \"babashka\" \"statsd.clj\"))"
+ "-e" "(require '[statsd-client :as c])"
+ "-e" "(c/increment :foo)"))
+ (is (= ":foo:1|c\n" (str sw)))))