Merge pull request #302 from jolt-lang/windows-nrepl-and-version

Fix nREPL on Windows; add a version string
This commit is contained in:
Dmitri Sotnikov 2026-07-04 04:36:47 +00:00 committed by GitHub
commit 080cbd710c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 97 additions and 15 deletions

View file

@ -154,6 +154,10 @@ jobs:
# dev-machine check — see jolt-8479). `make joltc-release`, not `make joltc`. # dev-machine check — see jolt-8479). `make joltc-release`, not `make joltc`.
- name: Build joltc (release) - name: Build joltc (release)
run: make joltc-release run: make joltc-release
env:
# Bake the release tag into the binary (build-joltc falls back to
# `git describe` when this is empty, e.g. a workflow_dispatch dry run).
JOLT_VERSION: ${{ startsWith(github.ref, 'refs/tags/') && github.ref_name || '' }}
- name: Inspect the binary (Windows) - name: Inspect the binary (Windows)
if: runner.os == 'Windows' if: runner.os == 'Windows'

View file

@ -14,5 +14,7 @@
# JOLT_PWD. # JOLT_PWD.
root="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)" root="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)"
export JOLT_PWD="${JOLT_PWD:-$PWD}" export JOLT_PWD="${JOLT_PWD:-$PWD}"
# Version for --version / banners: git describe of this checkout, else "dev".
export JOLT_VERSION="${JOLT_VERSION:-$(git -C "$root" describe --tags --always --dirty 2>/dev/null || echo dev)}"
cd "$root" || exit 1 cd "$root" || exit 1
exec chez --script host/chez/cli.ss "$@" exec chez --script host/chez/cli.ss "$@"

View file

@ -46,6 +46,15 @@
(unless (or jb-release? (string=? jb-profile "debug")) (unless (or jb-release? (string=? jb-profile "debug"))
(error 'build-joltc "profile must be \"release\" or \"debug\"" jb-profile)) (error 'build-joltc "profile must be \"release\" or \"debug\"" jb-profile))
;; Version baked into the binary's saved heap. Prefer $JOLT_VERSION (CI sets it to
;; the release tag); else derive it from git in this checkout; else "dev".
(define jb-version
(let ((env (getenv "JOLT_VERSION")))
(if (and env (> (string-length env) 0))
env
(let ((s (bld-sh-capture "git describe --tags --always --dirty 2>/dev/null")))
(if (> (string-length s) 0) s "dev")))))
(define jb-build (string-append jb-out ".build")) (define jb-build (string-append jb-out ".build"))
(bld-check-toolchain) (bld-check-toolchain)
(bld-system (string-append "mkdir -p '" (path-parent jb-out) "' '" jb-build "'")) (bld-system (string-append "mkdir -p '" (path-parent jb-out) "' '" jb-build "'"))
@ -164,6 +173,10 @@
(jb-emit-runtime-embeds out) (jb-emit-runtime-embeds out)
(put-string out "\n;; === embedded jolt-core + stdlib source ===\n") (put-string out "\n;; === embedded jolt-core + stdlib source ===\n")
(jb-emit-source-embeds out) (jb-emit-source-embeds out)
;; Bake the version into the saved heap (runs at heap-build; loader.ss defined
;; jolt-baked-version above, so this set! resolves).
(put-string out (string-append "\n;; === baked version ===\n(set! jolt-baked-version "
(ei-str-lit jb-version) ")\n"))
(put-string out "\n;; === joltc launcher ===\n") (put-string out "\n;; === joltc launcher ===\n")
(jb-emit-launcher out) (jb-emit-launcher out)
(close-port out)) (close-port out))

View file

