Merge branch 'master' into clojure.data.xml

This commit is contained in:
Michiel Borkent 2020-04-18 20:54:05 +02:00
commit 32dc498261
81 changed files with 3368 additions and 466 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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}}}

18
.dockerignore Normal file
View file

@ -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

2
.github/FUNDING.yml vendored
View file

@ -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

8
.github/script/deploy vendored Executable file
View file

@ -0,0 +1,8 @@
#!/usr/bin/env bash
if [ -z "$GITHUB_HEAD_REF" ] && [ "${GITHUB_REF##*/}" = "master" ]
then
lein deploy clojars
fi
exit 0;

36
.github/script/docker vendored Executable file
View file

@ -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;

345
.github/workflows/build.yml vendored Normal file
View file

@ -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

4
.gitignore vendored
View file

@ -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

3
.gitmodules vendored
View file

@ -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

View file

@ -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

1
CONTRIBUTING.md Normal file
View file

@ -0,0 +1 @@
See [doc/dev.md](doc/dev.md).

View file

@ -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"]

559
README.md
View file

@ -1,10 +1,8 @@
<img src="logo/babashka.svg" width="425px">
[![CircleCI](https://circleci.com/gh/borkdude/babashka/tree/master.svg?style=shield)](https://circleci.com/gh/borkdude/babashka/tree/master)
[![Clojars Project](https://img.shields.io/clojars/v/borkdude/babashka.svg)](https://clojars.org/borkdude/babashka)
[![project chat](https://img.shields.io/badge/slack-join_chat-brightgreen.svg)](https://app.slack.com/client/T03RZGPFR/CLX41ASCS)
<!-- [![cljdoc badge](https://cljdoc.org/badge/borkdude/babashka)](https://cljdoc.org/d/borkdude/babashka/CURRENT) -->
[![Financial Contributors on Open Collective](https://opencollective.com/babashka/all/badge.svg?label=financial+contributors)](https://opencollective.com/babashka) [![Clojars Project](https://img.shields.io/clojars/v/borkdude/babashka.svg)](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
<a href="https://github.com/laheadle">@laheadle</a> on Clojurians Slack
</blockquote>
## 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:
> Im quite at home in Bash most of the time, but theres 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).
* Performance<sup>1<sup>
* 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.
<sup>1<sup> 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
[![Babashka at ClojureD 2020](https://img.youtube.com/vi/Nw8aN-nrdEk/0.jpg)](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 ) <cp> ] [ --uberscript <file> ]
[ ( --main | -m ) <main-namespace> | -e <expression> | -f <file> |
--repl | --socket-repl [<host>:]<port> ]
--repl | --socket-repl [<host>:]<port> | --nrepl-server [<host>:]<port> ]
[ arg* ]
Options:
@ -160,6 +190,7 @@ Options:
-m, --main <ns> 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`.
<img src="assets/notes-example.png" width="400px">
### 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)].
<a href="https://github.com/borkdude/babashka/graphs/contributors"><img src="https://opencollective.com/babashka/contributors.svg?width=890&button=false" /></a>
### Financial Contributors
Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/babashka/contribute)]
#### Individuals
<a href="https://opencollective.com/babashka"><img src="https://opencollective.com/babashka/individuals.svg?width=890"></a>
#### 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)]
<a href="https://opencollective.com/babashka/organization/0/website"><img src="https://opencollective.com/babashka/organization/0/avatar.svg"></a>
<a href="https://opencollective.com/babashka/organization/1/website"><img src="https://opencollective.com/babashka/organization/1/avatar.svg"></a>
<a href="https://opencollective.com/babashka/organization/2/website"><img src="https://opencollective.com/babashka/organization/2/avatar.svg"></a>
<a href="https://opencollective.com/babashka/organization/3/website"><img src="https://opencollective.com/babashka/organization/3/avatar.svg"></a>
<a href="https://opencollective.com/babashka/organization/4/website"><img src="https://opencollective.com/babashka/organization/4/avatar.svg"></a>
<a href="https://opencollective.com/babashka/organization/5/website"><img src="https://opencollective.com/babashka/organization/5/avatar.svg"></a>
<a href="https://opencollective.com/babashka/organization/6/website"><img src="https://opencollective.com/babashka/organization/6/avatar.svg"></a>
<a href="https://opencollective.com/babashka/organization/7/website"><img src="https://opencollective.com/babashka/organization/7/avatar.svg"></a>
<a href="https://opencollective.com/babashka/organization/8/website"><img src="https://opencollective.com/babashka/organization/8/avatar.svg"></a>
<a href="https://opencollective.com/babashka/organization/9/website"><img src="https://opencollective.com/babashka/organization/9/avatar.svg"></a>
## License
Copyright © 2019 Michiel Borkent
Copyright © 2019-2020 Michiel Borkent
Distributed under the EPL License. See LICENSE.

BIN
assets/notes-example.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

1
babashka.curl Submodule

@ -0,0 +1 @@
Subproject commit a9e9fe83d56b020071c1a3bbeb4656e53c8a988d

View file

@ -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

View file

@ -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

128
doc/repl.md Normal file
View file

@ -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 <RET> localhost <RET> 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
```

147
examples/notes.clj Executable file
View file

@ -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
"<!DOCTYPE html>\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)))

17
examples/which.clj Executable file
View file

@ -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)))

View file

@ -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}

View file

@ -1 +1 @@
0.0.71
0.0.86

View file

@ -1 +1 @@
0.0.72-SNAPSHOT
0.0.87-SNAPSHOT

View file

@ -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 {
}

2
sci

@ -1 +1 @@
Subproject commit eebb456628beb2ac0d1e31c2be46ee0683b9ee7a
Subproject commit ead5dd7c25e0e38cb6244077ec9e57e00665cde9

View file

@ -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

View file

@ -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

7
script/install-leiningen Executable file
View file

@ -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

View file

@ -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}])"

View file

@ -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})))
"

