Skip to content
Open
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
82 changes: 82 additions & 0 deletions extension/ruby/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import org.gradle.language.jvm.tasks.ProcessResources

plugins {
`java-library`
}

base {
archivesName.set("${property("mod_id")}-ruby")
}

java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(property("java_version").toString().toInt()))
}
withSourcesJar()
}

repositories {
mavenCentral()
}

// Get minecraft version from stonecutter.active file
val minecraftVersion = rootProject.file("stonecutter.active").takeIf { it.exists() }
?.readText()?.trim()?.ifEmpty { null }
?: throw GradleException("stonecutter.active is empty; set an active version first")

// Configuration for runtime dependencies to embed in the extension jar
val embedDeps by configurations.creating {
isCanBeResolved = true
isCanBeConsumed = false
}

dependencies {
// Depends on extension module
implementation(project(":extension"))

// Compile against shared common code
compileOnly(project(":common:${minecraftVersion}"))
compileOnly("org.jetbrains:annotations:20.1.0")

// JRuby runtime - embedded into the extension jar so users don't need it on the classpath
implementation("org.jruby:jruby-complete:9.4.5.0")
add(embedDeps.name, "org.jruby:jruby-complete:9.4.5.0")

// Common library dependencies, google deps must align with neoforged
implementation("com.google.guava:guava:31.1-jre")
implementation("com.google.code.gson:gson:2.10")
implementation("org.slf4j:slf4j-api:2.0.16")

// Test dependencies
testImplementation(project(":extension"))
testImplementation(project(":common:${minecraftVersion}"))
testImplementation("org.junit.jupiter:junit-jupiter-api:5.8.1")
testImplementation("org.jetbrains:annotations:20.1.0")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.8.1")
}

// Collect embedded dependency paths for the json file
fun getEmbeddedDepPaths(): String =
embedDeps.files.joinToString(", ") { file ->
"\"META-INF/jsmacroscedeps/${file.name}\""
}

// Process resources to expand dependencies placeholder
tasks.named<ProcessResources>("processResources") {
filesMatching("jsmacrosce.ext.jruby.json") {
expand(mapOf("dependencies" to getEmbeddedDepPaths()))
}
}

// Embed dependencies into the extension jar
tasks.named<Jar>("jar") {
dependsOn(embedDeps)
from(embedDeps) {
into("META-INF/jsmacroscedeps")
}
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}