@ -384,3 +384,14 @@
(def-var! "jolt.host" "load-namespace" (lambda (n) (load-namespace n) jolt-nil)) (def-var! "jolt.host" "load-namespace" (lambda (n) (load-namespace n) jolt-nil))
(def-var! "jolt.host" "file-exists?" (lambda (p) (if (file-exists? p) #t #f))) (def-var! "jolt.host" "file-exists?" (lambda (p) (if (file-exists? p) #t #f)))
(def-var! "jolt.host" "getenv" (lambda (n) (let ((v (getenv n))) (if v v jolt-nil)))) (def-var! "jolt.host" "getenv" (lambda (n) (let ((v (getenv n))) (if v v jolt-nil))))
;; jolt version string. A self-contained binary build bakes the real tag into the
;; saved heap by emitting (set! jolt-baked-version "…") in flat.ss; a dev run off
;; the seed leaves it #f and falls back to $JOLT_VERSION (bin/joltc sets it from
;; `git describe`), then "dev".
(define jolt-baked-version #f)
(def-var! "jolt.host" "jolt-version"
(lambda ()
(or jolt-baked-version
(let ((v (getenv "JOLT_VERSION"))) (and v (> (string-length v) 0) v))
"dev")))

View file

@ -66,6 +66,10 @@
(if-let [root (:deps/root spec)] (str checkout "/" root) checkout)) (if-let [root (:deps/root spec)] (str checkout "/" root) checkout))
(:jolt/module spec) (:jolt/module spec)
(do (warn "skipping janet dependency " coord " (:jolt/module is obsolete on Chez)") nil) (do (warn "skipping janet dependency " coord " (:jolt/module is obsolete on Chez)") nil)
;; jolt IS Clojure — a dependency on org.clojure/clojure is satisfied
;; intrinsically, so skip it silently rather than warning about the (unusable)
;; :mvn/version coordinate.
(= coord 'org.clojure/clojure) nil
:else :else
(do (warn "skipping unsupported coordinate " coord " " (pr-str spec)) nil))) (do (warn "skipping unsupported coordinate " coord " " (pr-str spec)) nil)))

View file

