Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions .github/workflows/libdt.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Rebuilds the libdt native JVMTI agent binaries (libdt/build/*.so).
# Manually triggered; download the artifacts and commit them to libdt/build/.
# Adapted from nREPL's libnrepl workflow (https://github.com/nrepl/nrepl).
name: Build libdt native library

on:
workflow_dispatch:

jobs:
build-linux:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '21'

- name: Compile native library
run: cd libdt/ && make all

- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: libdt-linux-x64
path: libdt/build

build-linux-arm64:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up QEMU
uses: docker/setup-qemu-action@v3
with:
platforms: arm64

- name: Run the build process with Docker
uses: addnab/docker-run-action@v3
with:
image: eclipse-temurin:21
options: |
--platform linux/arm64
--volume ${{ github.workspace }}:/build
run: |
apt-get update && apt-get install -y make gcc
cd /build/libdt/ && make all

- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: libdt-linux-arm64
path: libdt/build

build-macos:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '21'

- name: Compile native library
run: cd libdt/ && make all

- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: libdt-macos-universal
path: libdt/build
12 changes: 9 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ on:
push:
branches:
- master
- feature/dockerfile
- fix/break-handling

jobs:
Config:
Expand Down Expand Up @@ -67,14 +67,20 @@ jobs:
bb: latest
- name: Update VERSION
run: echo "${{ needs.Config.outputs.version }}" > resources/VERSION
- name: Compile Java
run: bb java:compile
- name: Zip it
run: zip -r deps-try.zip . -x ".git/*"
- name: Build uberjar
run: |
rm -rf .git
# remove stuff not wanted in JAR
rm -rf .git .github
mv deps-try.zip ..
bb uberjar deps-try-bb.jar -m eval.deps-try

bb bb:uberjar
ls -lah deps-try-bb.jar
jar -tf deps-try-bb.jar

mv ../deps-try.zip .
- name: Testrun uberjar
run: bb deps-try-bb.jar -h
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@
/.clj-kondo
.lsp
/VERSION
*.class
/tmp/*
!/tmp/.keep
6 changes: 5 additions & 1 deletion bb.edn
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
{:deps {io.github.eval/deps-try {:local/root "."}}
:bbin/bin {deps-try {:main-opts ["-m" "eval.deps-try"]}}
:tasks
{gen-manifest {:doc "Generate recipe-manifest and print to stdout"
{java:compile {:doc "Compile files under java/"
:task (shell {:dir "java"} "javac --release 11 dt/JvmtiAgent.java")}
bb:uberjar {:doc "Create the app's uberjar"
:task (shell "bb uberjar deps-try-bb.jar -m eval.deps-try")}
gen-manifest {:doc "Generate recipe-manifest and print to stdout"
:requires ([babashka.fs :as fs])
:task (exec 'eval.deps-try.recipe/generate&print-manifest
{:exec-args
Expand Down
9 changes: 6 additions & 3 deletions deps.edn
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
{:paths ["src" "resources" "."]
;; NOTE keep "." out of :paths: `bb uberjar` jars every classpath folder
;; wholesale, so "." would put the entire repo in the released jar (and the
;; libdt binaries in twice).
{:paths ["src" "resources" "java" "libdt/build"]
:deps {borkdude/edamame {:mvn/version "1.4.24"}
cider/orchard {:mvn/version "0.22.0"}
cider/orchard {:mvn/version "0.30.0"}
com.bhauman/rebel-readline {:local/root "./vendor/rebel-readline/rebel-readline"}
deps-try/cli {:local/root "./vendor/deps-try.cli"}
deps-try/http-client {:local/root "./vendor/deps-try.http-client"}
io.github.eval/compliment {:git/sha "cd9706db27d456e8940dcd1118174c94effa9358"}
org.clojure/clojure {:mvn/version "1.12.0-alpha11"}
org.clojure/clojure {:mvn/version "1.12.5"}
org.clojure/tools.gitlibs {:mvn/version "2.5.197"}
;; suppress logging
org.slf4j/slf4j-nop {:mvn/version "2.0.5"}}
Expand Down
24 changes: 24 additions & 0 deletions java/dt/JvmtiAgent.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package dt;

/**
* Java facade for the C side of the libdt JVMTI agent. Currently used to
* stop threads on JDK20+.
*/
public class JvmtiAgent {

// This will bind to Java_dt_JvmtiAgent_stopThread function once the
// agent containing this function will be attached.
public static native void stopThread(Thread thread, Throwable throwable);

/**
* Forcibly stop a given thread.
*/
public static void stopThread(Thread thread) {
// ThreadDeath is deprecated, but so is Thread.stop(). We can revisit
// this when JVM actually decides to remove this class.
@SuppressWarnings("deprecation")
Throwable throwable = new ThreadDeath();
throwable.setStackTrace(new StackTraceElement[0]);
stopThread(thread, throwable);
}
}
49 changes: 49 additions & 0 deletions libdt/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
LIB=libdt-$(OS)-$(ARCH).so
CXXFLAGS=-O3 -fno-exceptions -fvisibility=hidden
INCLUDES=-I$(JAVA_HOME)/include
LIBS=-ldl
SOURCES := $(wildcard src/*.c)

ifeq ($(JAVA_HOME),)
$(error JAVA_HOME is not set (e.g. on macOS: JAVA_HOME=$$(/usr/libexec/java_home) make))
endif

ARCH:=$(shell uname -m)
ifeq ($(ARCH),x86_64)
ARCH=x64
else
ifeq ($(findstring arm,$(ARCH)),arm)
ifeq ($(findstring 64,$(ARCH)),64)
ARCH=arm64
else
ARCH=arm32
endif
else ifeq ($(findstring aarch64,$(ARCH)),aarch64)
ARCH=arm64
else
ARCH=x86
endif
endif

OS:=$(shell uname -s)
ifeq ($(OS),Darwin)
CXXFLAGS += -arch x86_64 -arch arm64 -mmacos-version-min=10.12
INCLUDES += -I$(JAVA_HOME)/include/darwin
OS=macos
ARCH=universal
else
CXXFLAGS += -Wl,-z,defs
LIBS += -lrt
INCLUDES += -I$(JAVA_HOME)/include/linux
OS=linux
CC=gcc
endif

all: build/$(LIB)

clean:
$(RM) -r build

build/$(LIB): $(SOURCES)
mkdir -p build
$(CC) $(CXXFLAGS) $(INCLUDES) -fPIC -shared -o $@ $(SOURCES) $(LIBS)
Binary file added libdt/build/libdt-linux-arm64.so
Binary file not shown.
Binary file added libdt/build/libdt-linux-x64.so
Binary file not shown.
Binary file added libdt/build/libdt-macos-universal.so
Binary file not shown.
51 changes: 51 additions & 0 deletions libdt/src/dt_agent.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Native agent that deps-try uses for deeply buried JDK functionality.
// Currently it is used to bring back Thread.stop() that was lost in JDK20+.
// Adapted from nREPL's libnrepl agent (https://github.com/nrepl/nrepl/pull/318).

#include <jvmti.h>
#include <stdio.h>

static jvmtiEnv* jvmti_env;

// https://docs.oracle.com/en/java/javase/21/docs/specs/jvmti.html#StopThread
JNIEXPORT void JNICALL Java_dt_JvmtiAgent_stopThread
(JNIEnv* env, jclass cls, jthread thread, jobject throwable) {
jvmtiError err;
jvmtiThreadInfo threadInfo;

err = (*jvmti_env)->GetThreadInfo(jvmti_env, thread, &threadInfo);
if (err != JVMTI_ERROR_NONE) {
fprintf(stderr, "Error getting thread info: %d\n", err);
return;
}

//printf("Stopping thread \"%s\" using JVMTI...\n", threadInfo.name);

err = (*jvmti_env)->StopThread(jvmti_env, thread, throwable);
if (err != JVMTI_ERROR_NONE) {
fprintf(stderr, "Error stopping thread: %d\n", err);
return;
}
}

JNIEXPORT jint JNICALL Agent_OnAttach(JavaVM* vm, char* options, void* _) {
//printf("libdt native agent loaded\n");

// Initialize JVMTI environment.
jint res = (*vm)->GetEnv(vm, (void**)&jvmti_env, JVMTI_VERSION_1_2);
if (res != JNI_OK) {
fprintf(stderr, "Failed to get JVMTI environment\n");
return JNI_ERR;
}

// Request capabilities for StopThread
jvmtiCapabilities capabilities = {0};
capabilities.can_signal_thread = 1;
jvmtiError err = (*jvmti_env)->AddCapabilities(jvmti_env, &capabilities);
if (err != JVMTI_ERROR_NONE) {
fprintf(stderr, "Failed to add JVMTI capabilities: %d\n", err);
return JNI_ERR;
}

return JNI_OK;
}
57 changes: 50 additions & 7 deletions src/eval/deps_try.clj
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,29 @@

(def ^:private dev? (nil? (io/resource "VERSION")))

(defn- repl-java-version
"Major version of the JVM that the `clojure` CLI will start (found via
JAVA_CMD, JAVA_HOME or PATH, mirroring the CLI's lookup), nil when it can't
be determined."
[]
(let [java-cmd (or (System/getenv "JAVA_CMD")
(some-> (System/getenv "JAVA_HOME") (fs/path "bin" "java") str)
"java")
;; `java -version` prints to stderr, e.g. `openjdk version "21.0.2" ...`
version-line (try (:err (p/sh {:err :string} java-cmd "-version"))
(catch Exception _ nil))
[_ major minor] (some->> version-line (re-find #"version \"(\d+)(?:\.(\d+))?"))
major (some-> major parse-long)]
(when major
(if (> major 1) major (some-> minor parse-long)))))

(def ^:private version
(string/trim
(if dev?
(let [git-dir (fs/file (io/resource ".git"))]
;; the repo-root isn't on the classpath (see NOTE in deps.edn), so derive
;; it from a source file that is
(let [src-file (fs/file (io/resource "eval/deps_try.clj"))
git-dir (fs/path (-> src-file fs/parent fs/parent fs/parent) ".git")]
(:out (p/sh {} "git" "--git-dir" (str git-dir) "describe" "--tags")))
(slurp (io/resource "VERSION")))))

Expand Down Expand Up @@ -100,20 +119,44 @@
;; TODO re-enable?
;; see https://github.com/clojure/brew-install/blob/1.11.3/CHANGELOG.md
#_(warn-unless-minimum-clojure-cli-version "1.11.1.1273" tdeps-version)
(let [paths (into ["."] ;; needed for clojure.java.io/resource
(string/split init-cp #":"))
(let [cp-parts (string/split init-cp #":")
;; When running from the packaged uberjar, its path is external to the
;; project and tools.deps deprecates such :paths entries, so it's added as
;; a :local/root dep instead. In dev the classpath is the project's own
;; source dirs and resolved deps, which belong in :paths as before.
;; "." is kept for clojure.java.io/resource.
{jars true, dirs false} (if dev?
{false cp-parts}
(group-by #(string/ends-with? % ".jar") cp-parts))
paths (into ["."] dirs)
self-deps (into {} (map-indexed (fn [i jar]
[(symbol "deps-try.self" (str "cp" i))
{:local/root jar}])
jars))
deps (merge
{'org.clojure/clojure {:mvn/version "1.12.0-alpha11"}}
{'org.clojure/clojure {:mvn/version "1.12.5"}}
self-deps
recipe-deps
requested-deps)
main-args (cond-> ["--version" version]
recipe-location (into (if ns-only
["--recipe-ns" recipe-location]
["--recipe" recipe-location]))
prepare (conj "-P"))]
(apply run-repl "-Sdeps" (str {:paths paths
prepare (conj "-P"))
;; Flags allowing the JVMTI agent to be loaded (needed for breaks on
;; Java 20+, see eval.deps-try.util.jvmti). The -XX flag (which also
;; suppresses the JEP 451 warning) only exists on Java 9+; Java 8
;; refuses to boot on unrecognized -XX options, so skip it there.
java-major (repl-java-version)
jvm-flags (cond-> ["-J-Djdk.attach.allowAttachSelf"]
(or (nil? java-major) (>= java-major 9))
(conj "-J-XX:+EnableDynamicAgentLoading"))]
(apply run-repl
(concat jvm-flags
["-Sdeps" (str {:paths paths
:deps deps})
"-M" "-m" "eval.deps-try.try" main-args)))
"-M" "-m" "eval.deps-try.try"]
main-args))))

(defn- recipe-manifest-contents [{:keys [refresh] :as _cli-opts}]
(let [remote-manifest-file "https://raw.githubusercontent.com/eval/deps-try/master/recipes/manifest.edn"
Expand Down
3 changes: 3 additions & 0 deletions src/eval/deps_try/deps.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,9 @@
(defn deps-available []
(->> (clojure.java.basis/current-basis)
:libs
;; synthetic deps used to put the packaged uberjar on the classpath
;; (see eval.deps-try/start-repl!) shouldn't show as user libraries
(remove (comp #{"deps-try.self"} namespace key))
(filter (comp #(every? empty? %) :parents val)))))

#?(:bb nil
Expand Down
Loading
Loading