Skip to content

Commit

Permalink
Use URLClassLoader for pulled tool jar loading
Browse files Browse the repository at this point in the history
  • Loading branch information
gayaldassanayake committed Jun 8, 2023
1 parent 84ad828 commit 92f12b3
Show file tree
Hide file tree
Showing 4 changed files with 124 additions and 60 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,83 @@
*/
package io.ballerina.cli.launcher;

import io.ballerina.cli.cmd.CommandUtil;
import io.ballerina.projects.BalToolsManifest;
import io.ballerina.projects.BalToolsToml;
import io.ballerina.projects.internal.BalToolsManifestBuilder;
import io.ballerina.runtime.api.values.BError;
import org.wso2.ballerinalang.util.RepoUtils;

import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;

import static io.ballerina.cli.cmd.Constants.ADD_COMMAND;
import static io.ballerina.cli.cmd.Constants.ASYNCAPI_COMMAND;
import static io.ballerina.cli.cmd.Constants.BINDGEN_COMMAND;
import static io.ballerina.cli.cmd.Constants.BUILD_COMMAND;
import static io.ballerina.cli.cmd.Constants.CLEAN_COMMAND;
import static io.ballerina.cli.cmd.Constants.DEPRECATE_COMMAND;
import static io.ballerina.cli.cmd.Constants.DIST_COMMAND;
import static io.ballerina.cli.cmd.Constants.DIST_TOOL_TOML_PREFIX;
import static io.ballerina.cli.cmd.Constants.DOC_COMMAND;
import static io.ballerina.cli.cmd.Constants.FORMAT_COMMAND;
import static io.ballerina.cli.cmd.Constants.GRAPHQL_COMMAND;
import static io.ballerina.cli.cmd.Constants.GRAPH_COMMAND;
import static io.ballerina.cli.cmd.Constants.GRPC_COMMAND;
import static io.ballerina.cli.cmd.Constants.HELP_COMMAND;
import static io.ballerina.cli.cmd.Constants.HOME_COMMAND;
import static io.ballerina.cli.cmd.Constants.INIT_COMMAND;
import static io.ballerina.cli.cmd.Constants.NEW_COMMAND;
import static io.ballerina.cli.cmd.Constants.OPENAPI_COMMAND;
import static io.ballerina.cli.cmd.Constants.PACK_COMMAND;
import static io.ballerina.cli.cmd.Constants.PERSIST_COMMAND;
import static io.ballerina.cli.cmd.Constants.PULL_COMMAND;
import static io.ballerina.cli.cmd.Constants.PUSH_COMMAND;
import static io.ballerina.cli.cmd.Constants.RUN_COMMAND;
import static io.ballerina.cli.cmd.Constants.SEARCH_COMMAND;
import static io.ballerina.cli.cmd.Constants.SEMVER_COMMAND;
import static io.ballerina.cli.cmd.Constants.SHELL_COMMAND;
import static io.ballerina.cli.cmd.Constants.START_DEBUG_ADAPTER_COMMAND;
import static io.ballerina.cli.cmd.Constants.START_LANG_SERVER_COMMAND;
import static io.ballerina.cli.cmd.Constants.TEST_COMMAND;
import static io.ballerina.cli.cmd.Constants.TOML_EXT;
import static io.ballerina.cli.cmd.Constants.TOOL_COMMAND;
import static io.ballerina.cli.cmd.Constants.UPDATE_COMMAND;
import static io.ballerina.cli.cmd.Constants.VERSION_COMMAND;
import static io.ballerina.projects.util.ProjectConstants.BALA_DIR_NAME;
import static io.ballerina.projects.util.ProjectConstants.CENTRAL_REPOSITORY_CACHE_NAME;
import static io.ballerina.projects.util.ProjectConstants.CONFIG_DIR;
import static io.ballerina.projects.util.ProjectConstants.HOME_REPO_DEFAULT_DIRNAME;
import static io.ballerina.projects.util.ProjectConstants.REPOSITORIES_DIR;
import static org.wso2.ballerinalang.programfile.ProgramFileConstants.ANY_PLATFORM;

