Compare commits

...

47 commits

Author SHA1 Message Date
Ingy döt Net
47e55fe5e7
Fix typo (#71) 2024-05-08 14:17:43 +02:00
Michiel Borkent
717cef7af5 ignore 2024-02-27 11:52:37 +01:00
Jo Geraerts
cd968459a7
correct small typo in README.md (#69) 2024-01-26 13:26:09 +01:00
Flávio Sousa
8b717eb001
Update README.md with BABASHKA_PODS_DIR (#68) 2023-06-05 11:13:35 +02:00
Michiel Borkent
6ad6045b94 minor 2023-05-12 16:42:25 +02:00
Jude Payne
b00133ca05
Fix #66: opt-in metadata (#67) 2023-05-12 16:09:40 +02:00
Michiel Borkent
64ecb94de8 changelog 2023-04-02 11:27:07 +02:00
Michiel Borkent
d29cf6aa65 Changelog 2023-04-02 11:26:24 +02:00
Michiel Borkent
1635931483 Fix #65, fix warnings when defining var with core name 2023-04-02 11:25:51 +02:00
Michiel Borkent
75c2216649 Changelog 2023-01-09 21:21:45 +01:00
Michiel Borkent
c2e3d8f8b8 Fix #59: delete port file on exit 2023-01-09 21:21:34 +01:00
Michiel Borkent
16bea5b7db Undo verbose 2023-01-09 21:19:04 +01:00
Michiel Borkent
85c554e643 Fix #63: create directory before un-tarring 2023-01-09 21:16:53 +01:00
Michiel Borkent
4fb0da7daf changelog script 2022-12-29 17:10:04 +01:00
Michiel Borkent
76313a7089 changelog script 2022-12-29 17:07:51 +01:00
Michiel Borkent
28cf3de4ef v0.2.0 2022-12-29 17:06:37 +01:00
Michiel Borkent
81f8845d72
Fix #60 and fix #61, see changelog (#62) 2022-12-29 17:05:23 +01:00
Nate Jones
decf791000
Switch "out" and "err" messages to print and flush instead of println (#57) 2022-09-29 09:27:03 +02:00
Crispin Wellington
53f79da09d
set TCP_NODELAY on transport socket (#55) 2022-09-12 12:43:15 +02:00
Michiel Borkent
8bc0852799
Revert "Revert "Allow env vars OS_NAME & OS_ARCH to override os props (#53)"" (#54)
This reverts commit 6cbbdd118d.
2022-09-07 21:23:32 +02:00
Michiel Borkent
6cbbdd118d Revert "Allow env vars OS_NAME & OS_ARCH to override os props (#53)"
This reverts commit 5754d433aa.
2022-09-07 20:02:27 +02:00
Wes Morgan
5754d433aa
Allow env vars OS_NAME & OS_ARCH to override os props (#53) 2022-09-07 19:47:38 +02:00
Randy Stauner
6df443fabf
Correct spelling of async invocation keys (#51) 2022-07-16 21:00:42 +02:00
Michiel Borkent
93081b75e6
Manifest dir with env var (#50) 2022-07-04 19:57:35 +02:00
Michiel Borkent
66867eee7f CHANGELOG 2022-05-30 10:11:03 +02:00
Michiel Borkent
eb0b01c0a6 fix #49: don't log socket closed exception 2022-05-30 10:10:12 +02:00
Wes Morgan
b9f3a28555
Add babashka/fs to project.clj and deps.edn (#48)
Now that we're using it in the pods.impl ns
2022-05-14 19:28:07 +02:00
Wes Morgan
aee5337cef
Check that cache dir is writable before writing (#47)
...pod metadata cache file
2022-05-14 18:44:00 +02:00
Michiel Borkent
c2d6ba847c pods env var 2022-05-06 14:53:31 +02:00
Michiel Borkent
e075b13bfe
Add fallback for Apple M1 (#46) 2022-03-23 19:32:47 +01:00
Wes Morgan
842ff34739
Fix tests, remove accidental babashka code dependency, & add CI config (#45)
* Update clojure CLI flags in script/test

* Throw error when not one of version or path

...with qualified symbol pod-spec

* Fix some minor formatting issues

* Check for new error message in pod-registry test

* Add a test for resolve fn in edn data readers

* Add CI config

* Try using clojure tools-deps image in CI

* Check for new error message in sci pod-registry test

* Use latest version of buddy in pod-registry test

...for more platform support

* Stop depending on babashka

Instead accept an arg for the thing we were getting from it (location of the bb.edn file)

* Save maven deps in cache in CI
2022-03-23 16:57:56 +01:00
Wes Morgan
f2cfdff899
Feature: declarative pods (#44)
* Use non-deprecated string->int method

* Remove unused next-pod-id

* Support declarative pods loaded on require

* Wait for pod shutdown in load-pod-metadata

* Type hint a File return value to avoid reflection

* Return pod metadata instead of putting in ctx

* Fix local pod loading & support :cache opt

* Document :pods in bb.edn

* Cache local pods metadata in project .babashka dir

* Pass pod resolve-fn to describe->metadata

Not only was this just a bug, but the accidental reference to clojure.core/resolve ballooned the final bb image size to >110MB!
2022-03-23 12:26:58 +01:00
Michiel Borkent
5fbf1d7b04 Revert "Feature: declarative pods loaded at require time (#43)"
This reverts commit 8f059da7e6.
2022-03-22 14:00:24 +01:00
Wes Morgan
8f059da7e6
Feature: declarative pods loaded at require time (#43)
* Use non-deprecated string->int method

* Remove unused next-pod-id

* Support declarative pods loaded on require

* Wait for pod shutdown in load-pod-metadata

* Type hint a File return value to avoid reflection

* Return pod metadata instead of putting in ctx

* Fix local pod loading & support :cache opt

* Document :pods in bb.edn

* Cache local pods metadata in project .babashka dir
2022-03-21 20:46:57 +01:00
Michiel Borkent
538fc6f414 v0.1.0 2021-12-19 20:25:20 +01:00
Paula Gearon
f360afa613
Readme update for metadata (#41) 2021-10-15 19:42:50 +02:00
Paula Gearon
973c4e5c9e
[#38] Support metadata on vars 2021-10-15 17:43:12 +02:00
Paula Gearon
401aeecf52
Fixed example (#39)
Fixed minor error in example
2021-10-13 13:15:05 +02:00
Michiel Borkent
de4c3610c9 minor 2021-05-19 23:48:14 +02:00
Michiel Borkent
0f31e57977 Rename 2021-05-19 21:31:51 +02:00
Michiel Borkent
37326045aa Add support for default transit handler #36 2021-05-19 21:19:18 +02:00
Michiel Borkent
6214f06146 Implicit pod-id 2021-05-19 20:34:38 +02:00
Michiel Borkent
05ecf97127 Scope transit read and write handlers to pod ids 2021-05-19 17:20:55 +02:00
Michiel Borkent
1fdd8231bd
[#33] Allow pods to register transit handlers for reading and writing 2021-05-17 11:41:51 +02:00
Huahai Yang
82aa362710
correct clojars badge url (#32) 2021-03-19 18:43:16 +01:00
Michiel Borkent
490073f93a Add clojars badge 2021-03-17 19:55:39 +01:00
Michiel Borkent
39241ebc82 deps 2021-03-17 19:53:42 +01:00
21 changed files with 870 additions and 310 deletions

28
.circleci/config.yml Normal file
View file

@ -0,0 +1,28 @@
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,3 +3,7 @@
.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

21
CHANGELOG.md Normal file
View file

@ -0,0 +1,21 @@
# 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

104
README.md
View file

@ -1,4 +1,6 @@
# 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.
@ -72,7 +74,44 @@ On the JVM:
### Where does the pod come from? ### Where does the pod come from?
When calling `load-pod` with a string or vector of strings, 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). 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
@ -91,7 +130,7 @@ light weight replacement for native interop (JNI, JNA, etc.).
### Examples ### Examples
Beyond the already available pods mentioned above, eductional examples of pods Beyond the already available pods mentioned above, educational examples of pods
can be found [here](examples): can be found [here](examples):
- [pod-lispyclouds-sqlite](examples/pod-lispyclouds-sqlite): a pod that - [pod-lispyclouds-sqlite](examples/pod-lispyclouds-sqlite): a pod that
@ -189,7 +228,7 @@ JSON. It also declares that the pod exposes one namespace,
To encode payloads in EDN use `"edn"` and for Transit JSON use `"transit+json"`. 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.
@ -335,6 +374,55 @@ 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
@ -409,7 +497,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 `:handler` containing callback functions: `:success`, `:error` and `:done` - an opts map containing `:handlers` 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
@ -435,9 +523,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 `:on-success` it pulls out received `pod.babashka.filewatcher/watch*`. In `: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 `:on-error` to `*err*`. errors received from the pod library in `: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:
@ -454,7 +542,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 Normal file
View file

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

View file

@ -1,6 +1,7 @@
{:deps {nrepl/bencode {:mvn/version "1.1.0"} {:deps {nrepl/bencode {:mvn/version "1.1.0"}
cheshire/cheshire {:mvn/version "5.10.0"} cheshire/cheshire {:mvn/version "5.10.0"}
com.cognitect/transit-clj {:mvn/version "1.0.324"}} com.cognitect/transit-clj {:mvn/version "1.0.324"}
babashka/fs {:mvn/version "0.1.6"}}
:aliases :aliases
{:sci {:sci
{:extra-deps {:extra-deps
@ -18,5 +19,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
{:paths ["src" "test-pod"] {:extra-paths ["src" "test-pod"]
:main-opts ["-m" "pod.test-pod"]}}} :main-opts ["-m" "pod.test-pod"]}}}

View file

@ -1,15 +1,17 @@
(defproject babashka/babashka.pods "0.0.1" (defproject babashka/babashka.pods "0.2.0"
: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.2-alpha1"] :dependencies [[org.clojure/clojure "1.10.3"]
[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/babashka_nrepl_clojars_user :username :env/clojars_user
:password :env/babashka_nrepl_clojars_pass :password :env/clojars_pass
:sign-releases false}]] :sign-releases false}]]
:profiles {:test {:dependencies [[borkdude/sci "0.0.13-alpha.27"]]}}) :profiles {:test {:dependencies [[borkdude/sci "0.2.4"]]}})

17
script/changelog.clj Executable file
View file

@ -0,0 +1,17 @@
#!/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,29 +1,32 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -eou pipefail
export BABASHKA_POD_TEST_FORMAT export BABASHKA_POD_TEST_FORMAT
export BABASHKA_POD_TEST_SOCKET export BABASHKA_POD_TEST_SOCKET
# format = edn # format = edn
BABASHKA_POD_TEST_FORMAT=edn BABASHKA_POD_TEST_FORMAT=edn
echo "Testing edn" echo "Testing edn"
clojure -A:test -n babashka.pods.jvm-test clojure -M:test -n babashka.pods.jvm-test
clojure -A:sci:test -n babashka.pods.sci-test clojure -M:sci:test -n babashka.pods.sci-test
clojure -M:test -n babashka.pods.impl-test
# format = json # format = json
BABASHKA_POD_TEST_FORMAT=json BABASHKA_POD_TEST_FORMAT=json
echo "Testing json" echo "Testing json"
clojure -A:test -n babashka.pods.jvm-test clojure -M:test -n babashka.pods.jvm-test
clojure -A:sci:test -n babashka.pods.sci-test clojure -M:sci:test -n babashka.pods.sci-test
# format = json # format = json
BABASHKA_POD_TEST_FORMAT="transit+json" BABASHKA_POD_TEST_FORMAT="transit+json"
echo "Testing transit" echo "Testing transit"
clojure -A:test -n babashka.pods.jvm-test clojure -M:test -n babashka.pods.jvm-test
clojure -A:sci:test -n babashka.pods.sci-test clojure -M:sci:test -n babashka.pods.sci-test
# socket = true # socket = true
unset BABASHKA_POD_TEST_FORMAT unset BABASHKA_POD_TEST_FORMAT
BABASHKA_POD_TEST_SOCKET=true BABASHKA_POD_TEST_SOCKET=true
echo "Testing socket" echo "Testing socket"
clojure -A:test -n babashka.pods.jvm-test clojure -M:test -n babashka.pods.jvm-test
clojure -A:sci:test -n babashka.pods.sci-test clojure -M:sci:test -n babashka.pods.sci-test

View file

@ -14,3 +14,18 @@
(defn invoke (defn invoke
([pod-id-or-pod sym args] (invoke pod-id-or-pod sym args {})) ([pod-id-or-pod sym args] (invoke pod-id-or-pod sym args {}))
([pod-id-or-pod sym args opts] (jvm/invoke pod-id-or-pod sym args opts))) ([pod-id-or-pod sym args opts] (jvm/invoke pod-id-or-pod 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

@ -28,6 +28,9 @@
(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))
@ -36,17 +39,63 @@
(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)))
(defn transit-json-read [^String s] (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"))] (with-open [bais (java.io.ByteArrayInputStream. (.getBytes s "UTF-8"))]
(let [r (transit/reader bais :json)] (let [r (transit/reader bais :json {:handlers (get @transit-read-handler-maps pod-id)})]
(transit/read r)))) (transit/read r))))
(defn transit-json-write [^String s] ;; 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)] (with-open [baos (java.io.ByteArrayOutputStream. 4096)]
(let [w (transit/writer baos :json)] (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) (transit/write w s)
(str baos)))) (str baos))))
@ -58,7 +107,7 @@
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) :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))
@ -82,11 +131,18 @@
#(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?})] (let [res (invoke pod sym args {:async async? :arg-meta metadata?})]
res)))])) res)))]))
vars)) vars))
@ -112,84 +168,96 @@
(throw e))))) (throw e)))))
:transit+json :transit+json
(fn [s] (fn [s]
(try (transit-json-read s) (try (transit-json-read (:pod-id pod) s)
(catch Exception e (catch Exception e
(binding [*out* *err*] (binding [*out* *err*]
(println "Cannot read Transit JSON: " (pr-str s)) (println "Cannot read Transit JSON: " (pr-str s))
(throw e))))))] (throw e))))))]
(try (binding [*pod-id* (:pod-id pod)]
(loop [] (try
(let [reply (try (read stdout) (loop []
(catch java.io.EOFException _ (let [reply (try (read stdout)
::EOF))] (catch java.io.EOFException _
(when-not (identical? ::EOF reply) ::EOF)
(let [id (get reply "id") (catch java.net.SocketException e
id (bytes->string id) (if (= "Socket closed" (ex-message e))
value* (find reply "value") ::EOF
value (some-> value* (throw e))))]
second (when-not (identical? ::EOF reply)
bytes->string (let [id (get reply "id")
read-fn) id (bytes->string id)
status (get reply "status") value* (find reply "value")
status (set (map (comp keyword bytes->string) status)) [exception value] (try (some->> value*
error? (contains? status :error) second
done? (or error? (contains? status :done)) bytes->string
[ex-message ex-data] read-fn
(when error? (vector nil))
[(or (some-> (get reply "ex-message") (catch Exception e
bytes->string) [e nil]))
"") status (get reply "status")
(or (some-> (get reply "ex-data") status (set (map (comp keyword bytes->string) status))
bytes->string error? (or exception (contains? status :error))
read-fn) done? (or error? exception (contains? status :done))
{})]) [ex-message ex-data]
namespace (when-let [v (get reply "vars")] (when error?
(let [name-str (-> (get reply "name") [(or (some-> (get reply "ex-message")
bytes->string) bytes->string)
name (symbol name-str)] "")
{:name name (or (some-> (get reply "ex-data")
:vars (bencode->vars pod name-str v)})) bytes->string
chan (get @chans id) read-fn)
promise? (instance? clojure.lang.IPending chan) {})])
exception (when (and promise? error?) namespace (when-let [v (get reply "vars")]
(ex-info ex-message ex-data)) (let [name-str (-> (get reply "name")
;; NOTE: if we need more fine-grained handlers, we will add bytes->string)
;; a :raw handler that will just get the bencode message's raw name (symbol name-str)]
;; data {:name name
{error-handler :error :vars (bencode->vars pod name-str v)}))
done-handler :done chan (get @chans id)
success-handler :success} (when (map? chan) promise? (instance? clojure.lang.IPending chan)
chan) exception (or exception
out (some-> (get reply "out") (when (and promise? error?)
bytes->string) (ex-info ex-message ex-data)))
err (some-> (get reply "err") ;; NOTE: if we need more fine-grained handlers, we will add
bytes->string)] ;; a :raw handler that will just get the bencode message's raw
;; NOTE: write to out and err before delivering promise for making ;; data
;; listening to output synchronous. {error-handler :error
(when out done-handler :done
(binding [*out* out-stream] success-handler :success} (when (map? chan)
(println out))) chan)
(when err (binding [*out* err-stream] out (some-> (get reply "out")
(println err))) bytes->string)
(when (or value* error? namespace) err (some-> (get reply "err")
(cond promise? bytes->string)]
(deliver chan (cond error? exception ;; NOTE: write to out and err before delivering promise for making
value value ;; listening to output synchronous.
namespace namespace)) (when out
(and (not error?) success-handler) (binding [*out* out-stream]
(success-handler value) (print out)
(and error? error-handler) (.flush ^java.io.Writer out-stream)))
(error-handler {:ex-message ex-message (when err
:ex-data ex-data}))) (binding [*out* err-stream]
(when (and done? (not error?)) (print err)
(when promise? (.flush ^java.io.Writer err-stream)))
(deliver chan nil)) (when (or value* error? namespace)
(when done-handler (cond promise?
(done-handler)))) (deliver chan (cond error? exception
(recur)))) value value
(catch Exception e namespace namespace))
(binding [*out* *err* #_err-stream] (and (not error?) success-handler)
(prn e)))))) (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 {}))
@ -201,27 +269,24 @@
(defn lookup-pod [pod-id] (defn lookup-pod [pod-id]
(get @pods pod-id)) (get @pods pod-id))
(defn destroy* [{:keys [:stdin :process :ops]}]
(if (contains? ops :shutdown)
(do (write stdin
{"op" "shutdown"
"id" (next-id)})
(.waitFor ^Process process))
(.destroy ^Process process)))
(defn destroy [pod-id-or-pod] (defn destroy [pod-id-or-pod]
(let [pod-id (get-pod-id pod-id-or-pod)] (let [pod-id (get-pod-id pod-id-or-pod)]
(when-let [pod (lookup-pod pod-id)] (when-let [pod (lookup-pod pod-id)]
(if (contains? (:ops pod) :shutdown) (destroy* pod)
(do (write (:stdin pod)
{"op" "shutdown"
"id" (next-id)})
(.waitFor ^Process (:process pod)))
(.destroy ^Process (:process pod)))
(when-let [rns (:remove-ns pod)] (when-let [rns (:remove-ns pod)]
(doseq [[ns-name _] (:namespaces pod)] (doseq [[ns-name _] (:namespaces pod)]
(rns ns-name)))) (rns ns-name))))
(swap! pods dissoc pod-id) (swap! pods dissoc pod-id)
nil)) nil))
(def next-pod-id
(let [counter (atom 0)]
(fn []
(let [[o _] (swap-vals! counter inc)]
o))))
(def bytes->symbol (def bytes->symbol
(comp symbol bytes->string)) (comp symbol bytes->string))
@ -244,7 +309,8 @@
the socket is connected." the socket is connected."
^Socket ^Socket
[^String hostname ^Integer port] [^String hostname ^Integer port]
(Socket. hostname port)) (doto (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."
@ -254,7 +320,8 @@
(catch java.net.SocketException _ nil))) (catch java.net.SocketException _ nil)))
(defn port-file [pid] (defn port-file [pid]
(io/file (str ".babashka-pod-" pid ".port"))) (doto (io/file (str ".babashka-pod-" pid ".port"))
(.deleteOnExit)))
(defn read-port [^java.io.File port-file] (defn read-port [^java.io.File port-file]
(loop [] (loop []
@ -263,67 +330,104 @@
(let [s (slurp f)] (let [s (slurp f)]
(when (str/ends-with? s "\n") (when (str/ends-with? s "\n")
(str/trim s))))] (str/trim s))))]
(Integer. s) (Integer/parseInt s)
(recur))))) (recur)))))
(defn debug [& strs] (defn debug [& strs]
(binding [*out* *err*] (binding [*out* *err*]
(println (str/join " " (map pr-str strs))))) (println (str/join " " (map pr-str strs)))))
;; TODO: symbol -> look up pod in local cache, invoke if present, else (defn resolve-pod [pod-spec {:keys [:version :path :force] :as opts}]
;; download via package. (when (qualified-symbol? pod-spec)
;; What about versions? (when (and (not version) (not path))
;; bb can package definitions of popular pods in its resources (throw (IllegalArgumentException. "Version or path must be provided")))
;; but what if the resources have an error - maybe best to fetch the definitions from github (when (and version path)
;; (load-pod 'org.babashka/postgresql) (throw (IllegalArgumentException. "You must provide either version or path, not both"))))
;; (load-pod 'org.babashka/postgresql_0.0.1) (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 opts]
(let [{:keys [:version :force]} opts (let [{:keys [:pod-spec :opts]} (resolve-pod pod-spec opts)
resolved (when (qualified-symbol? pod-spec) {:keys [:remove-ns :resolve]} opts
(resolver/resolve pod-spec version force))
opts (if resolved {p :process, stdin :stdin, stdout :stdout, socket :socket
(if-let [extra-opts (:options resolved)] :as running-pod}
(merge opts extra-opts) (run-pod pod-spec opts)
opts)
opts) reply (or (:metadata opts)
{:keys [:remove-ns :resolve :transport]} opts (describe-pod running-pod))
pod-spec (cond resolved [(:executable resolved)] {:keys [:format :ops :readers]} (describe->metadata reply resolve)
(string? pod-spec) [pod-spec]
:else pod-spec)
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))])
_ (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

View file

@ -15,29 +15,43 @@
"x86_64" "x86_64"
arch)) arch))
(def os {:os/name (System/getProperty "os.name") (defn normalize-os [os]
:os/arch (let [arch (System/getProperty "os.arch")] (-> os str/lower-case (str/replace #"\s+" "_")))
(normalize-arch arch))})
(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] (defn warn [& strs]
(binding [*out* *err*] (binding [*out* *err*]
(apply println strs))) (apply println strs)))
(defn match-artifacts [package] (defn match-artifacts
(let [artifacts (:pod/artifacts package) ([package] (match-artifacts package (:os/arch @os)))
res (filter (fn [{os-name :os/name ([package arch]
os-arch :os/arch}] (let [artifacts (:pod/artifacts package)
(let [os-arch (normalize-arch os-arch)] res (filter (fn [{os-name :os/name
(and (re-matches (re-pattern os-name) (:os/name os)) os-arch :os/arch}]
(re-matches (re-pattern os-arch) (let [os-arch (normalize-arch os-arch)]
(:os/arch os))))) (and (re-matches (re-pattern os-name) (:os/name @os))
artifacts)] (re-matches (re-pattern os-arch)
(when (empty? res) arch))))
(throw (IllegalArgumentException. (format "No executable found for pod %s (%s) and OS %s/%s" artifacts)]
(:pod/name package) (if (empty? res)
(:pod/version package) (if (and (= "Mac OS X" (:os/name @os))
(:os/name os) (= "aarch64" (:os/arch @os)))
(:os/arch os))))) ;; Rosetta2 fallback on Apple M1 machines
res)) (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 (defn unzip [{:keys [^java.io.File zip-file
^java.io.File destination-dir ^java.io.File destination-dir
@ -77,7 +91,10 @@
^"[Ljava.nio.file.CopyOption;" ^"[Ljava.nio.file.CopyOption;"
(into-array (into-array
[java.nio.file.StandardCopyOption/REPLACE_EXISTING]))) [java.nio.file.StandardCopyOption/REPLACE_EXISTING])))
(sh "tar" "xf" (.getPath tmp-file) "--directory" (.getPath destination-dir)) (.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))) (.delete tmp-file)))
(defn make-executable [dest-dir executables verbose?] (defn make-executable [dest-dir executables verbose?]
@ -97,11 +114,20 @@
(with-open [is (.getInputStream conn)] (with-open [is (.getInputStream conn)]
(io/copy is dest)))) (io/copy is dest))))
(def pod-manifests-dir (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 ;; wrapped in delay for GraalVM native-image
(delay (io/file (or (System/getenv "XDG_DATA_HOME") (delay
(System/getProperty "user.home")) (repo-dir)))
".babashka" "pods" "repository")))
(defn github-url [qsym version] (defn github-url [qsym version]
(format (format
@ -110,7 +136,7 @@
(defn pod-manifest (defn pod-manifest
[qsym version force?] [qsym version force?]
(let [f (io/file @pod-manifests-dir (str qsym) (str version) "manifest.edn")] (let [f (io/file @pods-repo-dir (str qsym) (str version) "manifest.edn")]
(if (and (not force?) (if (and (not force?)
(.exists f)) (.exists f))
(edn/read-string (slurp f)) (edn/read-string (slurp f))
@ -121,27 +147,30 @@
^java.io.File ^java.io.File
[{pod-name :pod/name [{pod-name :pod/name
pod-version :pod/version}] pod-version :pod/version}]
(io/file (or (let [base-file
(System/getenv "XDG_CACHE_HOME") (if-let [pods-dir (System/getenv "BABASHKA_PODS_DIR")]
(System/getProperty "user.home")) (io/file pods-dir)
".babashka" (io/file (or
"pods" (System/getenv "XDG_CACHE_HOME")
"repository" (System/getProperty "user.home"))
(str pod-name) ".babashka"
pod-version)) "pods"))]
(io/file base-file
"repository"
(str pod-name)
pod-version
(normalize-os (:os/name @os))
(:os/arch @os))))
(defn data-dir (defn data-dir
^java.io.File ^java.io.File
[{pod-name :pod/name [{pod-name :pod/name
pod-version :pod/version}] pod-version :pod/version}]
(io/file (or (io/file @pods-repo-dir
(System/getenv "XDG_DATA_HOME")
(System/getProperty "user.home"))
".babashka"
"pods"
"repository"
(str pod-name) (str pod-name)
pod-version)) pod-version
(normalize-os (:os/name @os))
(:os/arch @os)))
(defn sha256 [file] (defn sha256 [file]
(let [buf (byte-array 8192) (let [buf (byte-array 8192)

View file

@ -6,17 +6,18 @@
(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)
(do (intern name var-sym v)
(ns-unmap *ns* var-sym)
(intern name var-sym v))
(string? v) (string? v)
(load-string v))))) (load-string v)))))
@ -55,9 +56,10 @@
(when defer? (when defer?
[ns-name pod])) [ns-name pod]))
namespaces))) namespaces)))
(doseq [[ns-sym vars lazy?] namespaces (binding [impl/*pod-id* (:pod-id pod)]
:when (not lazy?)] (doseq [[ns-sym vars lazy?] namespaces
(process-namespace {:name ns-sym :vars vars})) :when (not lazy?)]
(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)})))
@ -69,3 +71,18 @@
(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,6 +1,12 @@
(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)
@ -11,14 +17,58 @@
(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] vars] (doseq [[var-name var-value :as var] 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)) (symbol (str ns-name) (str var-name)) var-value (meta var-name)))
(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]}]
(if version
(io/file (resolver/cache-dir {:pod/name pod-spec :pod/version version})
"metadata.cache")
(let [config-dir (.getParentFile bb-edn-file)
cache-dir (io/file config-dir ".babashka")
pod-file (-> path io/file .getName)
cache-file (io/file cache-dir (str pod-file ".metadata.cache"))]
cache-file)))
(defn load-metadata-from-cache [bb-edn-file pod-spec opts]
(let [cache-file (metadata-cache-file bb-edn-file pod-spec opts)]
(when (.exists cache-file)
(with-open [r (PushbackInputStream. (io/input-stream cache-file))]
(impl/read r)))))
(defn load-pod-metadata* [bb-edn-file pod-spec {:keys [:version :cache] :as opts}]
(let [metadata (impl/load-pod-metadata pod-spec opts)
cache-file (when (and metadata cache)
(metadata-cache-file bb-edn-file pod-spec opts))]
(when cache-file
(io/make-parents cache-file)
(when (fs/writable? (fs/parent cache-file))
(with-open [w (io/output-stream cache-file)]
(impl/write w metadata))))
metadata))
(defn load-pod-metadata
([pod-spec opts] (load-pod-metadata nil pod-spec opts))
([bb-edn-file pod-spec {:keys [:cache] :as opts}]
(let [metadata
(if-let [cached-metadata (when cache
(load-metadata-from-cache bb-edn-file
pod-spec
opts))]
cached-metadata
(load-pod-metadata* bb-edn-file pod-spec opts))]
(reduce
(fn [pod-namespaces ns]
(let [ns-sym (-> ns (get "name") impl/bytes->string symbol)]
(assoc pod-namespaces ns-sym {:pod-spec pod-spec
:opts (assoc opts :metadata metadata)})))
{} (get metadata "namespaces")))))
(defn load-pod (defn load-pod
([ctx pod-spec] (load-pod ctx pod-spec nil)) ([ctx pod-spec] (load-pod ctx pod-spec nil))
([ctx pod-spec version opts] (load-pod ctx pod-spec (assoc opts :version version))) ([ctx pod-spec version opts] (load-pod ctx pod-spec (assoc opts :version version)))
@ -65,9 +115,10 @@
(when prev-load-fn (when prev-load-fn
(prev-load-fn m))))] (prev-load-fn m))))]
(swap! env assoc :load-fn new-load-fn))) (swap! env assoc :load-fn new-load-fn)))
(doseq [[ns-name vars lazy?] namespaces (binding [impl/*pod-id* (:pod-id pod)]
:when (not lazy?)] (doseq [[ns-name vars lazy?] namespaces
(process-namespace ctx {:name ns-name :vars vars})) :when (not lazy?)]
(process-namespace ctx {:name ns-name :vars vars})))
(sci/future (impl/processor pod)) (sci/future (impl/processor pod))
{:pod/id (:pod-id pod)}))) {:pod/id (:pod-id pod)})))
@ -79,3 +130,18 @@
(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

@ -36,12 +36,34 @@
(defn transit-json-read [^String s] (defn transit-json-read [^String s]
(with-open [bais (java.io.ByteArrayInputStream. (.getBytes s "UTF-8"))] (with-open [bais (java.io.ByteArrayInputStream. (.getBytes s "UTF-8"))]
(let [r (transit/reader bais :json)] (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)))) (transit/read r))))
(defn transit-json-write [^String s] (defn transit-json-write [s]
(with-open [baos (java.io.ByteArrayOutputStream. 4096)] (with-open [baos (java.io.ByteArrayOutputStream. 4096)]
(let [w (transit/writer baos :json)] (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) (transit/write w s)
(str baos)))) (str baos))))
@ -94,7 +116,8 @@
"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"
"meta" "{:doc \"add the arguments\"}"}
{"name" "range-stream" {"name" "range-stream"
"async" "true"} "async" "true"}
{"name" "assoc"} {"name" "assoc"}
@ -111,7 +134,35 @@
{"name" "other-tag"} {"name" "other-tag"}
;; reads thing with other tag ;; reads thing with other tag
{"name" "read-other-tag" {"name" "read-other-tag"
"code" "(defn read-other-tag [x] [x x])"}] "code" "(defn read-other-tag [x] [x x])"
"meta" "{:doc \"unread\"}"}
{"name" "round-trip-meta"
"arg-meta" "true"}
{"name" "dont-round-trip-meta"
"arg-meta" "false"}
{"name" "-local-date-time"}
{"name" "transit-stuff"
"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)} dependents)}
{"name" "pod.test-pod.loaded" {"name" "pod.test-pod.loaded"
"defer" "true"} "defer" "true"}
@ -135,65 +186,91 @@
pod.test-pod/add-sync pod.test-pod/add-sync
(try (let [ret (apply + args)] (try (let [ret (apply + args)]
(write out (write out
{"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 out
{"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 out
{"value" (write-fn v) {"value" (write-fn v)
"id" id}) "id" id})
(Thread/sleep 100)) (Thread/sleep 100))
(write out (write out
{"status" ["done"] {"status" ["done"]
"id" id})) "id" id}))
pod.test-pod/assoc pod.test-pod/assoc
(write out (write out
{"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 out
{"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
{"out" (pr-str args) {"out" (with-out-str (prn args))
"id" id}) "id" id})
(write out (write out
{"status" ["done"] {"status" ["done"]
"id" id})) "id" id}))
pod.test-pod/print-err pod.test-pod/print-err
(do (write out (do (write out
{"err" (pr-str args) {"err" (with-out-str (prn args))
"id" id}) "id" id})
(write out (write out
{"status" ["done"] {"status" ["done"]
"id" id})) "id" id}))
pod.test-pod/return-nil pod.test-pod/return-nil
(write out (write out
{"status" ["done"] {"status" ["done"]
"id" id "id" id
"value" (write-fn nil)}) "value" (write-fn nil)})
pod.test-pod/reader-tag pod.test-pod/reader-tag
(write out (write out
{"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 out
{"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")
@ -204,20 +281,20 @@
(case ns (case ns
pod.test-pod.loaded pod.test-pod.loaded
(write out (write out
{"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 out
{"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*]
@ -225,6 +302,6 @@
(defn -main [& args] (defn -main [& args]
#_(binding [*out* *err*] #_(binding [*out* *err*]
(prn :args args)) (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,6 +1,6 @@
(require '[babashka.pods :as pods]) (require '[babashka.pods :as pods])
(pods/load-pod 'org.babashka/buddy "0.0.1") (pods/load-pod 'org.babashka/buddy "0.1.0")
(require '[pod.babashka.buddy.codecs :as codecs] (require '[pod.babashka.buddy.codecs :as codecs]
'[pod.babashka.buddy.hash :as hash]) '[pod.babashka.buddy.hash :as hash])
@ -8,4 +8,4 @@
(println (-> (hash/sha256 "foobar") (println (-> (hash/sha256 "foobar")
(codecs/bytes->hex))) (codecs/bytes->hex)))
(pods/load-pod 'org.babashka/etaoin) ;; should cause error when version is missing (pods/load-pod 'org.babashka/etaoin) ;; should cause error when version & path are missing

View file

@ -5,9 +5,13 @@
(def socket (System/getenv "BABASHKA_POD_TEST_SOCKET")) (def socket (System/getenv "BABASHKA_POD_TEST_SOCKET"))
(def pod-id (:pod/id (pods/load-pod (cond-> ["clojure" "-A:test-pod"] (def cmd (cond-> ["clojure" "-M:test-pod"]
(= "json" fmt) (conj "--json") (= "json" fmt) (conj "--json")
(= "transit+json" fmt) (conj "--transit+json")) (= "transit+json" fmt) (conj "--transit+json")))
;; (.println System/err cmd)
(def pod-id (:pod/id (pods/load-pod cmd
{:socket (boolean socket)}))) {:socket (boolean socket)})))
(require '[pod.test-pod :as pod]) (require '[pod.test-pod :as pod])
@ -51,6 +55,10 @@
(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 (if (= "edn" fmt)
@ -64,12 +72,43 @@
(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)))
@ -90,4 +129,13 @@
other-tagged other-tagged
loaded loaded
fn-called fn-called
should-be-1] local-date-time
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

@ -0,0 +1,11 @@
(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

@ -19,10 +19,10 @@
(let [out (java.io.StringWriter.) (let [out (java.io.StringWriter.)
err (java.io.StringWriter.) err (java.io.StringWriter.)
ex (binding [*out* out ex (binding [*out* out
*err* err] *err* err]
(try (load-string (try (load-string
pod-registry) pod-registry)
(catch Exception e (catch Exception e
e)))] e)))]
(is (str/includes? (str out) "c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2")) (is (str/includes? (str out) "c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2"))
(is (str/includes? (pr-str ex) "Version must be provided" )))) (is (str/includes? (pr-str ex) "Version or path must be provided"))))

View file

@ -13,12 +13,19 @@
{'load-pod (fn [& args] {'load-pod (fn [& args]
(apply pods/load-pod @ctx-ref args)) (apply pods/load-pod @ctx-ref args))
'invoke pods/invoke 'invoke pods/invoke
'unload-pod pods/unload-pod}} 'unload-pod pods/unload-pod
:classes {'System System}}) '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) _ (vreset! ctx-ref ctx)
ret (sci/binding [sci/out out ret (sci/binding [sci/out out
sci/err err] sci/err err]
(sci/eval-string* ctx test-program))] (binding [*out* out
*err* err]
(sci/eval-string* ctx test-program)))]
(assertions out err ret))) (assertions out err ret)))
(deftest pod-registry-test (deftest pod-registry-test
@ -31,4 +38,4 @@
(catch Exception e (catch Exception e
e)))] e)))]
(is (str/includes? (str out) "c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2")) (is (str/includes? (str out) "c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2"))
(is (str/includes? (pr-str ex) "Version must be provided" )))) (is (str/includes? (pr-str ex) "Version or path must be provided"))))

View file

@ -1,34 +1,54 @@
(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 '["pod.test-pod" (map vector (replace
pod.test-pod {::edn-error (if (= "edn"
{:a 1, :b 2} (System/getenv "BABASHKA_POD_TEST_FORMAT"))
6 "Map literal must contain an even number of forms"
3 ::dont-care)}
[1 2 3 4 5 6 7 8 9] '["pod.test-pod"
#"Illegal arguments / \{:args [\(\[]1 2 3[\)\]]\}" pod.test-pod
nil {:a 1, :b 2}
3 6
#"cast" 3
{:args ["1" 2]} [1 2 3 4 5 6 7 8 9]
true #"Illegal arguments / \{:args [\(\[]1 2 3[\)\]]\}"
9 nil
[1 2 3] 3
[[1] [1]] #"cast"
2 {:args ["1" 2]}
3 true
1] 9
[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)))]
(if (instance? java.util.regex.Pattern expected) (cond (instance? java.util.regex.Pattern expected)
(is (re-find expected actual)) (is (re-find expected actual))
(is (= expected actual)))) (= ::dont-care expected) nil
: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 (= "(\"hello\" \"print\" \"this\" \"error\")\n" (str err)))) (is (str/starts-with? (str err) "(\"hello\" \"print\" \"this\" \"error\")" )))
(def pod-registry (slurp (io/file "test-resources" "pod_registry.clj"))) (def pod-registry (slurp (io/file "test-resources" "pod_registry.clj")))