19
script/lib_tests/clj_yaml_test Executable file
View file

@ -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)))
"

View file

@ -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)))
"

21
script/lib_tests/comb_test Executable file
View file

@ -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"))
'

18
script/lib_tests/cprop_test Executable file
View file

@ -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)))
"

View file

@ -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\"))
"

9
script/reflection.clj Executable file
View file

@ -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")))

View file

@ -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

View file

@ -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

View file

@ -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/<!!
'>!! 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/<!!
'>! async/>!!
'alt! (with-meta alt!! {:sci/macro true})
'go-loop (with-meta go-loop {:sci/macro true})})
(def async-protocols-namespace
{'ReadPort protocols/ReadPort})

View file

@ -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)})

View file

@ -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 string<payload)
(defn #^{:private true} read-netstring*
[input]
(read-bytes input (read-long input colon)))
;; And the public facing API: `read-netstring`.
(defn #^"[B" read-netstring
"Reads a classic netstring from inputan InputStream. Returns the
contained binary data as byte array."
[input]
(let [content (read-netstring* input)]
(when (not= (read-byte input) comma)
(throw (IOException. "Invalid netstring. ',' expected.")))
content))
;; Similarly the `string>payload` and `string<payload` functions
;; are defined as follows to simplify the conversion between strings
;; and byte arrays in various parts of the code.
(defn #^{:private true :tag "[B"} string>payload
[#^String s]
(.getBytes s "UTF-8"))
(defn #^{:private true :tag String} string<payload
[#^"[B" b]
(String. b "UTF-8"))
;; ## Writing a netstring
;;
;; This opposite operation writing a netstring is just as important.
;;
;; *Note:* We take here a byte array, just as we returned a byte
;; array in `read-netstring`. The netstring should not be concerned
;; about the actual contents. It just sees binary data.
;;
;; Similar to `read-netstring` we also split `write-netstring` into
;; the entry point itself and a helper function.
(defn #^{:private true} write-netstring*
[#^OutputStream output #^"[B" content]
(doto output
(.write (string>payload (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<payload k) v]))))))
;; The final missing piece is `token-seq`. This a just a simple
;; sequence which reads tokens until the next `\e`.
(defn #^{:private true} token-seq
[input]
(->> #(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))))))

View file

@ -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))