tasks.test {
useJUnitPlatform()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package com.jsmacrosce.jsmacros.jruby.client;

import com.google.common.collect.Sets;
import org.jruby.RubyException;
import org.jruby.embed.EvalFailedException;
import org.jruby.embed.ScriptingContainer;
import org.jruby.exceptions.RaiseException;
import org.jruby.runtime.backtrace.RubyStackTraceElement;
import org.jruby.runtime.builtin.IRubyObject;
import com.jsmacrosce.jsmacros.core.Core;
import com.jsmacrosce.jsmacros.core.extensions.LanguageExtension;
import com.jsmacrosce.jsmacros.core.extensions.LibraryExtension;
import com.jsmacrosce.jsmacros.core.language.BaseLanguage;
import com.jsmacrosce.jsmacros.core.language.BaseWrappedException;
import com.jsmacrosce.jsmacros.core.library.BaseLibrary;
import com.jsmacrosce.jsmacros.jruby.language.impl.JRubyLanguageDefinition;
import com.jsmacrosce.jsmacros.jruby.library.impl.FWrapper;

import java.io.File;
import java.util.Arrays;
import java.util.Set;

public class JRubyExtension implements LanguageExtension, LibraryExtension {

private static JRubyLanguageDefinition languageDefinition;

@Override
public String getExtensionName() {
return "jruby";
}

@Override
public void init(Core<?, ?> runner) {
Thread t = new Thread(() -> {
ScriptingContainer instance = new ScriptingContainer();
instance.runScriptlet("p \"Ruby Pre-Loaded\"");
instance.terminate();
}, "JRuby-Preload");
t.setDaemon(true);
t.start();
}

@Override
public int getPriority() {
return 0;
}

@Override
public ExtMatch extensionMatch(File file) {
if (file.getName().endsWith(".rb")) {
if (file.getName().contains(getExtensionName())) {
return ExtMatch.MATCH_WITH_NAME;
} else {
return ExtMatch.MATCH;
}
}
return ExtMatch.NOT_MATCH;
}

@Override
public String defaultFileExtension() {
return "rb";
}

@Override
public synchronized BaseLanguage<?, ?> getLanguage(Core<?, ?> runner) {
if (languageDefinition == null) {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
Thread.currentThread().setContextClassLoader(JRubyExtension.class.getClassLoader());
try {
languageDefinition = new JRubyLanguageDefinition(this, runner);
} finally {
Thread.currentThread().setContextClassLoader(classLoader);
}
}
return languageDefinition;
}

@Override
public Set<Class<? extends BaseLibrary>> getLibraries() {
return Sets.newHashSet(FWrapper.class);
}

@Override
public BaseWrappedException<?> wrapException(Throwable ex) {
if (!(ex instanceof EvalFailedException)) return null;
Throwable cause = ex.getCause();
if (cause instanceof RaiseException) {
RubyException e = ((RaiseException) cause).getException();
StackTraceElement[] frames = Arrays.stream(e.getBacktraceElements())
.map(RubyStackTraceElement::asStackTraceElement)
.toArray(StackTraceElement[]::new);
return new BaseWrappedException<>(e, e.getMessageAsJavaString(), null, buildTrace(frames));
}
return new BaseWrappedException<>(cause, cause.getClass().getName() + ": " + cause.getMessage(), null, buildTrace(cause.getStackTrace()));
}

private BaseWrappedException<StackTraceElement> buildTrace(StackTraceElement[] frames) {
BaseWrappedException<StackTraceElement> head = null;
for (int i = frames.length - 1; i >= 0; i--) {
StackTraceElement frame = frames[i];
String cls = frame.getClassName();
if ("org.jruby.embed.internal.EmbedEvalUnitImpl".equals(cls)) {
// upstream ran here — discard everything we've accumulated above it in the chain
head = null;
continue;
}
if (cls.startsWith("org.jruby")) continue;
BaseWrappedException.SourceLocation loc;
if ("RUBY".equals(cls)) {
String fileName = frame.getFileName();
loc = new BaseWrappedException.GuestLocation(
fileName != null ? new File(fileName) : null,
-1, -1, frame.getLineNumber(), -1);
} else {
loc = new BaseWrappedException.HostLocation(cls + " " + frame.getLineNumber());
}
head = new BaseWrappedException<>(frame, frame.getMethodName(), loc, head);
}
return head;
}

@Override
public boolean isGuestObject(Object o) {
return o instanceof IRubyObject;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package com.jsmacrosce.jsmacros.jruby.language.impl;

import org.jruby.embed.LocalContextScope;
import org.jruby.embed.ScriptingContainer;
import com.jsmacrosce.jsmacros.core.Core;
import com.jsmacrosce.jsmacros.core.config.ScriptTrigger;
import com.jsmacrosce.jsmacros.core.event.BaseEvent;
import com.jsmacrosce.jsmacros.core.language.BaseLanguage;
import com.jsmacrosce.jsmacros.core.language.EventContainer;
import com.jsmacrosce.jsmacros.jruby.client.JRubyExtension;

import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.Reader;
import java.io.StringReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;

public class JRubyLanguageDefinition extends BaseLanguage<ScriptingContainer, JRubyScriptContext> {
public JRubyLanguageDefinition(JRubyExtension extension, Core<?, ?> runner) {
super(extension, runner);
}

private void runInstance(EventContainer<JRubyScriptContext> ctx, BaseEvent event, ScriptletRunner scriptlet, @Nullable Path cwd) throws Exception {
ScriptingContainer instance = new ScriptingContainer(LocalContextScope.SINGLETHREAD);
ctx.getCtx().setContext(instance);

if (cwd != null) {
instance.setCurrentDirectory(cwd.toString());
}

retrieveLibs(ctx.getCtx()).forEach((name, lib) -> {
// "Time" is a built-in Ruby class; expose jsmacros' Time library under FTime instead.
String bindName = "Time".equals(name) ? "FTime" : name;
instance.put(bindName, lib);
});
instance.put("event", event);
instance.put("file", ctx.getCtx().getFile());
instance.put("context", ctx);

scriptlet.run(instance);
}

@Override
protected void exec(EventContainer<JRubyScriptContext> ctx, ScriptTrigger macro, BaseEvent event) throws Exception {
File file = ctx.getCtx().getFile();
runInstance(ctx, event, instance -> {
try (Reader reader = Files.newBufferedReader(file.toPath(), StandardCharsets.UTF_8)) {
instance.runScriptlet(reader, file.getAbsolutePath());
}
}, parentPathOf(file));
}

@Override
protected void exec(EventContainer<JRubyScriptContext> ctx, String lang, String script, BaseEvent event) throws Exception {
File file = ctx.getCtx().getFile();
runInstance(ctx, event, instance -> {
if (file != null) {
instance.runScriptlet(new StringReader(script), file.getAbsolutePath());
} else {
instance.runScriptlet(script);
}
}, parentPathOf(file));
}

@Override
public JRubyScriptContext createContext(BaseEvent event, File path) {
return new JRubyScriptContext(runner, event, path);
}

private static @Nullable Path parentPathOf(@Nullable File f) {
if (f == null) return null;
File parent = f.getParentFile();
return parent != null ? parent.toPath() : null;
}

private interface ScriptletRunner {
void run(ScriptingContainer instance) throws Exception;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.jsmacrosce.jsmacros.jruby.language.impl;

import org.jruby.embed.ScriptingContainer;
import com.jsmacrosce.jsmacros.core.Core;
import com.jsmacrosce.jsmacros.core.event.BaseEvent;
import com.jsmacrosce.jsmacros.core.language.BaseScriptContext;

import java.io.File;

public class JRubyScriptContext extends BaseScriptContext<ScriptingContainer> {
public JRubyScriptContext(Core<?, ?> runner, BaseEvent event, File file) {
super(runner, event, file);
}

@Override
public synchronized void closeContext() {
super.closeContext();
ScriptingContainer ctx = getContext();
if (ctx != null) {
ctx.terminate();
}
}

@Override
public boolean isMultiThreaded() {
return true;
}

}
Loading