Compare commits

..

2 commits

Author SHA1 Message Date
Michiel Borkent
252be31a74 wip 2020-10-13 19:39:44 +02:00
Michiel Borkent
fec742194c wip 2020-10-13 11:29:41 +02:00
21 changed files with 390 additions and 1346 deletions

View file

@ -1,28 +0,0 @@
version: 2.1
jobs:
test:
docker:
- image: clojure:openjdk-11-tools-deps-1.10.3.1087-slim-bullseye
working_directory: ~/repo
environment:
LEIN_ROOT: "true"
BABASHKA_PLATFORM: linux
resource_class: large
steps:
- checkout
- restore_cache:
keys:
- v1-dependencies-{{ checksum "deps.edn" }}
# fallback to using latest cache if no exact match is found
- v1-dependencies-
- run: |
script/test
- save_cache:
paths:
- ~/.m2
key: v1-dependencies-{{ checksum "deps.edn" }}
workflows:
version: 2
ci:
jobs:
- test

4
.gitignore vendored
View file

@ -3,7 +3,3 @@
.lein-failures .lein-failures
/pom.xml /pom.xml
.lein-repl-history .lein-repl-history
.cache
.clj-kondo/babashka
.clj-kondo/rewrite-clj
src/scratch.clj

View file

@ -1,21 +0,0 @@
# Changelog
## Unreleased
- [#63](https://github.com/babashka/pods/issues/63): create directory before un-tarring
- [#59](https://github.com/babashka/pods/issues/59): delete port file on exit
- [#65](https://github.com/babashka/pods/issues/65): fix warnings when defining var with core name in JVM
- [#66](https://github.com/babashka/pods/issues/66): Allow metadata on fn arguments for transit+json
## v0.2.0
- [#61](https://github.com/babashka/pods/issues/61): add transit as explicit JVM dependency
- [#60](https://github.com/babashka/pods/issues/60): transform pod reader error into exception of caller
- Switch "out" and "err" messages to print and flush instead of `println` ([@justone](https://github.com/justone))
- Set TCP_NODELAY on transport socket ([@retrogradeorbit](https://github.com/retrogradeorbit))
- Allow env vars OS_NAME & OS_ARCH to override os props ([@cap10morgan](https://github.com/cap10morgan))
- [#49](https://github.com/babashka/pods/issues/49): don't log socket closed exception
## v0.1.0
Initial version

130
README.md
View file

@ -1,11 +1,9 @@
# Babashka pods # babashka.pods
[![Clojars Project](https://img.shields.io/clojars/v/babashka/babashka.pods.svg)](https://clojars.org/babashka/babashka.pods)
Babashka pods are programs that can be used as Clojure libraries by babashka. Babashka pods are programs that can be used as Clojure libraries by babashka.
This is the library to load babashka pods. It is used by This is the library to load babashka pods. It is used by
[babashka](https://github.com/babashka/babashka/) but also usable from the JVM [babashka](https://github.com/borkdude/babashka/) but also usable from the JVM
and [sci](https://github.com/borkdude/sci)-based projects other than babashka. and [sci](https://github.com/borkdude/sci)-based projects other than babashka.
<a title="Wa17gs / CC BY-SA (https://creativecommons.org/licenses/by-sa/4.0)" href="https://commons.wikimedia.org/wiki/File:Below_Golden_Gate_Bridge.jpeg"><img width="512" alt="Below Golden Gate Bridge" src="https://upload.wikimedia.org/wikipedia/commons/thumb/9/9a/Below_Golden_Gate_Bridge.jpeg/512px-Below_Golden_Gate_Bridge.jpeg"></a> <a title="Wa17gs / CC BY-SA (https://creativecommons.org/licenses/by-sa/4.0)" href="https://commons.wikimedia.org/wiki/File:Below_Golden_Gate_Bridge.jpeg"><img width="512" alt="Below Golden Gate Bridge" src="https://upload.wikimedia.org/wikipedia/commons/thumb/9/9a/Below_Golden_Gate_Bridge.jpeg/512px-Below_Golden_Gate_Bridge.jpeg"></a>
@ -26,7 +24,7 @@ babashka is the pod client. When a JVM invokes a pod, the JVM is the pod client.
- _message_: a message sent from the pod client to the pod or vice versa, - _message_: a message sent from the pod client to the pod or vice versa,
encoded in [bencode](https://en.wikipedia.org/wiki/Bencode) format. encoded in [bencode](https://en.wikipedia.org/wiki/Bencode) format.
- _payload_: a particular field of a _message_ encoded in a _payload format_ - _payload_: a particular field of a _message_ encoded in a _payload format_
(currently JSON, EDN or Transit JSON). Examples are `args`, `value` and `ex-data`. _ (currently only JSON or EDN). Examples are `args`, `value` and `ex-data`. _
- _pod protocol_: the documented way of exchanging messages between a _pod - _pod protocol_: the documented way of exchanging messages between a _pod
client_ and _pod_. client_ and _pod_.
@ -48,7 +46,7 @@ _below_ in Polish and Russian. In Romanian it means _bridge_
## Available pods ## Available pods
For a list of available pods, take a look For a list of available pods, take a look
[here](https://github.com/babashka/babashka/blob/master/doc/projects.md#pods). [here](https://github.com/borkdude/babashka/blob/master/doc/libraries.md#pods).
## Status ## Status
@ -72,47 +70,6 @@ On the JVM:
;;=> [#:next.jdbc{:update-count 0}] ;;=> [#:next.jdbc{:update-count 0}]
``` ```
### Where does the pod come from?
When calling `load-pod` with a string or vector of strings (or declaring it in your `bb.edn`),
the pod is looked up on the local file system (either using the PATH, or using an absolute path).
When it is called with a qualified symbol and a version - like `(load-pod 'org.babashka/aws "0.0.5")`
then it will be looked up in and downloaded from the [pod-registry](https://github.com/babashka/pod-registry). You can customize the file system location that `load-pod` will use by setting the `BABASHKA_PODS_DIR` environment variable.
By default babashka will search for a pod binary matching your system's OS and arch. If you want to download
pods for a different OS / arch (e.g. for deployment to servers), you can set one or both of the following
environment variables:
- `BABASHKA_PODS_OS_NAME=Linux` (or `Mac OS X` or any other value returned by Java's `os.name` property)
- `BABASHKA_PODS_OS_ARCH=aarch64` (or `amd64` or any other value returned by Java's `os.arch` property)
### In a babashka project
As of babashka 0.8.0 you can declare the pods your babashka project uses in your `bb.edn` file like so:
```clojure
:pods {org.babashka/hsqldb {:version "0.1.0"} ; will be downloaded from the babashka pod registry
my.local/pod {:path "../pod-my-local/my-pod-binary"
:cache false}} ; optionally disable namespace caching if you're actively working on this pod
```
Then you can just require the pods in your code like any other clojure lib:
```clojure
(ns my.project
(:require [pod.babashka.hsqldb :as sql]
[my.local.pod :as my-pod]))
(def db "jdbc:hsqldb:mem:testdb;sql.syntax_mys=true")
(sql/execute! db ["create table foo ( foo int );"])
;;=> [#:next.jdbc{:update-count 0}]
(my-pod/do-a-thing "foo")
;;=> "something"
```
The pods will then be loaded on demand when you require them. No need to call `load-pod` explicitly.
## Sci ## Sci
To use pods in a [sci](https://github.com/borkdude/sci) based project, see To use pods in a [sci](https://github.com/borkdude/sci) based project, see
@ -130,8 +87,8 @@ light weight replacement for native interop (JNI, JNA, etc.).
### Examples ### Examples
Beyond the already available pods mentioned above, educational examples of pods Beyond the already available pods mentioned above, eductional examples of pods
can be found [here](examples): can be found [here](../examples/pods):
- [pod-lispyclouds-sqlite](examples/pod-lispyclouds-sqlite): a pod that - [pod-lispyclouds-sqlite](examples/pod-lispyclouds-sqlite): a pod that
allows you to create and fire queries at a [sqlite](https://www.sqlite.org/) allows you to create and fire queries at a [sqlite](https://www.sqlite.org/)
@ -179,19 +136,19 @@ format that only has four types:
- byte strings - byte strings
Additionally, _payloads_ like `args` (arguments) or `value` (a function return Additionally, _payloads_ like `args` (arguments) or `value` (a function return
value) are encoded in either EDN, JSON or Transit JSON. value) are encoded in either JSON or EDN.
So remember: messages are in bencode, payloads (particular fields in the So remember: messages are in bencode, payloads (particular fields in the
message) are in either EDN, JSON or Transit JSON. message) are in either JSON or EDN.
Bencode is chosen as the message format because it is a light-weight format Bencode is chosen as the message format because it is a light-weight format
which can be implemented in 200-300 lines of code in most languages. If pods are which can be implemented in 200-300 lines of code in most languages. If pods are
implemented in Clojure, they only need to depend on the implemented in Clojure, they only need to depend on the
[bencode](https://github.com/nrepl/bencode) library and use `pr-str` and [bencode](https://github.com/nrepl/bencode) library and use `pr-str` and
`edn/read-string` for encoding and decoding payloads. `edn/read-string` for encoding and decoding payloads.
So we use bencode as the first encoding and choose one of multiple richer encodings on top of this, similar to how the nREPL protocol is implemented. More So we use bencode as the first encoding and choose one of multiple richer encodings on top of this, similar to how the nREPL protocol is implemented. More
payload formats might be added in the future. payload formats might be added in the future (e.g. transit).
Other languages typically use a bencode library + a JSON library to encode payloads. Other languages typically use a bencode library + a JSON library to encode payloads.
When calling the `babashka.pods/load-pod` function, the pod client will start When calling the `babashka.pods/load-pod` function, the pod client will start
@ -212,7 +169,7 @@ Encoded in bencode this looks like:
;;=> d2:op8:describee ;;=> d2:op8:describee
``` ```
The pod should reply to this request with a message similar to: The pod should reply to this request with a message in the vein of:
``` clojure ``` clojure
{"format" "json" {"format" "json"
@ -226,9 +183,7 @@ In this reply, the pod declares that payloads will be encoded and decoded using
JSON. It also declares that the pod exposes one namespace, JSON. It also declares that the pod exposes one namespace,
`pod.lispyclouds.sqlite` with one var `execute!`. `pod.lispyclouds.sqlite` with one var `execute!`.
To encode payloads in EDN use `"edn"` and for Transit JSON use `"transit+json"`. The pod encodes the above map to bencode and writes it to stdoud. The pod client
The pod encodes the above map to bencode and writes it to stdout. The pod client
reads this message from the pod's stdout. reads this message from the pod's stdout.
Upon receiving this message, the pod client creates these namespaces and vars. Upon receiving this message, the pod client creates these namespaces and vars.
@ -254,8 +209,8 @@ As a pod user, you can load the pod with:
#### invoke #### invoke
When invoking a var that is related to the pod, let's call it a _proxy var_, the When invoking a var that is related to the pod, let's call it a _proxy var_, the
pod client reaches out to the pod with the arguments encoded in EDN, JSON or Transit JSON. The pod client reaches out to the pod with the arguments encoded in JSON or EDN. The
pod will then respond with a return value encoded in EDN, JSON or Transit JSON. The pod client pod will then respond with a return value encoded in JSON or EDN. The pod client
will then decode the return value and present the user with that. will then decode the return value and present the user with that.
Example: the user invokes `(sql/execute! "select * from foo")`. The pod client Example: the user invokes `(sql/execute! "select * from foo")`. The pod client
@ -374,55 +329,6 @@ In the pod client:
nil nil
``` ```
#### Metadata
**From pod to pod client**
*Fixed Metadata on vars*
Pods may attach metadata to functions and macros by sending data to the pod client
in a `"meta"` field as part of a `"var"` section. The metadata must be an appropriate
map, encoded as an EDN string. This is only applicable to vars in the pod and will be
ignored if the var refers to Client-side code, since metadata can already be defined
in those code blocks (see 'Dynamic Metadata' below to enable the encoding of metadata).
For example, a pod can define a function called `add`:
``` clojure
{"format" "json"
"namespaces"
[{"name" "pod.babashka.demo"
"vars" [{"name" "add"
"meta" "{:doc \"arithmetic addition of 2 arguments\" :arglists ([a b])}"}]}]}
```
*Dynamic Metadata*
Pods may send metadata on values returned to the client if metadata encoding is enabled
for the particular transport format used by the pod.
For example, if your pod uses `:transit+json` as its format, you can enable metadata
encoding by adding `:transform transit/write-meta` (or whatever transit is aliased to)
to the optional map passed to `transit/writer`. e.g.:
````clojure
(transit/writer baos :json {:transform transit/write-meta})
````
##### From pod client to pod
Currently sending metadata on arguments passed to a pod function is available only for the
`transit+json` format and can be enabled on a per var basis.
A pod can enable metadata to be read on arguments by sending the "arg-meta" field to "true"
for the var representing that function. For example:
````clojure
{:format :transit+json
:namespaces [{:name "pod.babashka.demo"
:vars [{"name" "round-trip" "arg-meta" "true"}]}]}
````
#### Deferred namespace loading #### Deferred namespace loading
When your pod exposes multiple namespaces that can be used independently from When your pod exposes multiple namespaces that can be used independently from
@ -497,7 +403,7 @@ The arguments to `babashka.pods/invoke` are:
- a pod identifier string derived from the first described namespace. - a pod identifier string derived from the first described namespace.
- the symbol of the var to invoke - the symbol of the var to invoke
- the arguments to the var - the arguments to the var
- an opts map containing `:handlers` containing callback functions: `:success`, `:error` and `:done` - an opts map containing `:handler` containing callback functions: `:success`, `:error` and `:done`
The return value of `babashka.pods/invoke` is a map containing `:result`. When The return value of `babashka.pods/invoke` is a map containing `:result`. When
not using callbacks, this is the return value from the pod var invocation. When not using callbacks, this is the return value from the pod var invocation. When
@ -523,9 +429,9 @@ callback is only called if no errors were sent by the pod.
In the above example the wrapper function calls the pod identified by In the above example the wrapper function calls the pod identified by
`"pod.babashka.filewatcher"`. It calls the var `"pod.babashka.filewatcher"`. It calls the var
`pod.babashka.filewatcher/watch*`. In `:success` it pulls out received `pod.babashka.filewatcher/watch*`. In `:on-success` it pulls out received
values, passing them to the user-provided callback. Additionally, it prints any values, passing them to the user-provided callback. Additionally, it prints any
errors received from the pod library in `:error` to `*err*`. errors received from the pod library in `:on-error` to `*err*`.
A user will then use `pod.babashka.filewatcher/watch` like this: A user will then use `pod.babashka.filewatcher/watch` like this:
@ -542,7 +448,7 @@ user=> (fw/watch "/tmp" (fn [result] (prn "result" result)))
nil nil
user=> (spit "/tmp/foobar123.txt" "foo") user=> (spit "/tmp/foobar123.txt" "foo")
nil nil
user=> "result" {:path "/private/tmp/foobar123.txt", :type :create} user=> "result" {:path "/private/tmp/foobar123.txt", :type "create"}
``` ```
## Run tests ## Run tests

2
bb.edn
View file

@ -1,2 +0,0 @@
{:tasks {test {:doc "Run tests"
:task (shell "script/test")}}}

View file

@ -1,15 +1,13 @@
{:deps {nrepl/bencode {:mvn/version "1.1.0"} {:deps {nrepl/bencode {:mvn/version "1.1.0"}
cheshire/cheshire {:mvn/version "5.10.0"} cheshire {:mvn/version "5.10.0"}}
com.cognitect/transit-clj {:mvn/version "1.0.324"}
babashka/fs {:mvn/version "0.1.6"}}
:aliases :aliases
{:sci {:sci
{:extra-deps {:extra-deps
{borkdude/sci {:git/url "https://github.com/borkdude/sci" {borkdude/sci {:git/url "https://github.com/borkdude/sci"
:sha "5aa9031eb3692a2207106076088fcab7347c2299"}}} :sha "a7f8d05f08ab150621c2403dacdd57c47ea09ff4"}}}
:test :test
{:extra-deps {:extra-deps
{cognitect/test-runner {test-runner
{:git/url "https://github.com/cognitect-labs/test-runner" {:git/url "https://github.com/cognitect-labs/test-runner"
:sha "cb96e80f6f3d3b307c59cbeb49bb0dcb3a2a780b"}} :sha "cb96e80f6f3d3b307c59cbeb49bb0dcb3a2a780b"}}
:extra-paths ["test"] :extra-paths ["test"]
@ -19,5 +17,5 @@
{lambdaisland/kaocha {:mvn/version "1.0.632"}} {lambdaisland/kaocha {:mvn/version "1.0.632"}}
:main-opts ["-m" "kaocha.runner"]} :main-opts ["-m" "kaocha.runner"]}
:test-pod :test-pod
{:extra-paths ["src" "test-pod"] {:paths ["src" "test-pod"]
:main-opts ["-m" "pod.test-pod"]}}} :main-opts ["-m" "pod.test-pod"]}}}

View file

@ -1,17 +1,15 @@
(defproject babashka/babashka.pods "0.2.0" (defproject babashka/babashka.pods "0.0.1-SNAPSHOT"
:description "babashka pods" :description "babashka pods"
:url "https://github.com/babashka/babashka.pods" :url "https://github.com/babashka/babashka.pods"
:scm {:name "git" :scm {:name "git"
:url "https://github.com/babashka/babashka.pods"} :url "https://github.com/babashka/babashka.pods"}
:license {:name "EPL-1.0" :license {:name "EPL-1.0"
:url "https://www.eclipse.org/legal/epl-1.0/"} :url "https://www.eclipse.org/legal/epl-1.0/"}
:dependencies [[org.clojure/clojure "1.10.3"] :dependencies [[org.clojure/clojure "1.10.2-alpha1"]
[nrepl/bencode "1.1.0"] [nrepl/bencode "1.1.0"]
[cheshire "5.10.0"] [cheshire "5.10.0"]]
[babashka/fs "0.1.6"]
[com.cognitect/transit-clj "1.0.329"]]
:deploy-repositories [["clojars" {:url "https://clojars.org/repo" :deploy-repositories [["clojars" {:url "https://clojars.org/repo"
:username :env/clojars_user :username :env/babashka_nrepl_clojars_user
:password :env/clojars_pass :password :env/babashka_nrepl_clojars_pass
:sign-releases false}]] :sign-releases false}]]
:profiles {:test {:dependencies [[borkdude/sci "0.2.4"]]}}) :profiles {:test {:dependencies [[borkdude/sci "0.0.13-alpha.27"]]}})

View file

@ -1,17 +0,0 @@
#!/usr/bin/env bb
(ns changelog
(:require [clojure.string :as str]))
(let [changelog (slurp "CHANGELOG.md")
replaced (str/replace changelog
#" #(\d+)"
(fn [[_ issue after]]
(format " [#%s](https://github.com/babashka/pods/issues/%s)%s"
issue issue (str after))))
replaced (str/replace replaced
#"@([a-zA-Z0-9-_]+)([, \.)])"
(fn [[_ name after]]
(format "[@%s](https://github.com/%s)%s"
name name after)))]
(spit "CHANGELOG.md" replaced))

View file

@ -1,32 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -eou pipefail clojure -A:test -n babashka.pods.jvm-test
#clojure -A:test:kaocha --focus babashka.pods.jvm-test
export BABASHKA_POD_TEST_FORMAT clojure -A:sci:test -n babashka.pods.sci-test
export BABASHKA_POD_TEST_SOCKET #clojure -A:sci:test:kaocha --focus babashka.pods.sci-test
# format = edn
BABASHKA_POD_TEST_FORMAT=edn
echo "Testing edn"
clojure -M:test -n babashka.pods.jvm-test
clojure -M:sci:test -n babashka.pods.sci-test
clojure -M:test -n babashka.pods.impl-test
# format = json
BABASHKA_POD_TEST_FORMAT=json
echo "Testing json"
clojure -M:test -n babashka.pods.jvm-test
clojure -M:sci:test -n babashka.pods.sci-test
# format = json
BABASHKA_POD_TEST_FORMAT="transit+json"
echo "Testing transit"
clojure -M:test -n babashka.pods.jvm-test
clojure -M:sci:test -n babashka.pods.sci-test
# socket = true
unset BABASHKA_POD_TEST_FORMAT
BABASHKA_POD_TEST_SOCKET=true
echo "Testing socket"
clojure -M:test -n babashka.pods.jvm-test
clojure -M:sci:test -n babashka.pods.sci-test

View file

@ -3,29 +3,12 @@
(defn load-pod (defn load-pod
([pod-spec] (load-pod pod-spec nil)) ([pod-spec] (load-pod pod-spec nil))
([pod-spec version opts]
(load-pod pod-spec (assoc opts :version version)))
([pod-spec opts] (jvm/load-pod pod-spec opts))) ([pod-spec opts] (jvm/load-pod pod-spec opts)))
(defn unload-pod (defn unload-pod
([pod-id-or-pod] (unload-pod pod-id-or-pod {})) ([pod-id] (unload-pod pod-id {}))
([pod-id-or-pod opts] (jvm/unload-pod pod-id-or-pod opts))) ([pod-id opts] (jvm/unload-pod pod-id opts)))
(defn invoke (defn invoke
([pod-id-or-pod sym args] (invoke pod-id-or-pod sym args {})) ([pod-id sym args] (invoke pod-id sym args {}))
([pod-id-or-pod sym args opts] (jvm/invoke pod-id-or-pod sym args opts))) ([pod-id sym args opts] (jvm/invoke pod-id sym args opts)))
(defmacro copy-var [name var]
`(do (def ~name ~var)
(let [m# (meta (var ~var))
doc# (:doc m#)
arglists# (:arglists m#)]
(alter-meta! (var ~name) assoc
:arglists arglists#
:doc doc#))))
#_:clj-kondo/ignore
(do
(copy-var add-transit-read-handler! jvm/add-transit-read-handler!)
(copy-var add-transit-write-handler! jvm/add-transit-write-handler!)
(copy-var set-default-transit-write-handler! jvm/set-default-transit-write-handler!))

View file

@ -1,13 +1,10 @@
(ns babashka.pods.impl (ns babashka.pods.impl
{:no-doc true} {:no-doc true}
(:refer-clojure :exclude [read]) (:refer-clojure :exclude [read])
(:require [babashka.pods.impl.resolver :as resolver] (:require [bencode.core :as bencode]
[bencode.core :as bencode]
[cheshire.core :as cheshire] [cheshire.core :as cheshire]
[clojure.edn :as edn] [clojure.edn :as edn]
[clojure.java.io :as io] [clojure.java.io :as io])
[clojure.string :as str]
[cognitect.transit :as transit])
(:import [java.io PushbackInputStream] (:import [java.io PushbackInputStream]
[java.net Socket])) [java.net Socket]))
@ -28,9 +25,6 @@
(defn bytes->string [^"[B" bytes] (defn bytes->string [^"[B" bytes]
(String. bytes)) (String. bytes))
(defn bytes->boolean [^"[B" bytes]
(= "true" (String. bytes)))
(defn get-string [m k] (defn get-string [m k]
(-> (get m k) (-> (get m k)
bytes->string)) bytes->string))
@ -39,66 +33,9 @@
(some-> (get m k) (some-> (get m k)
bytes->string)) bytes->string))
(defn get-maybe-boolean [m k]
(some-> (get m k)
bytes->boolean))
(defn next-id [] (defn next-id []
(str (java.util.UUID/randomUUID))) (str (java.util.UUID/randomUUID)))
(def ^:dynamic *pod-id* nil)
(defonce transit-read-handlers (atom {}))
(defonce transit-read-handler-maps (atom {}))
(defn update-transit-read-handler-map []
(swap! transit-read-handler-maps assoc *pod-id*
(transit/read-handler-map (get @transit-read-handlers *pod-id*))))
(defn transit-json-read [pod-id ^String s]
(with-open [bais (java.io.ByteArrayInputStream. (.getBytes s "UTF-8"))]
(let [r (transit/reader bais :json {:handlers (get @transit-read-handler-maps pod-id)})]
(transit/read r))))
;; https://www.cognitect.com/blog/2015/9/10/extending-transit
(defn add-transit-read-handler!
([tag fn]
(let [rh (transit/read-handler fn)]
(swap! transit-read-handlers assoc-in [*pod-id* tag] rh)
(update-transit-read-handler-map)
nil)))
(defonce transit-write-handlers (atom {}))
(defonce transit-write-handler-maps (atom {}))
(defn update-transit-write-handler-map []
(swap! transit-write-handler-maps assoc *pod-id*
(transit/write-handler-map (get @transit-write-handlers *pod-id*))))
;; https://www.cognitect.com/blog/2015/9/10/extending-transit
(defn add-transit-write-handler!
[classes tag fn]
(let [rh (transit/write-handler tag fn)]
(doseq [class classes]
(swap! transit-write-handlers assoc-in [*pod-id* class] rh)))
(update-transit-write-handler-map)
nil)
(defonce transit-default-write-handlers (atom {}))
(defn set-default-transit-write-handler! [tag-fn val-fn]
(let [wh (transit/write-handler tag-fn val-fn)]
(swap! transit-default-write-handlers assoc *pod-id* wh)))
(defn transit-json-write
[pod-id ^String s metadata?]
(with-open [baos (java.io.ByteArrayOutputStream. 4096)]
(let [w (transit/writer baos :json (cond-> {:handlers (get @transit-write-handler-maps pod-id)
:default-handler (get @transit-default-write-handlers pod-id)}
metadata? (assoc :transform transit/write-meta)))]
(transit/write w s)
(str baos))))
(defn invoke [pod pod-var args opts] (defn invoke [pod pod-var args opts]
(let [handlers (:handlers opts) (let [handlers (:handlers opts)
stream (:stdin pod) stream (:stdin pod)
@ -106,8 +43,7 @@
chans (:chans pod) chans (:chans pod)
write-fn (case format write-fn (case format
:edn pr-str :edn pr-str
:json cheshire/generate-string :json cheshire/generate-string)
:transit+json #(transit-json-write (:pod-id pod) % (:arg-meta opts)))
id (next-id) id (next-id)
chan (if handlers handlers chan (if handlers handlers
(promise)) (promise))
@ -131,18 +67,11 @@
#(Boolean/parseBoolean %)) #(Boolean/parseBoolean %))
name-sym (symbol name) name-sym (symbol name)
sym (symbol ns-name-str name) sym (symbol ns-name-str name)
code (get-maybe-string var "code") code (get-maybe-string var "code")]
vmeta (some-> (get var "meta")
bytes->string
edn/read-string)
name-sym (if vmeta
(with-meta name-sym vmeta)
name-sym)
metadata? (get-maybe-boolean var "arg-meta")]
[name-sym [name-sym
(or code (or code
(fn [& args] (fn [& args]
(let [res (invoke pod sym args {:async async? :arg-meta metadata?})] (let [res (invoke pod sym args {:async async?})]
res)))])) res)))]))
vars)) vars))
@ -165,127 +94,101 @@
(catch Exception e (catch Exception e
(binding [*out* *err*] (binding [*out* *err*]
(println "Cannot read JSON: " (pr-str s)) (println "Cannot read JSON: " (pr-str s))
(throw e))))) (throw e))))))]
:transit+json (try
(fn [s] (loop []
(try (transit-json-read (:pod-id pod) s) (let [reply (try (read stdout)
(catch Exception e (catch java.io.EOFException _
(binding [*out* *err*] ::EOF))]
(println "Cannot read Transit JSON: " (pr-str s)) (when-not (identical? ::EOF reply)
(throw e))))))] (let [id (get reply "id")
(binding [*pod-id* (:pod-id pod)] id (bytes->string id)
(try value* (find reply "value")
(loop [] value (some-> value*
(let [reply (try (read stdout) second
(catch java.io.EOFException _ bytes->string
::EOF) read-fn)
(catch java.net.SocketException e status (get reply "status")
(if (= "Socket closed" (ex-message e)) status (set (map (comp keyword bytes->string) status))
::EOF error? (contains? status :error)
(throw e))))] done? (or error? (contains? status :done))
(when-not (identical? ::EOF reply) [ex-message ex-data]
(let [id (get reply "id") (when error?
id (bytes->string id) [(or (some-> (get reply "ex-message")
value* (find reply "value") bytes->string)
[exception value] (try (some->> value* "")
second (or (some-> (get reply "ex-data")
bytes->string bytes->string
read-fn read-fn)
(vector nil)) {})])
(catch Exception e namespace (when-let [v (get reply "vars")]
[e nil])) (let [name-str (-> (get reply "name")
status (get reply "status") bytes->string)
status (set (map (comp keyword bytes->string) status)) name (symbol name-str)]
error? (or exception (contains? status :error)) {:name name
done? (or error? exception (contains? status :done)) :vars (bencode->vars pod name-str v)}))
[ex-message ex-data] chan (get @chans id)
(when error? promise? (instance? clojure.lang.IPending chan)
[(or (some-> (get reply "ex-message") exception (when (and promise? error?)
bytes->string) (ex-info ex-message ex-data))
"") ;; NOTE: if we need more fine-grained handlers, we will add
(or (some-> (get reply "ex-data") ;; a :raw handler that will just get the bencode message's raw
bytes->string ;; data
read-fn) {error-handler :error
{})]) done-handler :done
namespace (when-let [v (get reply "vars")] success-handler :success} (when (map? chan)
(let [name-str (-> (get reply "name") chan)
bytes->string) out (some-> (get reply "out")
name (symbol name-str)] bytes->string)
{:name name err (some-> (get reply "err")
:vars (bencode->vars pod name-str v)})) bytes->string)]
chan (get @chans id) (when (or value* error? namespace)
promise? (instance? clojure.lang.IPending chan) (cond promise?
exception (or exception (deliver chan (cond error? exception
(when (and promise? error?) value value
(ex-info ex-message ex-data))) namespace namespace))
;; NOTE: if we need more fine-grained handlers, we will add (and (not error?) success-handler)
;; a :raw handler that will just get the bencode message's raw (success-handler value)
;; data (and error? error-handler)
{error-handler :error (error-handler {:ex-message ex-message
done-handler :done :ex-data ex-data})))
success-handler :success} (when (map? chan) (when (and done? (not error?))
chan) (when promise?
out (some-> (get reply "out") (deliver chan nil))
bytes->string) (when done-handler
err (some-> (get reply "err") (done-handler)))
bytes->string)] (when out
;; NOTE: write to out and err before delivering promise for making (binding [*out* out-stream]
;; listening to output synchronous. (println out)))
(when out (when err (binding [*out* err-stream]
(binding [*out* out-stream] (println err))))
(print out) (recur))))
(.flush ^java.io.Writer out-stream))) (catch Exception e
(when err (binding [*out* *err* #_err-stream]
(binding [*out* err-stream] (prn e))))))
(print err)
(.flush ^java.io.Writer err-stream)))
(when (or value* error? namespace)
(cond promise?
(deliver chan (cond error? exception
value value
namespace namespace))
(and (not error?) success-handler)
(success-handler value)
(and error? error-handler)
(error-handler {:ex-message ex-message
:ex-data ex-data})))
(when (and done? (not error?))
(when promise?
(deliver chan nil))
(when done-handler
(done-handler))))
(recur))))
(catch Exception e
(binding [*out* *err* #_err-stream]
(prn e)))))))
(def pods (atom {})) (def pods (atom {}))
(defn get-pod-id [x]
(if (map? x)
(:pod/id x)
x))
(defn lookup-pod [pod-id] (defn lookup-pod [pod-id]
(get @pods pod-id)) (get @pods pod-id))
(defn destroy* [{:keys [:stdin :process :ops]}] (defn destroy [pod-id]
(if (contains? ops :shutdown) (when-let [pod (lookup-pod pod-id)]
(do (write stdin (if (contains? (:ops pod) :shutdown)
{"op" "shutdown" (do (write (:stdin pod)
"id" (next-id)}) {"op" "shutdown"
(.waitFor ^Process process)) "id" (next-id)})
(.destroy ^Process process))) (.waitFor ^Process (:process pod)))
(.destroy ^Process (:process pod)))
(when-let [rns (:remove-ns pod)]
(doseq [[ns-name _] (:namespaces pod)]
(rns ns-name)))))
(defn destroy [pod-id-or-pod] (def next-pod-id
(let [pod-id (get-pod-id pod-id-or-pod)] (let [counter (atom 0)]
(when-let [pod (lookup-pod pod-id)] (fn []
(destroy* pod) (let [[o _] (swap-vals! counter inc)]
(when-let [rns (:remove-ns pod)] o))))
(doseq [[ns-name _] (:namespaces pod)]
(rns ns-name))))
(swap! pods dissoc pod-id)
nil))
(def bytes->symbol (def bytes->symbol
(comp symbol bytes->string)) (comp symbol bytes->string))
@ -309,125 +212,54 @@
the socket is connected." the socket is connected."
^Socket ^Socket
[^String hostname ^Integer port] [^String hostname ^Integer port]
(doto (Socket. hostname port) (Socket. hostname port))
(.setTcpNoDelay true)))
(defn close-socket (defn close-socket
"Close the socket, and also closes its input and output streams." "Close the socket, and also closes its input and output streams."
[^Socket socket] [^Socket socket]
(try (.close socket) (.close socket))
nil
(catch java.net.SocketException _ nil)))
(defn port-file [pid] (defn gobbler [^java.io.InputStream is]
(doto (io/file (str ".babashka-pod-" pid ".port")) (future
(.deleteOnExit))) (loop []
(let [v (.read is)]
(when-not (= -1 v)
(print (char v))
(recur))))))
(defn read-port [^java.io.File port-file] (defn read-port [pid]
(loop [] 1888 #_(loop []
(let [f port-file] (let [f (io/file (str ".babashka/pods/" pid ".port"))]
(if-let [s (when (.exists f) (if (.exists f)
(let [s (slurp f)] (edn/read-string (slurp f))
(when (str/ends-with? s "\n")
(str/trim s))))]
(Integer/parseInt s)
(recur))))) (recur)))))
(defn debug [& strs]
(binding [*out* *err*]
(println (str/join " " (map pr-str strs)))))
(defn resolve-pod [pod-spec {:keys [:version :path :force] :as opts}]
(when (qualified-symbol? pod-spec)
(when (and (not version) (not path))
(throw (IllegalArgumentException. "Version or path must be provided")))
(when (and version path)
(throw (IllegalArgumentException. "You must provide either version or path, not both"))))
(let [resolved (when (and (qualified-symbol? pod-spec) version)
(resolver/resolve pod-spec version force))
opts (if resolved
(if-let [extra-opts (:options resolved)]
(merge opts extra-opts)
opts)
opts)
pod-spec (cond
resolved [(:executable resolved)]
path [path]
(string? pod-spec) [pod-spec]
:else pod-spec)]
{:pod-spec pod-spec, :opts opts}))
(defn run-pod [pod-spec {:keys [:transport] :as _opts}]
(let [pb (ProcessBuilder. ^java.util.List pod-spec)
socket? (identical? :socket transport)
_ (if socket?
(.inheritIO pb)
(.redirectError pb java.lang.ProcessBuilder$Redirect/INHERIT))
_ (cond-> (doto (.environment pb)
(.put "BABASHKA_POD" "true"))
socket? (.put "BABASHKA_POD_TRANSPORT" "socket"))
p (.start pb)
port-file (when socket? (port-file (.pid p)))
socket-port (when socket? (read-port port-file))
[socket stdin stdout]
(if socket?
(let [^Socket socket
(loop []
(if-let [sock (try (create-socket "localhost" socket-port)
(catch java.net.ConnectException _
nil))]
sock
(recur)))]
[socket
(.getOutputStream socket)
(PushbackInputStream. (.getInputStream socket))])
[nil (.getOutputStream p) (java.io.PushbackInputStream. (.getInputStream p))])]
{:process p
:socket socket
:stdin stdin
:stdout stdout}))
(defn describe-pod [{:keys [:stdin :stdout]}]
(write stdin {"op" "describe"
"id" (next-id)})
(read stdout))
(defn describe->ops [describe-reply]
(some->> (get describe-reply "ops") keys (map keyword) set))
(defn describe->metadata [describe-reply resolve-fn]
(let [format (-> (get describe-reply "format") bytes->string keyword)
ops (describe->ops describe-reply)
readers (when (identical? :edn format)
(read-readers describe-reply resolve-fn))]
{:format format, :ops ops, :readers readers}))
(defn run-pod-for-metadata [pod-spec opts]
(let [running-pod (run-pod pod-spec opts)
describe-reply (describe-pod running-pod)
ops (describe->ops describe-reply)]
(destroy* (assoc running-pod :ops ops))
describe-reply))
(defn load-pod-metadata [unresolved-pod-spec {:keys [:download-only] :as opts}]
(let [{:keys [:pod-spec :opts]} (resolve-pod unresolved-pod-spec opts)]
(if download-only
(resolver/warn "Not running pod" unresolved-pod-spec "to pre-cache metadata because OS and/or arch are different than system")
(run-pod-for-metadata pod-spec opts))))
(defn load-pod (defn load-pod
([pod-spec] (load-pod pod-spec nil)) ([pod-spec] (load-pod pod-spec nil))
([pod-spec opts] ([pod-spec {:keys [:remove-ns :resolve :socket :inherit-io]}]
(let [{:keys [:pod-spec :opts]} (resolve-pod pod-spec opts) (let [pod-spec (if (string? pod-spec) [pod-spec] pod-spec)
{:keys [:remove-ns :resolve]} opts pb (ProcessBuilder. ^java.util.List pod-spec)
_ (if inherit-io
{p :process, stdin :stdin, stdout :stdout, socket :socket (.inheritIO pb)
:as running-pod} (.redirectError pb java.lang.ProcessBuilder$Redirect/INHERIT))
(run-pod pod-spec opts) _ (doto (.environment pb)
(.put "BABASHKA_POD" "true"))
reply (or (:metadata opts) p (.start pb)
(describe-pod running-pod)) pid (.pid p)
{:keys [:format :ops :readers]} (describe->metadata reply resolve) socket-port (when socket (read-port pid))
[stdin stdout _gobbler]
(if socket
(let [socket (create-socket "localhost" socket-port)]
[(.getOutputStream socket)
(PushbackInputStream. (.getInputStream socket))])
[(.getOutputStream p) (java.io.PushbackInputStream. (.getInputStream p))])
_ (write stdin {"op" "describe"
"id" (next-id)})
reply (read stdout)
format (-> (get reply "format") bytes->string keyword)
ops (some->> (get reply "ops") keys (map keyword) set)
readers (when (identical? :edn format)
(read-readers reply resolve))
pod {:process p pod {:process p
:pod-spec pod-spec :pod-spec pod-spec
:stdin stdin :stdin stdin
@ -439,16 +271,11 @@
:err *err* :err *err*
:remove-ns remove-ns :remove-ns remove-ns
:readers readers} :readers readers}
_ (add-shutdown-hook! #(destroy pod))
pod-namespaces (get reply "namespaces") pod-namespaces (get reply "namespaces")
pod-id (or (when-let [ns (first pod-namespaces)] pod-id (or (when-let [ns (first pod-namespaces)]
(get-string ns "name")) (get-string ns "name"))
(next-id)) (next-id))
_ (add-shutdown-hook! #(do
(destroy pod-id)
(when socket
;; this probably isn't necessary because we
;; killed the process, but anyway
(close-socket socket))))
pod (assoc pod :pod-id pod-id) pod (assoc pod :pod-id pod-id)
pod-namespaces (mapv #(bencode->namespace pod %) pod-namespaces (mapv #(bencode->namespace pod %)
pod-namespaces) pod-namespaces)
@ -468,9 +295,8 @@
@prom)) @prom))
(defn invoke-public [pod-id fn-sym args opts] (defn invoke-public [pod-id fn-sym args opts]
(let [pod-id (get-pod-id pod-id) (let [pod (lookup-pod pod-id)]
pod (lookup-pod pod-id)]
(invoke pod fn-sym args opts))) (invoke pod fn-sym args opts)))
(defn unload-pod [pod-id-or-pod] (defn unload-pod [pod-id]
(destroy pod-id-or-pod)) (destroy pod-id))

View file

@ -1,225 +0,0 @@
(ns babashka.pods.impl.resolver
{:no-doc true}
(:refer-clojure :exclude [resolve])
(:require [clojure.edn :as edn]
[clojure.java.io :as io]
[clojure.java.shell :refer [sh]]
[clojure.string :as str])
(:import [java.net URL HttpURLConnection]
[java.nio.file Files]))
(set! *warn-on-reflection* true)
(defn normalize-arch [arch]
(if (= "amd64" arch)
"x86_64"
arch))
(defn normalize-os [os]
(-> os str/lower-case (str/replace #"\s+" "_")))
(def os
(delay
{:os/name (or (System/getenv "BABASHKA_PODS_OS_NAME")
(System/getProperty "os.name"))
:os/arch (let [arch (or (System/getenv "BABASHKA_PODS_OS_ARCH")
(System/getProperty "os.arch"))]
(normalize-arch arch))}))
(defn warn [& strs]
(binding [*out* *err*]
(apply println strs)))
(defn match-artifacts
([package] (match-artifacts package (:os/arch @os)))
([package arch]
(let [artifacts (:pod/artifacts package)
res (filter (fn [{os-name :os/name
os-arch :os/arch}]
(let [os-arch (normalize-arch os-arch)]
(and (re-matches (re-pattern os-name) (:os/name @os))
(re-matches (re-pattern os-arch)
arch))))
artifacts)]
(if (empty? res)
(if (and (= "Mac OS X" (:os/name @os))
(= "aarch64" (:os/arch @os)))
;; Rosetta2 fallback on Apple M1 machines
(match-artifacts package "x86_64")
(throw (IllegalArgumentException. (format "No executable found for pod %s (%s) and OS %s/%s"
(:pod/name package)
(:pod/version package)
(:os/name @os)
(:os/arch @os)))))
res))))
(defn unzip [{:keys [^java.io.File zip-file
^java.io.File destination-dir
verbose]}]
(when verbose (warn "Unzipping" (.getPath zip-file) "to" (.getPath destination-dir)))
(let [output-path (.toPath destination-dir)
zip-file (io/file zip-file)
_ (.mkdirs (io/file destination-dir))]
(with-open
[fis (Files/newInputStream (.toPath zip-file) (into-array java.nio.file.OpenOption []))
zis (java.util.zip.ZipInputStream. fis)]
(loop []
(let [entry (.getNextEntry zis)]
(when entry
(let [entry-name (.getName entry)
new-path (.resolve output-path entry-name)]
(if (.isDirectory entry)
(Files/createDirectories new-path ^"[Ljava.nio.file.attribute.FileAttribute;"
(into-array java.nio.file.attribute.FileAttribute []))
(Files/copy ^java.io.InputStream zis
new-path
^"[Ljava.nio.file.CopyOption;"
(into-array
java.nio.file.CopyOption
[java.nio.file.StandardCopyOption/REPLACE_EXISTING]))))
(recur)))))))
(defn un-tgz [^java.io.File zip-file ^java.io.File destination-dir verbose?]
(when verbose? (warn "Unzipping" (.getPath zip-file) "to" (.getPath destination-dir)))
(let [tmp-file (java.io.File/createTempFile "glam" ".tar")
output-path (.toPath tmp-file)]
(with-open
[fis (Files/newInputStream (.toPath zip-file) (into-array java.nio.file.OpenOption []))
zis (java.util.zip.GZIPInputStream. fis)]
(Files/copy ^java.io.InputStream zis
output-path
^"[Ljava.nio.file.CopyOption;"
(into-array
[java.nio.file.StandardCopyOption/REPLACE_EXISTING])))
(.mkdirs destination-dir)
(let [res (sh "tar" "xf" (.getPath tmp-file) "--directory" (.getPath destination-dir))]
(when-not (zero? (:exit res))
(throw (ex-info (:err res) res))))
(.delete tmp-file)))
(defn make-executable [dest-dir executables verbose?]
(doseq [e executables]
(let [f (io/file dest-dir e)]
(when verbose? (warn "Making" (.getPath f) "executable."))
(.setExecutable f true))))
(defn download [source ^java.io.File dest verbose?]
(when verbose? (warn "Downloading" source "to" (.getPath dest)))
(let [source (URL. source)
dest (io/file dest)
conn ^HttpURLConnection (.openConnection ^URL source)]
(.setInstanceFollowRedirects conn true)
(.connect conn)
(io/make-parents dest)
(with-open [is (.getInputStream conn)]
(io/copy is dest))))
(defn repo-dir []
(io/file (if-let [pods-dir (System/getenv "BABASHKA_PODS_DIR")]
(io/file pods-dir)
(io/file (or
(System/getenv "XDG_DATA_HOME")
(System/getProperty "user.home"))
".babashka"
"pods"))
"repository"))
(def pods-repo-dir
;; wrapped in delay for GraalVM native-image
(delay
(repo-dir)))
(defn github-url [qsym version]
(format
"https://raw.githubusercontent.com/babashka/pod-registry/master/manifests/%s/%s/manifest.edn"
qsym version))
(defn pod-manifest
[qsym version force?]
(let [f (io/file @pods-repo-dir (str qsym) (str version) "manifest.edn")]
(if (and (not force?)
(.exists f))
(edn/read-string (slurp f))
(do (download (github-url qsym version) f false)
(edn/read-string (slurp f))))))
(defn cache-dir
^java.io.File
[{pod-name :pod/name
pod-version :pod/version}]
(let [base-file
(if-let [pods-dir (System/getenv "BABASHKA_PODS_DIR")]
(io/file pods-dir)
(io/file (or
(System/getenv "XDG_CACHE_HOME")
(System/getProperty "user.home"))
".babashka"
"pods"))]
(io/file base-file
"repository"
(str pod-name)
pod-version
(normalize-os (:os/name @os))
(:os/arch @os))))
(defn data-dir
^java.io.File
[{pod-name :pod/name
pod-version :pod/version}]
(io/file @pods-repo-dir
(str pod-name)
pod-version
(normalize-os (:os/name @os))
(:os/arch @os)))
(defn sha256 [file]
(let [buf (byte-array 8192)
digest (java.security.MessageDigest/getInstance "SHA-256")]
(with-open [bis (io/input-stream (io/file file))]
(loop []
(let [count (.read bis buf)]
(when (pos? count)
(.update digest buf 0 count)
(recur)))))
(-> (.encode (java.util.Base64/getEncoder)
(.digest digest))
(String. "UTF-8"))))
(defn resolve [qsym version force?]
(when-not (string? version)
(throw (IllegalArgumentException. "Version must be provided for resolving from pod registry!")))
(when-let [manifest (pod-manifest qsym version force?)]
(let [artifacts (match-artifacts manifest)
cdir (cache-dir manifest)
ddir (data-dir manifest)
execs (mapv (fn [artifact]
(let [url (:artifact/url artifact)
file-name (last (str/split url #"/"))
cache-file (io/file cdir file-name)
executable (io/file ddir (:artifact/executable artifact))]
(when (or force? (not (.exists executable)))
(warn (format "Downloading pod %s (%s)" qsym version))
(download url cache-file false)
(when-let [expected-sha (:artifact/hash artifact)]
(let [sha (sha256 cache-file)]
(when-not (= (str/replace expected-sha #"^sha256:" "")
sha)
(throw (ex-info (str "Wrong SHA-256 for file" (str cache-file))
{:sha sha
:expected-sha expected-sha})))))
(let [filename (.getName cache-file)]
(cond (str/ends-with? filename ".zip")
(unzip {:zip-file cache-file
:destination-dir ddir
:verbose false})
(or (str/ends-with? filename ".tgz")
(str/ends-with? filename ".tar.gz"))
(un-tgz cache-file ddir
false))
(.delete cache-file))
(make-executable ddir [(:artifact/executable artifact)] false)
(warn (format "Successfully installed pod %s (%s)" qsym version))
(io/file ddir (:artifact/executable artifact)))
(io/file ddir (:artifact/executable artifact)))) artifacts)]
{:executable (.getAbsolutePath ^java.io.File (first execs))
:options (:pod/options manifest)})))

View file

@ -6,18 +6,17 @@
(defn- unroot-resource [^String path] (defn- unroot-resource [^String path]
(symbol (.. path (symbol (.. path
(substring 1) (substring 1)
(replace \/ \.) (replace \/ \. )
(replace \_ \-)))) (replace \_ \-))))
(defn- process-namespace [{:keys [:name :vars]}] (defn- process-namespace [{:keys [:name :vars]}]
(binding [*ns* (load-string (format "(ns %s) *ns*" name))] (binding [*ns* (load-string (format "(ns %s) *ns*" name))]
(doseq [[var-sym v] vars] (doseq [[var-sym v] vars]
(when-let [maybe-core (some-> (ns-resolve *ns* var-sym) meta :ns str symbol)]
(when (= 'clojure.core maybe-core)
(ns-unmap *ns* var-sym)))
(cond (cond
(ifn? v) (ifn? v)
(intern name var-sym v) (do
(ns-unmap *ns* var-sym)
(intern name var-sym v))
(string? v) (string? v)
(load-string v))))) (load-string v)))))
@ -34,20 +33,15 @@
(defn load-pod (defn load-pod
([pod-spec] (load-pod pod-spec nil)) ([pod-spec] (load-pod pod-spec nil))
([pod-spec version opts] (load-pod pod-spec (assoc opts :version version))) ([pod-spec _opts]
([pod-spec opts] (let [pod (impl/load-pod
(let [opts (if (string? opts)
{:version opts}
opts)
pod (impl/load-pod
pod-spec pod-spec
(merge {:remove-ns remove-ns {:remove-ns remove-ns
:resolve (fn [sym] :resolve (fn [sym]
(or (resolve sym) (or (resolve sym)
(intern (intern
(create-ns (symbol (namespace sym))) (create-ns (symbol (namespace sym)))
(symbol (name sym)))))} (symbol (name sym)))))})
opts))
namespaces (:namespaces pod)] namespaces (:namespaces pod)]
(swap! namespaces-to-load (swap! namespaces-to-load
merge merge
@ -56,10 +50,9 @@
(when defer? (when defer?
[ns-name pod])) [ns-name pod]))
namespaces))) namespaces)))
(binding [impl/*pod-id* (:pod-id pod)] (doseq [[ns-sym vars lazy?] namespaces
(doseq [[ns-sym vars lazy?] namespaces :when (not lazy?)]
:when (not lazy?)] (process-namespace {:name ns-sym :vars vars}))
(process-namespace {:name ns-sym :vars vars})))
(future (impl/processor pod)) (future (impl/processor pod))
{:pod/id (:pod-id pod)}))) {:pod/id (:pod-id pod)})))
@ -71,18 +64,3 @@
(defn invoke (defn invoke
([pod-id sym args] (invoke pod-id sym args {})) ([pod-id sym args] (invoke pod-id sym args {}))
([pod-id sym args opts] (impl/invoke-public pod-id sym args opts))) ([pod-id sym args opts] (impl/invoke-public pod-id sym args opts)))
(defmacro copy-var [name var]
`(do (def ~name ~var)
(let [m# (meta (var ~var))
doc# (:doc m#)
arglists# (:arglists m#)]
(alter-meta! (var ~name) assoc
:arglists arglists#
:doc doc#))))
#_:clj-kondo/ignore
(do
(copy-var add-transit-read-handler! impl/add-transit-read-handler!)
(copy-var add-transit-write-handler! impl/add-transit-write-handler!)
(copy-var set-default-transit-write-handler! impl/set-default-transit-write-handler!))

View file

@ -1,12 +1,6 @@
(ns babashka.pods.sci (ns babashka.pods.sci
(:require [babashka.pods.impl :as impl] (:require [babashka.pods.impl :as impl]
[sci.core :as sci] [sci.core :as sci]))
[clojure.java.io :as io]
[babashka.pods.impl.resolver :as resolver]
[babashka.fs :as fs])
(:import (java.io PushbackInputStream File)))
(set! *warn-on-reflection* true)
(defn- process-namespace [ctx {:keys [:name :vars]}] (defn- process-namespace [ctx {:keys [:name :vars]}]
(let [env (:env ctx) (let [env (:env ctx)
@ -17,110 +11,62 @@
(swap! env update-in [:namespaces ns-name] (swap! env update-in [:namespaces ns-name]
(fn [ns-map] (fn [ns-map]
(if ns-map ns-map {:obj sci-ns}))) (if ns-map ns-map {:obj sci-ns})))
(doseq [[var-name var-value :as var] vars] (doseq [[var-name var-value] vars]
(cond (ifn? var-value) (cond (ifn? var-value)
(swap! env assoc-in [:namespaces ns-name var-name] (swap! env assoc-in [:namespaces ns-name var-name]
(sci/new-var (sci/new-var
(symbol (str ns-name) (str var-name)) var-value (meta var-name))) (symbol (str ns-name) (str var-name)) var-value))
(string? var-value) (string? var-value)
(sci/eval-string* ctx var-value)))))) (sci/eval-string* ctx var-value))))))
(defn metadata-cache-file ^File [^File bb-edn-file pod-spec {:keys [:version :path]}] (def load-pod
(if version (with-meta
(io/file (resolver/cache-dir {:pod/name pod-spec :pod/version version}) (fn
"metadata.cache") ([ctx pod-spec] (load-pod ctx pod-spec nil))
(let [config-dir (.getParentFile bb-edn-file) ([ctx pod-spec _opts]
cache-dir (io/file config-dir ".babashka") (let [env (:env ctx)
pod-file (-> path io/file .getName) pod (binding [*out* @sci/out
cache-file (io/file cache-dir (str pod-file ".metadata.cache"))] *err* @sci/err]
cache-file))) (impl/load-pod
pod-spec
(defn load-metadata-from-cache [bb-edn-file pod-spec opts] {:remove-ns
(let [cache-file (metadata-cache-file bb-edn-file pod-spec opts)] (fn [sym]
(when (.exists cache-file) (swap! env update :namespaces dissoc sym))
(with-open [r (PushbackInputStream. (io/input-stream cache-file))] :resolve
(impl/read r))))) (fn [sym]
(let [sym-ns (or (some-> (namespace sym)
(defn load-pod-metadata* [bb-edn-file pod-spec {:keys [:version :cache] :as opts}] symbol)
(let [metadata (impl/load-pod-metadata pod-spec opts) 'clojure.core)
cache-file (when (and metadata cache) sym-name (symbol (name sym))]
(metadata-cache-file bb-edn-file pod-spec opts))] (or (get-in @env [:namespaces sym-ns sym-name])
(when cache-file (let [v (sci/new-var sym {:predefined true})]
(io/make-parents cache-file) (swap! env assoc-in [:namespaces sym-ns sym-name]
(when (fs/writable? (fs/parent cache-file)) v)
(with-open [w (io/output-stream cache-file)] v))))}))
(impl/write w metadata)))) namespaces (:namespaces pod)
metadata)) namespaces-to-load (set (keep (fn [[ns-name _ defer?]]
(when defer?
(defn load-pod-metadata ns-name))
([pod-spec opts] (load-pod-metadata nil pod-spec opts)) namespaces))]
([bb-edn-file pod-spec {:keys [:cache] :as opts}] (when (seq namespaces-to-load)
(let [metadata (let [load-fn (fn load-fn [{:keys [:namespace]}]
(if-let [cached-metadata (when cache (when (contains? namespaces-to-load namespace)
(load-metadata-from-cache bb-edn-file (let [ns (impl/load-ns pod namespace)]
pod-spec (process-namespace ctx ns))
opts))] {:file nil
cached-metadata :source ""}))
(load-pod-metadata* bb-edn-file pod-spec opts))] prev-load-fn (:load-fn @env)
(reduce new-load-fn (fn [m]
(fn [pod-namespaces ns] (or (load-fn m)
(let [ns-sym (-> ns (get "name") impl/bytes->string symbol)] (when prev-load-fn
(assoc pod-namespaces ns-sym {:pod-spec pod-spec (prev-load-fn m))))]
:opts (assoc opts :metadata metadata)}))) (swap! env assoc :load-fn new-load-fn)))
{} (get metadata "namespaces"))))) (doseq [[ns-name vars lazy?] namespaces
:when (not lazy?)]
(defn load-pod (process-namespace ctx {:name ns-name :vars vars}))
([ctx pod-spec] (load-pod ctx pod-spec nil)) (sci/future (impl/processor pod))
([ctx pod-spec version opts] (load-pod ctx pod-spec (assoc opts :version version))) {:pod/id (:pod-id pod)})))
([ctx pod-spec opts] {:sci.impl/op :needs-ctx}))
(let [opts (if (string? opts)
{:version opts}
opts)
env (:env ctx)
pod (binding [*out* @sci/out
*err* @sci/err]
(impl/load-pod
pod-spec
(merge
{:remove-ns
(fn [sym]
(swap! env update :namespaces dissoc sym))
:resolve
(fn [sym]
(let [sym-ns (or (some-> (namespace sym)
symbol)
'clojure.core)
sym-name (symbol (name sym))]
(or (get-in @env [:namespaces sym-ns sym-name])
(let [v (sci/new-var sym {:predefined true})]
(swap! env assoc-in [:namespaces sym-ns sym-name]
v)
v))))}
opts)))
namespaces (:namespaces pod)
namespaces-to-load (set (keep (fn [[ns-name _ defer?]]
(when defer?
ns-name))
namespaces))]
(when (seq namespaces-to-load)
(let [load-fn (fn load-fn [{:keys [:namespace]}]
(when (contains? namespaces-to-load namespace)
(let [ns (impl/load-ns pod namespace)]
(process-namespace ctx ns))
{:file nil
:source ""}))
prev-load-fn (:load-fn @env)
new-load-fn (fn [m]
(or (load-fn m)
(when prev-load-fn
(prev-load-fn m))))]
(swap! env assoc :load-fn new-load-fn)))
(binding [impl/*pod-id* (:pod-id pod)]
(doseq [[ns-name vars lazy?] namespaces
:when (not lazy?)]
(process-namespace ctx {:name ns-name :vars vars})))
(sci/future (impl/processor pod))
{:pod/id (:pod-id pod)})))
(defn unload-pod (defn unload-pod
([pod-id] (unload-pod pod-id {})) ([pod-id] (unload-pod pod-id {}))
@ -130,18 +76,3 @@
(defn invoke (defn invoke
([pod-id sym args] (invoke pod-id sym args {})) ([pod-id sym args] (invoke pod-id sym args {}))
([pod-id sym args opts] (impl/invoke-public pod-id sym args opts))) ([pod-id sym args opts] (impl/invoke-public pod-id sym args opts)))
(defmacro copy-var [name var]
`(do (def ~name ~var)
(let [m# (meta (var ~var))
doc# (:doc m#)
arglists# (:arglists m#)]
(alter-meta! (var ~name) assoc
:arglists arglists#
:doc doc#))))
#_:clj-kondo/ignore
(do
(copy-var add-transit-read-handler! impl/add-transit-read-handler!)
(copy-var add-transit-write-handler! impl/add-transit-write-handler!)
(copy-var set-default-transit-write-handler! impl/set-default-transit-write-handler!))

View file

@ -3,10 +3,8 @@
(:require [bencode.core :as bencode] (:require [bencode.core :as bencode]
[cheshire.core :as cheshire] [cheshire.core :as cheshire]
[clojure.edn :as edn] [clojure.edn :as edn]
[clojure.java.io :as io] [clojure.java.io :as io])
[cognitect.transit :as transit]) (:import [java.io PushbackInputStream])
(:import [java.io PushbackInputStream]
[java.net ServerSocket])
(:gen-class)) (:gen-class))
(def debug? false) (def debug? false)
@ -16,15 +14,17 @@
(binding [*out* (io/writer "/tmp/log.txt" :append true)] (binding [*out* (io/writer "/tmp/log.txt" :append true)]
(apply println args)))) (apply println args))))
(defn write [stream v] (def stdin (PushbackInputStream. System/in))
(bencode/write-bencode stream v)
(flush)) (defn write [v]
(bencode/write-bencode System/out v)
(.flush System/out))
(defn read-string [^"[B" v] (defn read-string [^"[B" v]
(String. v)) (String. v))
(defn read [stream] (defn read []
(bencode/read-bencode stream)) (bencode/read-bencode stdin))
(def dependents (def dependents
(for [i (range 10)] (for [i (range 10)]
@ -34,70 +34,19 @@
(format "(def x%s (inc x%s))" i (dec i)) (format "(def x%s (inc x%s))" i (dec i))
"(def x0 0)")})) "(def x0 0)")}))
(defn transit-json-read [^String s]
(with-open [bais (java.io.ByteArrayInputStream. (.getBytes s "UTF-8"))]
(let [r (transit/reader bais :json {:handlers
{"local-date-time"
(transit/read-handler
(fn [s]
(java.time.LocalDateTime/parse s)))
"java.array"
(transit/read-handler
(fn [v]
(into-array v)))}})]
(transit/read r))))
(defn transit-json-write [s]
(with-open [baos (java.io.ByteArrayOutputStream. 4096)]
(let [w (transit/writer baos :json {:handlers
{java.time.LocalDateTime
(transit/write-handler
"local-date-time"
str)}
:default-handler
(transit/write-handler
(fn [x] (when (.isArray (class x)) "java.array"))
vec)})]
(transit/write w s)
(str baos))))
(defn transit-json-write-meta [s]
(with-open [baos (java.io.ByteArrayOutputStream. 4096)]
(let [w (transit/writer baos :json {:transform transit/write-meta})]
(transit/write w s)
(str baos))))
(defn run-pod [cli-args] (defn run-pod [cli-args]
(let [format (cond (contains? cli-args "--json") :json (let [format (if (contains? cli-args "--json")
(contains? cli-args "--transit+json") :transit+json :json
:else :edn) :edn)
write-fn (case format write-fn (if (identical? :json format)
:edn pr-str cheshire/generate-string
:json cheshire/generate-string pr-str)
:transit+json transit-json-write) read-fn (if (identical? :json format)
read-fn (case format #(cheshire/parse-string % true)
:edn edn/read-string edn/read-string)]
:json #(cheshire/parse-string % true)
:transit+json transit-json-read)
socket (= "socket" (System/getenv "BABASHKA_POD_TRANSPORT"))
[in out] (if socket
(let [server (ServerSocket. 0)
port (.getLocalPort server)
pid (.pid (java.lang.ProcessHandle/current))
port-file (io/file (str ".babashka-pod-" pid ".port"))
_ (.addShutdownHook (Runtime/getRuntime)
(Thread. (fn [] (.delete port-file))))
_ (spit port-file
(str port "\n"))
socket (.accept server)
in (PushbackInputStream. (.getInputStream socket))
out (.getOutputStream socket)]
[in out])
[(PushbackInputStream. System/in)
System/out])]
(try (try
(loop [] (loop []
(let [message (try (read in) (let [message (try (read)
(catch java.io.EOFException _ (catch java.io.EOFException _
::EOF))] ::EOF))]
(when-not (identical? ::EOF message) (when-not (identical? ::EOF message)
@ -106,72 +55,42 @@
op (keyword op)] op (keyword op)]
(case op (case op
:describe :describe
(do (write out {"format" (case format (do (write {"format" (if (= format :json)
:edn "edn" "json"
:json "json" "edn")
:transit+json "transit+json") "readers" {"my/tag" "identity"
"readers" {"my/tag" "identity" ;; NOTE: this function is defined later,
;; NOTE: this function is defined later, ;; which should be supported
;; which should be supported "my/other-tag" "pod.test-pod/read-other-tag"}
"my/other-tag" "pod.test-pod/read-other-tag"} "namespaces"
"namespaces" [{"name" "pod.test-pod"
[{"name" "pod.test-pod" "vars" (into [{"name" "add-sync"}
"vars" (into [{"name" "add-sync" {"name" "range-stream"
"meta" "{:doc \"add the arguments\"}"} "async" "true"}
{"name" "range-stream" {"name" "assoc"}
"async" "true"} {"name" "error"}
{"name" "assoc"} {"name" "print"}
{"name" "error"} {"name" "print-err"}
{"name" "print"} {"name" "return-nil"}
{"name" "print-err"} {"name" "do-twice"
{"name" "return-nil"} "code" "(defmacro do-twice [x] `(do ~x ~x))"}
{"name" "do-twice" {"name" "fn-call"
"code" "(defmacro do-twice [x] `(do ~x ~x))"} "code" "(defn fn-call [f x] (f x))"}
{"name" "fn-call" {"name" "reader-tag"}
"code" "(defn fn-call [f x] (f x))"} ;; returns thing with other tag
{"name" "reader-tag"} {"name" "other-tag"}
;; returns thing with other tag ;; reads thing with other tag
{"name" "other-tag"} {"name" "read-other-tag"
;; reads thing with other tag "code" "(defn read-other-tag [x] [x x])"}]
{"name" "read-other-tag" dependents)}
"code" "(defn read-other-tag [x] [x x])" {"name" "pod.test-pod.loaded"
"meta" "{:doc \"unread\"}"} "defer" "true"}
{"name" "round-trip-meta" {"name" "pod.test-pod.loaded2"
"arg-meta" "true"} "defer" "true"}
{"name" "dont-round-trip-meta" {"name" "pod.test-pod.only-code"
"arg-meta" "false"} "vars" [{"name" "foo"
{"name" "-local-date-time"} "code" "(defn foo [] 1)"}]}]
{"name" "transit-stuff" "ops" {"shutdown" {}}})
"code" "
(babashka.pods/add-transit-read-handler! \"local-date-time\"
(fn [s] (java.time.LocalDateTime/parse s)))
(babashka.pods/add-transit-write-handler! #{java.time.LocalDateTime}
\"local-date-time\"
str )
(defn local-date-time [x]
(-local-date-time x))
;; serialize Java arrays as vectors with tag java.array
(babashka.pods/set-default-transit-write-handler!
(fn [x] (when (.isArray (class x)) \"java.array\"))
vec)
(babashka.pods/add-transit-read-handler! \"java.array\"
into-array)
"}
{"name" "incorrect-edn"}]
dependents)}
{"name" "pod.test-pod.loaded"
"defer" "true"}
{"name" "pod.test-pod.loaded2"
"defer" "true"}
{"name" "pod.test-pod.only-code"
"vars" [{"name" "foo"
"code" "(defn foo [] 1)"}]}]
"ops" {"shutdown" {}}})
(recur)) (recur))
:invoke (let [var (-> (get message "var") :invoke (let [var (-> (get message "var")
read-string read-string
@ -185,92 +104,66 @@
(case var (case var
pod.test-pod/add-sync pod.test-pod/add-sync
(try (let [ret (apply + args)] (try (let [ret (apply + args)]
(write out (write
{"value" (write-fn ret) {"value" (write-fn ret)
"id" id "id" id
"status" ["done"]})) "status" ["done"]}))
(catch Exception e (catch Exception e
(write out (write
{"ex-data" (write-fn {:args args}) {"ex-data" (write-fn {:args args})
"ex-message" (.getMessage e) "ex-message" (.getMessage e)
"status" ["done" "error"] "status" ["done" "error"]
"id" id}))) "id" id})))
pod.test-pod/range-stream pod.test-pod/range-stream
(let [rng (apply range args)] (let [rng (apply range args)]
(doseq [v rng] (doseq [v rng]
(write out (write
{"value" (write-fn v) {"value" (write-fn v)
"id" id}) "id" id})
(Thread/sleep 100)) (Thread/sleep 100))
(write out (write
{"status" ["done"] {"status" ["done"]
"id" id})) "id" id}))
pod.test-pod/assoc pod.test-pod/assoc
(write out (write
{"value" (write-fn (apply assoc args)) {"value" (write-fn (apply assoc args))
"status" ["done"] "status" ["done"]
"id" id}) "id" id})
pod.test-pod/error pod.test-pod/error
(write out (write
{"ex-data" (write-fn {:args args}) {"ex-data" (write-fn {:args args})
"ex-message" (str "Illegal arguments") "ex-message" (str "Illegal arguments")
"status" ["done" "error"] "status" ["done" "error"]
"id" id}) "id" id})
pod.test-pod/print pod.test-pod/print
(do (write out (do (write
{"out" (with-out-str (prn args)) {"out" (pr-str args)
"id" id}) "id" id})
(write out (write
{"status" ["done"] {"status" ["done"]
"id" id})) "id" id}))
pod.test-pod/print-err pod.test-pod/print-err
(do (write out (do (write
{"err" (with-out-str (prn args)) {"err" (pr-str args)
"id" id}) "id" id})
(write out (write
{"status" ["done"] {"status" ["done"]
"id" id})) "id" id}))
pod.test-pod/return-nil pod.test-pod/return-nil
(write out (write
{"status" ["done"] {"status" ["done"]
"id" id "id" id
"value" (write-fn nil)}) "value" "nil"})
pod.test-pod/reader-tag pod.test-pod/reader-tag
(write out (write
{"status" ["done"] {"status" ["done"]
"id" id "id" id
"value" "#my/tag[1 2 3]"}) "value" "#my/tag[1 2 3]"})
pod.test-pod/other-tag pod.test-pod/other-tag
(write out (write
{"status" ["done"] {"status" ["done"]
"id" id "id" id
"value" "#my/other-tag[1]"}) "value" "#my/other-tag[1]"}))
pod.test-pod/round-trip-meta
(write out
{"status" ["done"]
"id" id
"value"
(case format
:transit+json (transit-json-write-meta (first args))
(write-fn (first args)))})
pod.test-pod/dont-round-trip-meta
(write out
{"status" ["done"]
"id" id
"value"
(case format
:transit+json (transit-json-write-meta (first args))
(write-fn (first args)))})
pod.test-pod/-local-date-time
(write out
{"status" ["done"]
"id" id
"value" (write-fn (first args))})
pod.test-pod/incorrect-edn
(write out
{"status" ["done"]
"id" id
"value" (write-fn {(keyword "foo bar") 1})}))
(recur)) (recur))
:shutdown (System/exit 0) :shutdown (System/exit 0)
:load-ns (let [ns (-> (get message "ns") :load-ns (let [ns (-> (get message "ns")
@ -280,28 +173,26 @@
read-string)] read-string)]
(case ns (case ns
pod.test-pod.loaded pod.test-pod.loaded
(write out (write
{"status" ["done"] {"status" ["done"]
"id" id "id" id
"name" "pod.test-pod.loaded" "name" "pod.test-pod.loaded"
"vars" [{"name" "loaded" "vars" [{"name" "loaded"
"code" "(defn loaded [x] (inc x))"}]}) "code" "(defn loaded [x] (inc x))"}]})
pod.test-pod.loaded2 pod.test-pod.loaded2
(write out (write
{"status" ["done"] {"status" ["done"]
"id" id "id" id
"name" "pod.test-pod.loaded2" "name" "pod.test-pod.loaded2"
"vars" [{"name" "x" "vars" [{"name" "x"
"code" "(require '[pod.test-pod.loaded :as loaded])"} "code" "(require '[pod.test-pod.loaded :as loaded])"}
{"name" "loaded" {"name" "loaded"
"code" "(defn loaded [x] (loaded/loaded x))"}]})) "code" "(defn loaded [x] (loaded/loaded x))"}]}))
(recur))))))) (recur)))))))
(catch Exception e (catch Exception e
(binding [*out* *err*] (binding [*out* *err*]
(prn e)))))) (prn e))))))
(defn -main [& args] (defn -main [& args]
#_(binding [*out* *err*]
(prn :args args))
(when (= "true" (System/getenv "BABASHKA_POD")) (when (= "true" (System/getenv "BABASHKA_POD"))
(run-pod (set args)))) (run-pod (set args))))

View file

@ -1,11 +0,0 @@
(require '[babashka.pods :as pods])
(pods/load-pod 'org.babashka/buddy "0.1.0")
(require '[pod.babashka.buddy.codecs :as codecs]
'[pod.babashka.buddy.hash :as hash])
(println (-> (hash/sha256 "foobar")
(codecs/bytes->hex)))
(pods/load-pod 'org.babashka/etaoin) ;; should cause error when version & path are missing

View file

@ -1,19 +1,5 @@
(require '[babashka.pods :as pods]) (require '[babashka.pods :as pods])
(def pod-id (:pod/id (pods/load-pod ["clojure" "-A:test-pod"])))
(def fmt (or (System/getenv "BABASHKA_POD_TEST_FORMAT")
"edn"))
(def socket (System/getenv "BABASHKA_POD_TEST_SOCKET"))
(def cmd (cond-> ["clojure" "-M:test-pod"]
(= "json" fmt) (conj "--json")
(= "transit+json" fmt) (conj "--transit+json")))
;; (.println System/err cmd)
(def pod-id (:pod/id (pods/load-pod cmd
{:socket (boolean socket)})))
(require '[pod.test-pod :as pod]) (require '[pod.test-pod :as pod])
(def pod-ns-name (ns-name (find-ns 'pod.test-pod))) (def pod-ns-name (ns-name (find-ns 'pod.test-pod)))
@ -55,60 +41,19 @@
(def add-result (pod.test-pod/add-sync 1 2 3)) (def add-result (pod.test-pod/add-sync 1 2 3))
(def nil-result (pod.test-pod/return-nil)) (def nil-result (pod.test-pod/return-nil))
(def add-sync-meta (:doc (meta #'pod.test-pod/add-sync)))
(def error-meta (:doc (meta #'pod.test-pod/error)))
(def read-other-tag-meta (:doc (meta #'pod.test-pod/read-other-tag)))
(def x9 pod.test-pod/x9) (def x9 pod.test-pod/x9)
(def tagged (if (= "edn" fmt) (def tagged (pod/reader-tag))
(pod/reader-tag) (def other-tagged (pod/other-tag))
[1 2 3]))
(def other-tagged
(if (= "edn" fmt)
(pod/other-tag)
[[1] [1]]))
(def fn-called (pod.test-pod/fn-call inc 2)) (def fn-called (pod.test-pod/fn-call inc 2))
;; (.println System/err (str :fmt " " fmt))
(def local-date-time
(if (= "transit+json" fmt)
(instance? java.time.LocalDateTime (pod.test-pod/local-date-time (java.time.LocalDateTime/now)))
true))
(def assoc-string-array
(if (= "transit+json" fmt)
(let [v (:a (pod.test-pod/assoc {} :a (into-array String ["foo"])))]
(.isArray (class v)))
true))
(def round-trip-meta
(if (= "transit+json" fmt)
(= {:my-meta 2} (meta (pod.test-pod/round-trip-meta (with-meta [2] {:my-meta 2}))))
true))
(def round-trip-meta-nested
(if (= "transit+json" fmt)
(= {:my-meta 3} (meta (first (pod.test-pod/round-trip-meta [(with-meta [3] {:my-meta 3})]))))
true))
(def dont-round-trip-meta
(if (= "transit+json" fmt)
(= nil (meta (pod.test-pod/dont-round-trip-meta (with-meta [2] {:my-meta 2}))))
true))
(require '[pod.test-pod.only-code :as only-code]) (require '[pod.test-pod.only-code :as only-code])
(def should-be-1 (only-code/foo)) (def should-be-1 (only-code/foo))
(require '[pod.test-pod.loaded2 :as loaded2]) (require '[pod.test-pod.loaded2 :as loaded2])
(def loaded (loaded2/loaded 1)) (def loaded (loaded2/loaded 1))
(def incorrect-edn-response
(try (pod.test-pod/incorrect-edn)
(catch Exception e (ex-message e))))
(pods/unload-pod pod-id) (pods/unload-pod pod-id)
(def successfully-removed (nil? (find-ns 'pod.test-pod))) (def successfully-removed (nil? (find-ns 'pod.test-pod)))
@ -129,13 +74,4 @@
other-tagged other-tagged
loaded loaded
fn-called fn-called
local-date-time should-be-1]
assoc-string-array
round-trip-meta
round-trip-meta-nested
dont-round-trip-meta
should-be-1
add-sync-meta
error-meta
read-other-tag-meta
incorrect-edn-response]

View file

@ -1,11 +0,0 @@
(ns babashka.pods.impl-test
(:require [clojure.test :refer :all]
[babashka.pods.impl :refer :all]))
(deftest load-pod-test
(testing "resolve fn gets called when pod has EDN data readers"
(let [resolved? (atom false)
test-resolve (fn [_sym]
(reset! resolved? true))]
(load-pod ["clojure" "-M:test-pod"] {:resolve test-resolve})
(is @resolved?))))

View file

@ -1,8 +1,6 @@
(ns babashka.pods.jvm-test (ns babashka.pods.jvm-test
(:require [babashka.pods.test-common :refer [test-program assertions (:require [babashka.pods.test-common :refer [test-program assertions]]
pod-registry]] [clojure.test :refer [deftest]]))
[clojure.string :as str]
[clojure.test :refer [deftest is]]))
(deftest jvm-test (deftest jvm-test
(let [out (java.io.StringWriter.) (let [out (java.io.StringWriter.)
@ -14,15 +12,3 @@
(catch Exception e (prn e))))] (catch Exception e (prn e))))]
(assertions out err ret))) (assertions out err ret)))
(deftest pod-registry-test
(let [out (java.io.StringWriter.)
err (java.io.StringWriter.)
ex (binding [*out* out
*err* err]
(try (load-string
pod-registry)
(catch Exception e
e)))]
(is (str/includes? (str out) "c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2"))
(is (str/includes? (pr-str ex) "Version or path must be provided"))))

View file

@ -1,41 +1,18 @@
(ns babashka.pods.sci-test (ns babashka.pods.sci-test
(:require [babashka.pods.sci :as pods] (:require [babashka.pods.sci :as pods]
[babashka.pods.test-common :refer [test-program assertions pod-registry]] [babashka.pods.test-common :refer [test-program assertions]]
[clojure.string :as str] [clojure.test :refer [deftest]]
[clojure.test :refer [deftest is]]
[sci.core :as sci])) [sci.core :as sci]))
(deftest sci-test (deftest sci-test
(let [out (java.io.StringWriter.) (let [out (java.io.StringWriter.)
err (java.io.StringWriter.) err (java.io.StringWriter.)
ctx-ref (volatile! nil)
ctx (sci/init {:namespaces {'babashka.pods
{'load-pod (fn [& args]
(apply pods/load-pod @ctx-ref args))
'invoke pods/invoke
'unload-pod pods/unload-pod
'add-transit-read-handler! pods/add-transit-read-handler!
'add-transit-write-handler! pods/add-transit-write-handler!
'set-default-transit-write-handler! pods/set-default-transit-write-handler!}}
:classes {'System System
'java.time.LocalDateTime java.time.LocalDateTime
'java.lang.Class Class}})
_ (vreset! ctx-ref ctx)
ret (sci/binding [sci/out out ret (sci/binding [sci/out out
sci/err err] sci/err err]
(binding [*out* out (sci/eval-string
*err* err] test-program
(sci/eval-string* ctx test-program)))] {:namespaces {'babashka.pods
{'load-pod pods/load-pod
'invoke pods/invoke
'unload-pod pods/unload-pod}}}))]
(assertions out err ret))) (assertions out err ret)))
(deftest pod-registry-test
(let [out (java.io.StringWriter.)
err (java.io.StringWriter.)
ex (binding [*out* out
*err* err]
(try (load-string
pod-registry)
(catch Exception e
e)))]
(is (str/includes? (str out) "c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2"))
(is (str/includes? (pr-str ex) "Version or path must be provided"))))

View file

@ -1,54 +1,32 @@
(ns babashka.pods.test-common (ns babashka.pods.test-common
(:require [clojure.java.io :as io] (:require [clojure.java.io :as io]
[clojure.string :as str]
[clojure.test :refer [is]])) [clojure.test :refer [is]]))
(def test-program (slurp (io/file "test-resources" "test_program.clj"))) (def test-program (slurp (io/file "test-resources" "test_program.clj")))
(defn assertions [out err ret] (defn assertions [out err ret]
;; (.println System/err ret)
;; (.println System/err out)
;; (.println System/err err)
(doseq [[expected actual] (doseq [[expected actual]
(map vector (replace (map vector '["pod.test-pod"
{::edn-error (if (= "edn" pod.test-pod
(System/getenv "BABASHKA_POD_TEST_FORMAT")) {:a 1, :b 2}
"Map literal must contain an even number of forms" 6
::dont-care)} 3
'["pod.test-pod" [1 2 3 4 5 6 7 8 9]
pod.test-pod "Illegal arguments / {:args (1 2 3)}"
{:a 1, :b 2} nil
6 3
3 #"cast"
[1 2 3 4 5 6 7 8 9] {:args ["1" 2]}
#"Illegal arguments / \{:args [\(\[]1 2 3[\)\]]\}" true
nil 9
3 [1 2 3]
#"cast" [[1] [1]]
{:args ["1" 2]} 2
true 3
9 1]
[1 2 3]
[[1] [1]]
2
3
true ;; local-date
true ;; roundtrip string array
true ;; roundtrip metadata
true ;; roundtrip metadata nested
true ;; dont roundtrip metadata (when arg-meta "false"/ absent)
1
"add the arguments"
nil
nil
::edn-error])
(concat ret (repeat ::nil)))] (concat ret (repeat ::nil)))]
(cond (instance? java.util.regex.Pattern expected) (if (instance? java.util.regex.Pattern expected)
(is (re-find expected actual)) (is (re-find expected actual))
(= ::dont-care expected) nil (is (= expected actual))))
:else
(is (= expected actual))))
(is (= "(\"hello\" \"print\" \"this\" \"debugging\" \"message\")\n:foo\n:foo\n" (str out))) (is (= "(\"hello\" \"print\" \"this\" \"debugging\" \"message\")\n:foo\n:foo\n" (str out)))
(is (str/starts-with? (str err) "(\"hello\" \"print\" \"this\" \"error\")" ))) (is (= "(\"hello\" \"print\" \"this\" \"error\")\n" (str err))))
(def pod-registry (slurp (io/file "test-resources" "pod_registry.clj")))