View file

@ -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)})

View file

@ -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

View file

@ -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

View file

@ -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})

View file

@ -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)})

View file

@ -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#))))

View file

@ -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)})

View file

@ -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)))

View file

@ -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)))

View file

@ -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))))))

View file

@ -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

View file

@ -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)))))

View file

@ -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)})

View file

@ -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)})

View file

@ -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 ) <cp> ] [ --uberscript <file> ]
[ ( --main | -m ) <main-namespace> | -e <expression> | -f <file> |
--repl | --socket-repl [<host>:]<port> ]
--repl | --socket-repl [<host>:]<port> | --nrepl-server [<host>:]<port> ]
[ arg* ]")
(defn print-usage []
(println usage-string))
@ -184,6 +199,7 @@
-m, --main <ns> 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)

View file

@ -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))

View file

@ -0,0 +1,4 @@
(ns my.main2)
(defn -main [& _args]
(System/getProperty "babashka.main"))

View file

@ -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")))

View file

@ -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]}

View file

@ -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))))))

View file

@ -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)))))

View file

@ -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/<!)
(reduce +))))
(async/<!! (get-result-go))")))))
(deftest binding-conveyance-test
(is (number? (edn/read-string (test-utils/bb nil "
(def ^:dynamic x 0)
(binding [x 10] (async/<!! (async/thread x)))")))))

View file

@ -3,7 +3,8 @@
[babashka.test-utils :as tu]
[clojure.edn :as edn]
[clojure.java.io :as io]
[clojure.test :as t :refer [deftest is]]))
[clojure.test :as t :refer [deftest is testing]]
[clojure.string :as str]))
(defn bb [input & args]
(edn/read-string (apply tu/bb (when (some? input) (str input)) (map str args))))
@ -28,7 +29,10 @@
(deftest main-test
(is (= "(\"1\" \"2\" \"3\" \"4\")\n"
(tu/bb nil "--classpath" "test-resources/babashka/src_for_classpath_test" "-m" "my.main" "1" "2" "3" "4"))))
(tu/bb nil "--classpath" "test-resources/babashka/src_for_classpath_test" "-m" "my.main" "1" "2" "3" "4")))
(testing "system property"
(is (= "\"my.main2\""
(str/trim (tu/bb nil "--classpath" "test-resources/babashka/src_for_classpath_test" "-m" "my.main2"))))))
(deftest uberscript-test
(let [tmp-file (java.io.File/createTempFile "uberscript" ".clj")]

View file

@ -1,8 +1,8 @@
(ns babashka.file-var-test
(:require
[babashka.test-utils :as tu]
[clojure.test :as t :refer [deftest is]]
[clojure.string :as str]))
[clojure.string :as str]
[clojure.test :as t :refer [deftest is]]))
(defn bb [input & args]
(apply tu/bb (when (some? input) (str input)) (map str args)))

View file

@ -1,14 +1,14 @@
(ns babashka.http-connection-test
(:require
[babashka.test-utils :as tu]
[clojure.test :as t :refer [deftest is]]
[clojure.string :as str]))
[clojure.string :as str]
[clojure.test :as t :refer [deftest is]]))
(defn bb [& args]
(apply tu/bb nil (map str args)))
(deftest open-connection-test
(is (= "\"1\"" (str/trim (bb "-e" "
(is (try (= "\"1\"" (str/trim (bb "-e" "
(require '[cheshire.core :as json])
(let [conn ^java.net.HttpURLConnection (.openConnection (java.net.URL. \"https://postman-echo.com/get?foo=1\"))]
(.setConnectTimeout conn 1000)
@ -18,4 +18,6 @@
err (.getErrorStream conn)
response (json/decode (slurp is) true)]
(-> response :args :foo)))
")))))
")))
(catch Exception e
(str/includes? (str e) "timed out")))))

View file

@ -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
)

View file

@ -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

View file

@ -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)

View file

@ -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))"))))

View file

@ -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)))

View file

@ -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))))

View file

@ -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)

View file

@ -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)))))

View file

@ -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")))))))

View file

@ -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)))))