- Add JarFile-based plugin.json manifest reading and validation - Implement reflection-based plugin class instantiation (Data or no-arg constructor) - Wrap Tool objects into OllamaFunctionTool with basic parameter/response handling - Implement ToolArguments storage and ToolResponse record - Extract RPCP_SOURCE constant; add plugin name validation - Update Data interface: getLoadedPlugins() returns LoadedPlugin[] - Fix launcher package references (me.zacharias.chat → me.neurodock) TODO: Extract FileSystem from JAR, populate PluginMetadata, complete OllamaFunctionArgument↔ToolArguments wrapping, invoke plugin lifecycle hooks Signed-off-by: zacharias <alienfromdia@proton.me>
This commit is contained in:
@@ -3,6 +3,9 @@ package me.neurodock.core;
|
|||||||
import me.neurodock.core.memory.*;
|
import me.neurodock.core.memory.*;
|
||||||
import me.neurodock.ollama.*;
|
import me.neurodock.ollama.*;
|
||||||
import me.neurodock.ollama.exceptions.OllamaToolErrorException;
|
import me.neurodock.ollama.exceptions.OllamaToolErrorException;
|
||||||
|
import me.neurodock.plugin.Data;
|
||||||
|
import me.neurodock.plugin.LoadedPlugin;
|
||||||
|
import me.neurodock.plugin.Plugin;
|
||||||
import me.neurodock.plugin.loader.Loader;
|
import me.neurodock.plugin.loader.Loader;
|
||||||
import me.neurodock.plugin.exceptions.PluginLoadingException;
|
import me.neurodock.plugin.exceptions.PluginLoadingException;
|
||||||
import org.intellij.lang.annotations.MagicConstant;
|
import org.intellij.lang.annotations.MagicConstant;
|
||||||
@@ -15,14 +18,19 @@ import java.net.*;
|
|||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.FileSystem;
|
import java.nio.file.FileSystem;
|
||||||
import java.nio.file.FileSystems;
|
import java.nio.file.FileSystems;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.jar.JarEntry;
|
||||||
|
import java.util.jar.JarFile;
|
||||||
|
|
||||||
import static me.neurodock.ollama.OllamaFunctionArgument.deconstructOllamaFunctionArguments;
|
import static me.neurodock.ollama.OllamaFunctionArgument.deconstructOllamaFunctionArguments;
|
||||||
|
import static me.neurodock.plugin.Plugin.UNKNOWN_PLUGIN;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Main class for the System, responsible for managing the OllamaObject, tools, and the Ollama API.
|
* The Main class for the System, responsible for managing the OllamaObject, tools, and the Ollama API.
|
||||||
@@ -822,21 +830,65 @@ public class Core {
|
|||||||
if(files == null) {
|
if(files == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
LoaderPluginData data = new LoaderPluginData();
|
||||||
Loader loader = new Loader();
|
Loader loader = new Loader();
|
||||||
for(File file : files) {
|
for(File file : files) {
|
||||||
try(FileSystem fs = FileSystems.newFileSystem(file.toPath())){
|
try(JarFile jar = new JarFile(file)){
|
||||||
if(!fs.getPath("/plugin.json").toFile().exists())
|
JarEntry pluginJsonFile = jar.getJarEntry("/plugin.json");
|
||||||
|
if(pluginJsonFile == null)
|
||||||
{
|
{
|
||||||
throw new PluginLoadingException("Plugin does not contain a plugin.json file", file.getName());
|
throw new PluginLoadingException("Plugin does not contain a plugin.json file", file.getName());
|
||||||
}
|
}
|
||||||
//JSONObject pluginJson = new JSONObject(new String(fs.getPath("/plugin.json").toFile().readAllBytes()));
|
if(jar.getJarEntry("/me/neurodock/plugin/Plugin") != null)
|
||||||
//Plugin plugin = loader.loadPlugin(pluginJson, fs);
|
throw new PluginLoadingException("Plugin bundles NeuroDock API classes. Consider using compileOnly", file.getName());
|
||||||
|
StringBuilder pluginJsonData = new StringBuilder();
|
||||||
|
String tmp = null;
|
||||||
|
BufferedReader inReader = new BufferedReader(new InputStreamReader(jar.getInputStream(pluginJsonFile)));
|
||||||
|
while((tmp = inReader.readLine()) != null)
|
||||||
|
{
|
||||||
|
pluginJsonData.append(tmp);
|
||||||
|
}
|
||||||
|
JSONObject pluginJson = new JSONObject(pluginJsonData.toString());
|
||||||
|
LoadedPlugin plugin = loader.loadPlugin(pluginJson, file.toPath(), data);
|
||||||
|
|
||||||
|
data.addPlugin(plugin);
|
||||||
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static class LoaderPluginData extends Data {
|
||||||
|
|
||||||
|
ArrayList<LoadedPlugin> plugins = new ArrayList<>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public File getDataDictionary() {
|
||||||
|
return DATA_DIR;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LoadedPlugin[] getLoadedPlugins() {
|
||||||
|
return plugins.toArray(new LoadedPlugin[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public File getCacheDirectory() {
|
||||||
|
return CACHE_DIRECTORY;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addPlugin(LoadedPlugin plugin)
|
||||||
|
{
|
||||||
|
plugins.add(plugin);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removePlugin(LoadedPlugin plugin)
|
||||||
|
{
|
||||||
|
plugins.remove(plugin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents the source of a tool.
|
* Represents the source of a tool.
|
||||||
* <p>
|
* <p>
|
||||||
|
|||||||
@@ -3,14 +3,33 @@ package me.neurodock.plugin.loader;
|
|||||||
import jdk.jshell.spi.ExecutionControl;
|
import jdk.jshell.spi.ExecutionControl;
|
||||||
import me.neurodock.core.Pair;
|
import me.neurodock.core.Pair;
|
||||||
import me.neurodock.ollama.*;
|
import me.neurodock.ollama.*;
|
||||||
|
import me.neurodock.ollama.exceptions.OllamaToolErrorException;
|
||||||
|
import me.neurodock.plugin.Data;
|
||||||
|
import me.neurodock.plugin.LoadedPlugin;
|
||||||
import me.neurodock.plugin.Plugin;
|
import me.neurodock.plugin.Plugin;
|
||||||
import me.neurodock.plugin.PluginMetadata;
|
import me.neurodock.plugin.PluginMetadata;
|
||||||
|
import me.neurodock.plugin.exceptions.PluginLoadingException;
|
||||||
import me.neurodock.plugin.exceptions.ToolRuntimeException;
|
import me.neurodock.plugin.exceptions.ToolRuntimeException;
|
||||||
import me.neurodock.plugin.tool.Tool;
|
import me.neurodock.plugin.tool.Tool;
|
||||||
|
import me.neurodock.plugin.tool.ToolArguments;
|
||||||
import me.neurodock.plugin.tool.ToolResponse;
|
import me.neurodock.plugin.tool.ToolResponse;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.lang.reflect.Constructor;
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
import java.lang.reflect.Parameter;
|
||||||
|
import java.net.MalformedURLException;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.net.URLClassLoader;
|
||||||
|
import java.nio.file.FileSystem;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
import static me.neurodock.plugin.Plugin.UNKNOWN_PLUGIN;
|
||||||
|
|
||||||
public class Loader {
|
public class Loader {
|
||||||
private final ArrayList<Pair<Plugin, PluginMetadata>> plugins = new ArrayList<>();
|
private final ArrayList<Pair<Plugin, PluginMetadata>> plugins = new ArrayList<>();
|
||||||
@@ -19,11 +38,12 @@ public class Loader {
|
|||||||
* Mock example!
|
* Mock example!
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
public OllamaTool getTool() {
|
public OllamaTool[] getTools(Plugin plugin) {
|
||||||
// Mock example!
|
|
||||||
Tool tool = null;
|
|
||||||
|
|
||||||
return new OllamaFunctionTool() {
|
ArrayList<OllamaTool> tools = new ArrayList<>();
|
||||||
|
|
||||||
|
for(Tool tool : plugin.getTools()) {
|
||||||
|
tools.add(new OllamaFunctionTool() {
|
||||||
@Override
|
@Override
|
||||||
public @NotNull String name() {
|
public @NotNull String name() {
|
||||||
return tool.name();
|
return tool.name();
|
||||||
@@ -43,13 +63,18 @@ public class Loader {
|
|||||||
@Override
|
@Override
|
||||||
public @NotNull OllamaToolResponse function(OllamaFunctionArgument... args) {
|
public @NotNull OllamaToolResponse function(OllamaFunctionArgument... args) {
|
||||||
// Wrap OllamaFunctionArguments[] to ether ToolArgument[] or ToolArguments(current implementation)
|
// Wrap OllamaFunctionArguments[] to ether ToolArgument[] or ToolArguments(current implementation)
|
||||||
|
ToolArguments toolArgs = new ToolArguments();
|
||||||
|
for (OllamaFunctionArgument arg : args) {
|
||||||
|
toolArgs.addArgument(arg.argument(), arg.value());
|
||||||
|
}
|
||||||
|
ToolResponse toolResponse;
|
||||||
try {
|
try {
|
||||||
ToolResponse toolResponse = tool.callTool(null);
|
toolResponse = tool.callTool(toolArgs);
|
||||||
} catch (ToolRuntimeException e) {
|
} catch (ToolRuntimeException e) {
|
||||||
throw new RuntimeException(e);
|
throw new OllamaToolErrorException(name(), e);
|
||||||
}
|
}
|
||||||
// Wrap ToolResponce to an OllamaToolRespnce(Well I see a typo here now)
|
// Wrap ToolResponce to an OllamaToolRespnce(Well I see a typo here now)
|
||||||
return null;
|
return new OllamaToolResponse(toolResponse.name(), toolResponse.response());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -57,6 +82,66 @@ public class Loader {
|
|||||||
// Replaces the toString which is used thru out the Core and OllamaObject to get the JSON representation of a Function Tool
|
// Replaces the toString which is used thru out the Core and OllamaObject to get the JSON representation of a Function Tool
|
||||||
return tool.getToolJSON();
|
return tool.getToolJSON();
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return tools.toArray(tools.toArray(new OllamaTool[0]));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public LoadedPlugin loadPlugin(JSONObject pluginJson, Path pluginJar, Data data) {
|
||||||
|
if(!pluginJson.has("name") || !(pluginJson.get("name") instanceof String pluginName))
|
||||||
|
throw new PluginLoadingException("Malformed plugin json", UNKNOWN_PLUGIN);
|
||||||
|
if(!pluginJson.has("entryPoint") || !(pluginJson.get("entryPoint") instanceof String pluginEntryPoint))
|
||||||
|
throw new PluginLoadingException("Malformed plugin json", pluginName);
|
||||||
|
Path pluginEntryPointFile = fs.getPath("/"+pluginEntryPoint.replaceAll("\\.", "/")+".class");
|
||||||
|
if(!Files.exists(pluginEntryPointFile)) throw new PluginLoadingException("Missing plugin entrypoint", pluginName);
|
||||||
|
|
||||||
|
Plugin plugin = null;
|
||||||
|
URLClassLoader classLoader;
|
||||||
|
|
||||||
|
try {
|
||||||
|
classLoader = new URLClassLoader(
|
||||||
|
new URL[]{pluginJar.toUri().toURL()},
|
||||||
|
getClass().getClassLoader()
|
||||||
|
);
|
||||||
|
|
||||||
|
Class<?> pluginClass = classLoader.loadClass(pluginEntryPoint);
|
||||||
|
|
||||||
|
if(!Plugin.class.isAssignableFrom(pluginClass)) throw new PluginLoadingException(
|
||||||
|
"Entrypoint does not implement Plugin",
|
||||||
|
pluginName
|
||||||
|
);
|
||||||
|
|
||||||
|
Class<? extends Plugin> typedClass =
|
||||||
|
pluginClass.asSubclass(Plugin.class);
|
||||||
|
|
||||||
|
for(Constructor<?> constructor : typedClass.getDeclaredConstructors())
|
||||||
|
{
|
||||||
|
Parameter[] parameters = constructor.getParameters();
|
||||||
|
if(parameters.length == 1 &&
|
||||||
|
parameters[0].getType() == Data.class)
|
||||||
|
{
|
||||||
|
plugin = (Plugin) constructor.newInstance(data);
|
||||||
|
}
|
||||||
|
else if(parameters.length == 0)
|
||||||
|
{
|
||||||
|
plugin = (Plugin) constructor.newInstance();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}catch (ClassNotFoundException | InvocationTargetException | InstantiationException | IllegalAccessException | IOException e)
|
||||||
|
{
|
||||||
|
throw new PluginLoadingException("Failed to load plugin jar", e, pluginName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(plugin == null)
|
||||||
|
{
|
||||||
|
throw new PluginLoadingException(
|
||||||
|
"Missing proper constructor",
|
||||||
|
pluginName
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new LoadedPlugin(plugin, classLoader, null/*TODO: MUST BE REPLACED WITH ACTUAL PLUGIN METADATA! this is read from the PluginEntyPoint*/);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,6 @@ import java.io.File;
|
|||||||
|
|
||||||
public abstract class Data {
|
public abstract class Data {
|
||||||
public abstract File getDataDictionary();
|
public abstract File getDataDictionary();
|
||||||
public abstract LoadedPlugins getLoadedPlugins();
|
public abstract LoadedPlugin[] getLoadedPlugins();
|
||||||
public abstract File getCacheDirectory();
|
public abstract File getCacheDirectory();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package me.neurodock.plugin;
|
||||||
|
|
||||||
|
public record LoadedPlugin(
|
||||||
|
Plugin plugin,
|
||||||
|
ClassLoader loader,
|
||||||
|
PluginMetadata metaData
|
||||||
|
) {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
package me.neurodock.plugin;
|
|
||||||
|
|
||||||
public class LoadedPlugins {
|
|
||||||
// TODO: Implement
|
|
||||||
}
|
|
||||||
@@ -9,6 +9,10 @@ import org.jetbrains.annotations.NotNull;
|
|||||||
* Entry point for the plugin, this is required for the plugin to work, and you MUST annotate it with {@link PluginEntryPoint}
|
* Entry point for the plugin, this is required for the plugin to work, and you MUST annotate it with {@link PluginEntryPoint}
|
||||||
*/
|
*/
|
||||||
public abstract class Plugin {
|
public abstract class Plugin {
|
||||||
|
/**
|
||||||
|
* This is a magic constant plugin name used for when a plugin's name cant be loaded
|
||||||
|
*/
|
||||||
|
public static final String UNKNOWN_PLUGIN = "UNKNOWN PLUGIN";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This shuld be used if {@link PluginEntryPoint#requireData()} is false (default)
|
* This shuld be used if {@link PluginEntryPoint#requireData()} is false (default)
|
||||||
@@ -29,12 +33,12 @@ public abstract class Plugin {
|
|||||||
public void onInit(){}
|
public void onInit(){}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This will be called if the core gets a "fetal" exception or fails to fetch certain info
|
* This will be called if the core gets a "fetal" exception or fails to fetch certain info AND the plugin was able to be loaded.
|
||||||
*/
|
*/
|
||||||
public void onDisable(){}
|
public void onDisable(){}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This will be called after all plugins have been initialised; here it is safe to perform potential stuff where you load data from optional dependencies, or similar things
|
* This will be called after all plugins have been initialized; here it is safe to perform potential stuff where you load data from optional dependencies, or similar things
|
||||||
*/
|
*/
|
||||||
public void onEnable(){}
|
public void onEnable(){}
|
||||||
|
|
||||||
|
|||||||
@@ -9,5 +9,10 @@ import java.lang.annotation.Target;
|
|||||||
@Target(ElementType.TYPE)
|
@Target(ElementType.TYPE)
|
||||||
public @interface PluginEntryPoint {
|
public @interface PluginEntryPoint {
|
||||||
boolean requireData() default true;
|
boolean requireData() default true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NOT YET IMPLEMENTED!
|
||||||
|
* @return An array of plugins this plugin depends on
|
||||||
|
*/
|
||||||
String[] dependencies() default {};
|
String[] dependencies() default {};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,4 +4,9 @@ public class PluginLoadingException extends RuntimeException {
|
|||||||
public PluginLoadingException(String message, String pluginName) {
|
public PluginLoadingException(String message, String pluginName) {
|
||||||
super("Plugin: \""+pluginName+"\"> "+message);
|
super("Plugin: \""+pluginName+"\"> "+message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public PluginLoadingException(String message, Throwable cause, String pluginName)
|
||||||
|
{
|
||||||
|
super("Plugin: \""+pluginName+"\"> "+message, cause);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,11 @@ import org.jetbrains.annotations.NotNull;
|
|||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
|
||||||
public abstract class Tool {
|
public abstract class Tool {
|
||||||
|
/**
|
||||||
|
* Magic constant for RPCP sources.<br>
|
||||||
|
* This is solely here to replace the case of a magic string being used in the code
|
||||||
|
*/
|
||||||
|
public static final String RPCP_SOURCE = "RPCP";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The parent plugin of this tool, used by {@link Tool#getToolJSON()}.
|
* The parent plugin of this tool, used by {@link Tool#getToolJSON()}.
|
||||||
@@ -29,7 +34,7 @@ public abstract class Tool {
|
|||||||
ret.put("type", "function");
|
ret.put("type", "function");
|
||||||
|
|
||||||
JSONObject function = new JSONObject();
|
JSONObject function = new JSONObject();
|
||||||
function.put("name", String.format("%s_%s_RPCP", name(), PLUGIN.getMetadata().getName()).replaceAll(" ", "_"));
|
function.put("name", String.format("%s_%s_%s", name(), PLUGIN.getMetadata().getName(), RPCP_SOURCE).replaceAll(" ", "_"));
|
||||||
if(description() != null) {
|
if(description() != null) {
|
||||||
function.put("description", description());
|
function.put("description", description());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,30 @@
|
|||||||
package me.neurodock.plugin.tool;
|
package me.neurodock.plugin.tool;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.NoSuchElementException;
|
||||||
|
|
||||||
public class ToolArguments {
|
public class ToolArguments {
|
||||||
|
Map<String, Object> arguments = new HashMap<>();
|
||||||
|
|
||||||
|
public void addArguments(Map<String, Object> args)
|
||||||
|
{
|
||||||
|
arguments.putAll(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addArgument(String name, Object value)
|
||||||
|
{
|
||||||
|
arguments.put(name, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Object getArgument(String name)
|
||||||
|
{
|
||||||
|
if(!arguments.containsKey(name)) throw new NoSuchElementException("Trying to access missing argument");
|
||||||
|
return arguments.get(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasArgument(String name)
|
||||||
|
{
|
||||||
|
return arguments.containsKey(name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
package me.neurodock.plugin.tool;
|
package me.neurodock.plugin.tool;
|
||||||
|
|
||||||
public class ToolResponse {
|
public record ToolResponse(String name, String response) {
|
||||||
public static ToolResponse empty(Tool tool) {
|
public static ToolResponse empty(Tool tool) {
|
||||||
return new ToolResponse();
|
return new ToolResponse(tool.name(), "");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ToolResponse empty(String toolName)
|
public static ToolResponse empty(String toolName)
|
||||||
{
|
{
|
||||||
return new ToolResponse();
|
return new ToolResponse(toolName, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id 'java'
|
id 'java'
|
||||||
|
id 'application'
|
||||||
}
|
}
|
||||||
|
|
||||||
version = '1.0-SNAPSHOT'
|
version = '1.0-SNAPSHOT'
|
||||||
@@ -11,10 +12,14 @@ dependencies {
|
|||||||
|
|
||||||
jar{
|
jar{
|
||||||
manifest {
|
manifest {
|
||||||
attributes 'Main-Class': 'me.zacharias.chat.launcher.Launcher'
|
attributes 'Main-Class': 'me.neurodock.launcher.Launcher'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
application {
|
||||||
|
mainClass = 'me.neurodock.launcher.Launcher'
|
||||||
|
}
|
||||||
|
|
||||||
java {
|
java {
|
||||||
toolchain {
|
toolchain {
|
||||||
languageVersion.set(JavaLanguageVersion.of(javaVersion)) // Set Java version
|
languageVersion.set(JavaLanguageVersion.of(javaVersion)) // Set Java version
|
||||||
|
|||||||
Reference in New Issue
Block a user