@ -8,6 +8,8 @@
(defn- project-dir [] (or (jolt.host/getenv "JOLT_PWD") ".")) (defn- project-dir [] (or (jolt.host/getenv "JOLT_PWD") "."))
(defn- version [] (jolt.host/jolt-version))
(defn- current-platform [] (defn- current-platform []
(let [os (str/lower-case (or (System/getProperty "os.name") ""))] (let [os (str/lower-case (or (System/getProperty "os.name") ""))]
(cond (str/includes? os "mac") :darwin (cond (str/includes? os "mac") :darwin
@ -145,7 +147,7 @@
;; loaded — same context a run gets, so (require '[some.lib]) works in the REPL. ;; loaded — same context a run gets, so (require '[some.lib]) works in the REPL.
(try (apply-project! (deps/resolve-project (project-dir))) (try (apply-project! (deps/resolve-project (project-dir)))
(catch :default _ nil)) (catch :default _ nil))
(println ";; jolt repl — :repl/quit or ^D to exit") (println (str ";; jolt " (version) " repl — :repl/quit or ^D to exit"))
(loop [] (loop []
(let [form (repl-read-form)] (let [form (repl-read-form)]
(when form (when form
@ -294,7 +296,9 @@
(when stop (stop)))))) (when stop (stop))))))
(defn- usage [] (defn- usage []
(println (str "jolt " (version)))
(println "usage: jolt <command> [args]") (println "usage: jolt <command> [args]")
(println " -e EXPR evaluate EXPR and print the result")
(println " run -m NS [args] resolve deps.edn, load NS, call its -main") (println " run -m NS [args] resolve deps.edn, load NS, call its -main")
(println " run FILE load a Clojure file") (println " run FILE load a Clojure file")
(println " build -m NS [-o OUT] [--opt|--dev] [--direct-link] [--tree-shake] [--dynamic] compile a standalone binary") (println " build -m NS [-o OUT] [--opt|--dev] [--direct-link] [--tree-shake] [--dynamic] compile a standalone binary")
@ -304,6 +308,7 @@
(println " --nrepl-server [port] start an nREPL server (default 7888) for editors") (println " --nrepl-server [port] start an nREPL server (default 7888) for editors")
(println " path print the resolved source roots") (println " path print the resolved source roots")
(println " <task> run a deps.edn :tasks entry") (println " <task> run a deps.edn :tasks entry")
(println " --version print the jolt version")
(println " --help print this message")) (println " --help print this message"))
(defn -main [& args] (defn -main [& args]
@ -312,6 +317,7 @@
(nil? cmd) (usage) (nil? cmd) (usage)
(= cmd "--help") (usage) (= cmd "--help") (usage)
(= cmd "-h") (usage) (= cmd "-h") (usage)
(#{"--version" "-V"} cmd) (println (str "jolt " (version)))
(= cmd "run") (cmd-run more) (= cmd "run") (cmd-run more)
(= cmd "repl") (repl) (= cmd "repl") (repl)
(= cmd "--nrepl-server") (nrepl more) (= cmd "--nrepl-server") (nrepl more)

View file

@ -21,25 +21,66 @@
[jolt.ffi :as ffi])) [jolt.ffi :as ffi]))
;; --- sockets (loopback server) --------------------------------------------- ;; --- sockets (loopback server) ---------------------------------------------
;; Load libc (the running process's symbols) BEFORE the foreign-fn bindings below (def ^:private os-name
;; — defcfn resolves the C entry point when the def is evaluated (at ns load), so (str/lower-case (or (System/getProperty "os.name") "")))
;; the socket symbols must already be available. (def ^:private macos? (str/includes? os-name "mac"))
(ffi/load-library) (def ^:private windows? (str/includes? os-name "win"))
;; Load the library that provides the socket symbols BEFORE the foreign-fn
;; bindings below — defcfn resolves the C entry point when the def is evaluated
;; (at ns load), so the symbols must already be available. POSIX: the running
;; process's own libc symbols. Windows: the Winsock DLL (ws2_32), whose symbols
;; are NOT in joltc.exe's export table even though it's linked in — without this
;; explicit load, (ffi/defcfn c-socket "socket" ...) fails at load with
;; "no entry for socket".
(if windows?
(ffi/load-library "ws2_32.dll")
(ffi/load-library))
;; A socket is an int fd on POSIX; on Win64 it's a SOCKET (uintptr_t) handle, but
;; those are small kernel handle values that round-trip through :int, and the
;; INVALID_SOCKET error sentinel (~0) reads back as -1 — so the fd checks below
;; work unchanged on both.
(ffi/defcfn c-socket "socket" [:int :int :int] :int) (ffi/defcfn c-socket "socket" [:int :int :int] :int)
(ffi/defcfn c-bind "bind" [:int :pointer :int] :int) (ffi/defcfn c-bind "bind" [:int :pointer :int] :int)
(ffi/defcfn c-listen "listen" [:int :int] :int) (ffi/defcfn c-listen "listen" [:int :int] :int)
(ffi/defcfn c-setsockopt "setsockopt" [:int :int :int :pointer :int] :int) (ffi/defcfn c-setsockopt "setsockopt" [:int :int :int :pointer :int] :int)
(ffi/defcfn c-accept "accept" [:int :pointer :pointer] :int :blocking) (ffi/defcfn c-accept "accept" [:int :pointer :pointer] :int :blocking)
(ffi/defcfn c-recv "recv" [:int :pointer :size_t :int] :ssize_t :blocking)
(ffi/defcfn c-send "send" [:int :pointer :size_t :int] :ssize_t :blocking) ;; recv/send and the socket-close call differ by platform. Winsock's recv/send
(ffi/defcfn c-close "close" [:int] :int) ;; take an int length and return int (not ssize_t), and a socket is closed with
;; closesocket, not close. A symbol that exists on only one OS (closesocket on
;; Windows, close on POSIX) can only be bound there, so these live in the taken
;; platform branch — jolt interns the vars from both branches at analysis time,
;; so later references resolve either way.
(if windows?
(do
(ffi/defcfn c-recv "recv" [:int :pointer :int :int] :int :blocking)
(ffi/defcfn c-send "send" [:int :pointer :int :int] :int :blocking)
(ffi/defcfn c-close "closesocket" [:int] :int)
;; Winsock must be initialized once per process before any socket call.
(ffi/defcfn c-wsastartup "WSAStartup" [:int :pointer] :int))
(do
(ffi/defcfn c-recv "recv" [:int :pointer :size_t :int] :ssize_t :blocking)
(ffi/defcfn c-send "send" [:int :pointer :size_t :int] :ssize_t :blocking)
(ffi/defcfn c-close "close" [:int] :int)))
(def ^:private AF-INET 2) (def ^:private AF-INET 2)
(def ^:private SOCK-STREAM 1) (def ^:private SOCK-STREAM 1)
(def ^:private macos? ;; SOL_SOCKET / SO_REUSEADDR: 0xffff / 4 on macOS and Windows, 1 / 2 on Linux.
(str/includes? (str/lower-case (or (System/getProperty "os.name") "")) "mac")) (def ^:private sol-socket (if (or macos? windows?) 0xffff 1))
(def ^:private sol-socket (if macos? 0xffff 1)) (def ^:private so-reuse (if (or macos? windows?) 4 2))
(def ^:private so-reuse (if macos? 4 2))
;; Initialize Winsock (a no-op off Windows). WSAStartup is refcounted and must
;; precede any socket call; WSADATA is ~408 bytes on x64, so 512 is ample.
(defn- ensure-winsock! []
(when windows?
(let [wsadata (ffi/alloc 512)]
(try
(let [r (c-wsastartup 0x0202 wsadata)]
(when-not (zero? r)
(throw (ex-info (str "WSAStartup failed: " r) {}))))
(finally (ffi/free wsadata))))))
(defn- make-sockaddr [port] (defn- make-sockaddr [port]
(let [sa (ffi/alloc 16)] (let [sa (ffi/alloc 16)]
@ -53,7 +94,7 @@
sa)) sa))
(defn- listen-socket [port] (defn- listen-socket [port]
(ffi/load-library) ; libc process symbols (ensure-winsock!) ; no-op off Windows
(let [fd (c-socket AF-INET SOCK-STREAM 0)] (let [fd (c-socket AF-INET SOCK-STREAM 0)]
(when (neg? fd) (throw (ex-info "socket() failed" {}))) (when (neg? fd) (throw (ex-info "socket() failed" {})))
(let [opt (ffi/alloc 4)] (ffi/write opt :int 0 1) (c-setsockopt fd sol-socket so-reuse opt 4) (ffi/free opt)) (let [opt (ffi/alloc 4)] (ffi/write opt :int 0 1) (c-setsockopt fd sol-socket so-reuse opt 4) (ffi/free opt))
@ -240,7 +281,8 @@
fd (listen-socket port) ; throws on bind/listen failure fd (listen-socket port) ; throws on bind/listen failure
stopped (atom false)] stopped (atom false)]
(try (spit ".nrepl-port" (str port)) (catch :default _ nil)) (try (spit ".nrepl-port" (str port)) (catch :default _ nil))
(println (str "nREPL server started on port " port " (127.0.0.1) — .nrepl-port written")) (println (str "jolt " (jolt.host/jolt-version) " nREPL server started on port "
port " (127.0.0.1) — .nrepl-port written"))
(when (seq middleware) (println (str ";; middleware: " (str/join " " middleware)))) (when (seq middleware) (println (str ";; middleware: " (str/join " " middleware))))
(println ";; connect your editor; ^C to stop") (println ";; connect your editor; ^C to stop")
(future (future