/**
* Contains utility methods for executing a Ballerina program.
*
* @since 0.8.0
*/
public class LauncherUtils {
private static final String TOOL = "tool";
public static final String LIBS = "libs";

private static final List<String> coreCommands = Arrays.asList(
BUILD_COMMAND, RUN_COMMAND, TEST_COMMAND, DOC_COMMAND, PACK_COMMAND);
private static final List<String> packageCommands = Arrays.asList(NEW_COMMAND, ADD_COMMAND, PULL_COMMAND,
PUSH_COMMAND, SEARCH_COMMAND, SEMVER_COMMAND, GRAPH_COMMAND, DEPRECATE_COMMAND);
private static final List<String> otherCommands = Arrays.asList(CLEAN_COMMAND, FORMAT_COMMAND, GRPC_COMMAND,
GRAPHQL_COMMAND, OPENAPI_COMMAND, ASYNCAPI_COMMAND, PERSIST_COMMAND, BINDGEN_COMMAND, SHELL_COMMAND,
VERSION_COMMAND);
private static final List<String> hiddenCommands = Arrays.asList(INIT_COMMAND, TOOL_COMMAND, DIST_COMMAND,
UPDATE_COMMAND, START_LANG_SERVER_COMMAND, START_DEBUG_ADAPTER_COMMAND, HELP_COMMAND, HOME_COMMAND);

public static Path getSourceRootPath(String sourceRoot) {
// Get source root path.
Expand Down Expand Up @@ -94,34 +155,45 @@ static String makeFirstLetterLowerCase(String s) {
return new String(c);
}

static String wrapString(String str, int wrapLength, int indent) {
StringBuilder wrappedStr = new StringBuilder();
int i = 0;
while (i < str.length()) {
if (Character.isWhitespace(str.charAt(i))) {
i++;
continue;
}
if (i > 0) {
wrappedStr.append("\n");
wrappedStr.append(" ".repeat(indent));
}
int lineEnd = Math.min(i + wrapLength, str.length());
if (lineEnd < str.length() && !Character.isWhitespace(str.charAt(lineEnd))) {
// find the last whitespace character before the maximum line length
int lastWhitespace = str.lastIndexOf(' ', lineEnd);
if (lastWhitespace > i) {
lineEnd = lastWhitespace;
static boolean isToolCommand(String commandName) {
// TODO: if openapi was to be pushed as a tool, here it will be ignored and openapi in distribution will be used
// instead. Need to look into possible solutions.
return Stream.of(coreCommands, packageCommands, otherCommands, hiddenCommands)
.flatMap(List::stream).noneMatch(commandName::equals);
}

static List<File> getToolCommandJarAndDependenciesPath(String commandName) {
Path userHomeDirPath = Path.of(System.getProperty(CommandUtil.USER_HOME), HOME_REPO_DEFAULT_DIRNAME);

String distSpecificToolsTomlName = DIST_TOOL_TOML_PREFIX + RepoUtils.getBallerinaShortVersion() + TOML_EXT;
Path distSpecificToolsTomlPath = userHomeDirPath.resolve(Path.of(CONFIG_DIR, distSpecificToolsTomlName));

Path centralBalaDirPath = userHomeDirPath.resolve(
Path.of(REPOSITORIES_DIR, CENTRAL_REPOSITORY_CACHE_NAME, BALA_DIR_NAME));
BalToolsToml distSpecificToolsToml = BalToolsToml.from(distSpecificToolsTomlPath);
BalToolsManifest distSpecificToolsManifest = BalToolsManifestBuilder.from(distSpecificToolsToml).build();

if (distSpecificToolsManifest.tools().containsKey(commandName)) {
BalToolsManifest.Tool tool = distSpecificToolsManifest.tools().get(commandName);
File libsDir = centralBalaDirPath.resolve(
Path.of(tool.org(), tool.name(), tool.version(), ANY_PLATFORM, TOOL, LIBS)).toFile();
return findJarFiles(libsDir);
}
throw LauncherUtils.createUsageExceptionWithHelp("unknown command '" + commandName + "'");
}

static List<File> findJarFiles(File directory) {
List<File> jarFiles = new ArrayList<>();
if (directory.isDirectory()) {
File[] files = directory.listFiles();
if (files != null) {
for (File file : files) {
if (file.isFile() && file.getName().toLowerCase().endsWith(".jar")) {
jarFiles.add(file);
}
}
}
wrappedStr.append(str, i, lineEnd);
i = lineEnd;
// skip any whitespace characters at the beginning of the next line
while (i < str.length() && Character.isWhitespace(str.charAt(i))) {
i++;
}
}
return wrappedStr.toString();
return jarFiles;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,12 @@
import org.ballerinalang.compiler.BLangCompilerException;
import picocli.CommandLine;

import java.io.File;
import java.io.InputStream;
import java.io.PrintStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
Expand Down Expand Up @@ -83,7 +87,27 @@ private static Optional<BLauncherCmd> getInvokedCmd(String... args) {
cmdParser.setStopAtPositional(true);

// loading additional commands via SPI
ServiceLoader<BLauncherCmd> bCmds = ServiceLoader.load(BLauncherCmd.class);
ServiceLoader<BLauncherCmd> bCmds;
if (null != args && args.length > 0 && LauncherUtils.isToolCommand(args[0])) {
List<File> jars = LauncherUtils.getToolCommandJarAndDependenciesPath(args[0]);
URL[] urls = jars.stream()
.map(file -> {
try {
return file.toURI().toURL();
} catch (MalformedURLException e) {
throw LauncherUtils.createUsageExceptionWithHelp("invalid tool jar : " + file
.getAbsolutePath());
}
})
.toArray(URL[]::new);
// Combine custom class loader with system class loader
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
ClassLoader combinedClassLoader = new URLClassLoader(urls, systemClassLoader);
bCmds = ServiceLoader.load(BLauncherCmd.class, combinedClassLoader);
} else {
bCmds = ServiceLoader.load(BLauncherCmd.class);
}

for (BLauncherCmd bCmd : bCmds) {
cmdParser.addSubcommand(bCmd.getName(), bCmd);
bCmd.setParentCmdParser(cmdParser);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1163,6 +1163,7 @@ public static boolean isProjectEmpty(Project project) {
* Given a list of patterns in include field, find the directories and files in the package that match the patterns.
*
* @param patterns list of string patterns to be matched
* @param packageRoot package root
* @return the list of matching paths
*/
public static List<Path> getPathsMatchingIncludePatterns(List<String> patterns, Path packageRoot) {
Expand Down
33 changes: 0 additions & 33 deletions distribution/zip/jballerina/bin/bal
Original file line number Diff line number Diff line change
Expand Up @@ -195,39 +195,6 @@ do
BALLERINA_CLASSPATH="$BALLERINA_CLASSPATH":$j
done

# Add bal tool jar files to the class path
bal_version_file="$HOME/.ballerina/ballerina-version"
if [ -e "$bal_version_file" ]; then
bal_version=$(sed -n '1s/[^-]*-\(.*\)/\1/p' $bal_version_file)
BAL_TOOLS_FILE="$HOME/.ballerina/.config/dist-$bal_version.toml"
if [ -e "$BAL_TOOLS_FILE" ]; then
while IFS='=' read -r line; do
if echo "$line" | grep -q '\[.*tool.*\]'; then
read -r line
while [ ! -z "$line" ] && ! echo "$line" | grep -q '\[.*tool.*\]'; do
if echo "$line" | grep -q "org ="; then
org=$(echo "$line" | cut -d '"' -f 2)
fi
if echo "$line" | grep -q "name ="; then
name=$(echo "$line" | cut -d '"' -f 2)
fi
if echo "$line" | grep -q "version ="; then
version=$(echo "$line" | cut -d '"' -f 2)
fi
read -r line
done
if [ ! -z "$org" ] && [ ! -z "$name" ] && [ ! -z "$version" ]; then
for jar_file in "$HOME/.ballerina/repositories/central.ballerina.io/bala/$org/$name/$version/any/tool/libs"/*.jar; do
BALLERINA_CLASSPATH="$BALLERINA_CLASSPATH":"$jar_file"
done
fi
org=""
name=""
fi
done < "$BAL_TOOLS_FILE"
fi
fi

# For Cygwin, switch paths to Windows format before running java
if $cygwin; then
JAVA_HOME=`cygpath --absolute --windows "$JAVA_HOME"`
Expand Down

0 comments on commit 92f12b3

Please sign in to comment.