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
1 change: 1 addition & 0 deletions CHANGELOG.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Release with new features and bugfixes:
* https://github.com/devonfw/IDEasy/issues/2251[#2251]: Provide generic uninstall support for globally installed tools (windows)
* https://github.com/devonfw/IDEasy/issues/1135[#1135]: Fix PowerShell env variable initialization on Windows by sourcing functions from the PowerShell profile
* https://github.com/devonfw/IDEasy/issues/741[#741]: Add a warning message for legacy devonfw-ide settings users
* https://github.com/devonfw/IDEasy/issues/1548[#1548]: Use version-specific plugin directories to avoid Windows file-lock failures during IDE updates

The full list of changes for this release can be found in https://github.com/devonfw/IDEasy/milestone/49?closed=1[milestone 2026.08.002].

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,17 +45,20 @@ public IdeaBasedIdeToolCommandlet(IdeContext context, String tool, Set<Tag> tags
}

@Override
public boolean installPlugin(ToolPluginDescriptor plugin, final Step step, ProcessContext pc) {
public boolean installPlugin(ToolPluginDescriptor plugin, Step step, ProcessContext pc) {

if (plugin.url() == null) {
IdeaPluginDownloader pluginDownloader = new IdeaPluginDownloader(this.context, this);
return pluginDownloader.installPlugin(plugin, step, pc);
}

// In case of plugins with a custom repo url
boolean customRepo = plugin.url() != null;
List<String> args = new ArrayList<>();
args.add("installPlugins");
args.add(plugin.id().replace("+", " "));
if (customRepo) {
args.add(plugin.url());
}
args.add(plugin.url());

ProcessResult result = runTool(pc, ProcessMode.DEFAULT, args);

if (result.isSuccessful()) {
IdeLogLevel.SUCCESS.log(LOG, "Successfully installed plugin: {}", plugin.name());
step.success();
Expand Down Expand Up @@ -88,23 +91,35 @@ public ProcessResult runTool(ProcessContext pc, ProcessMode processMode, List<St

String variableName = getName().toUpperCase(Locale.ROOT).replace("-", "_") + VM_ARGS_ENV_SUFFIX;
String userVmArgsContent = this.context.getVariables().get(variableName);
if (userVmArgsContent == null || userVmArgsContent.isEmpty()) {
return super.runTool(pc, processMode, args);

String[] userVmArgs = new String[0];
if ((userVmArgsContent != null) && !userVmArgsContent.isEmpty()) {
userVmArgs = userVmArgsContent.trim().split("\\s+");
}
String[] userVmArgs = userVmArgsContent.trim().split("\\s+");

String prefix = getIdeProductPrefix();
Path defaultVmOptionsPath = resolveDefaultVmOptionsPath(this.getToolPath(), prefix);
if ((prefix == null) || (defaultVmOptionsPath == null)) {
return super.runTool(pc, processMode, args);
}

String defaultVmArgsContent = this.context.getFileAccess().readFileContent(defaultVmOptionsPath);
if (defaultVmArgsContent == null || defaultVmArgsContent.isEmpty()) {
LOG.debug("Default {} jvm options not found at: {}", getName(), defaultVmOptionsPath);
return super.runTool(pc, processMode, args);
}

String[] defaultVmArgs = defaultVmArgsContent.trim().split("\\s+");
String pluginsPathArg = "-Didea.plugins.path="
+ getPluginsInstallationPath().toAbsolutePath();
String[] additionalVmArgs = new String[userVmArgs.length + 1];
System.arraycopy(userVmArgs, 0, additionalVmArgs, 0, userVmArgs.length);
additionalVmArgs[userVmArgs.length] = pluginsPathArg;

String userOptionsFileName = "." + prefix + VM_OPTIONS_FILE_EXTENSION;
Path confPath = this.context.getWorkspacePath().resolve(userOptionsFileName);
this.context.getFileAccess().writeFileContent(mergeVmArgs(defaultVmArgs, userVmArgs), confPath, true);

this.context.getFileAccess().writeFileContent(mergeVmArgs(defaultVmArgs, additionalVmArgs), confPath, true);

pc.withEnvVar(prefix.toUpperCase() + VM_OPTIONS_ENV_SUFFIX, confPath.toAbsolutePath().toString());
return super.runTool(pc, processMode, args);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,20 @@
import com.devonfw.tools.ide.tool.plugin.ToolPluginDescriptor;

/**
* Used for a direct download and installation of idea plugins
* Used for a direct download and installation of idea plugins.
*/
public class IdeaPluginDownloader {

private static final Logger LOG = LoggerFactory.getLogger(IdeaPluginDownloader.class);

private static final String BUILD_FILE = "build.txt";

private final IdeContext context;

private final IdeaBasedIdeToolCommandlet commandlet;

/**
* the constructor
* The constructor.
*
* @param context the {@link IdeContext}.
* @param commandlet the {@link IdeaBasedIdeToolCommandlet} to use.
Expand All @@ -50,11 +52,11 @@ public IdeaPluginDownloader(IdeContext context, IdeaBasedIdeToolCommandlet comma
* @param plugin the {@link ToolPluginDescriptor} to install.
* @param step the {@link Step} for the plugin installation.
* @param pc the {@link ProcessContext} to use.
* @return boolean {@code true} if successful installed, {@code false} if not.
* @return boolean {@code true} if successfully installed, {@code false} otherwise.
*/
public boolean installPlugin(ToolPluginDescriptor plugin, Step step, ProcessContext pc) {
String downloadUrl = getDownloadUrl(plugin);

String downloadUrl = getDownloadUrl(plugin);
String pluginId = plugin.id();

Path tmpDir = null;
Expand All @@ -63,111 +65,176 @@ public boolean installPlugin(ToolPluginDescriptor plugin, Step step, ProcessCont
Path installationPath = this.commandlet.getPluginsInstallationPath();
ensureInstallationPathExists(installationPath);

FileAccess fileAccess = context.getFileAccess();
FileAccess fileAccess = this.context.getFileAccess();
tmpDir = fileAccess.createTempDir(pluginId);

Path downloadedFile = downloadPlugin(fileAccess, downloadUrl, tmpDir, pluginId);
extractDownloadedPlugin(fileAccess, downloadedFile, pluginId);
extractDownloadedPlugin(fileAccess, downloadedFile, pluginId, installationPath);

step.success();
return true;

} catch (IOException e) {

step.error(e);
throw new IllegalStateException("Failed to process installation of plugin: " + pluginId, e);

} finally {

if (tmpDir != null) {
context.getFileAccess().delete(tmpDir);
this.context.getFileAccess().delete(tmpDir);
}
}
}

/**
* @param plugin the {@link ToolPluginDescriptor} to be installer
* @param plugin the {@link ToolPluginDescriptor} to be installed.
* @return a {@link String} representing the download URL.
*/
private String getDownloadUrl(ToolPluginDescriptor plugin) {

String downloadUrl = plugin.url();
String pluginId = URLEncoder.encode(plugin.id(), StandardCharsets.UTF_8).replaceAll("\\+", "%20");

String pluginId = URLEncoder.encode(plugin.id(), StandardCharsets.UTF_8)
.replaceAll("\\+", "%20");

String buildVersion = readBuildVersion();

if (downloadUrl == null || downloadUrl.isEmpty()) {
downloadUrl = String.format("https://plugins.jetbrains.com/pluginManager?action=download&id=%s&build=%s", pluginId, buildVersion);
if ((downloadUrl == null) || downloadUrl.isEmpty()) {
downloadUrl = String.format(
"https://plugins.jetbrains.com/pluginManager?action=download&id=%s&build=%s",
pluginId,
buildVersion);
}

return downloadUrl;
}

private String readBuildVersion() {

Path buildFile = this.commandlet.getToolPath().resolve(BUILD_FILE);
if (context.getSystemInfo().isMac()) {
MacOsHelper macOsHelper = new MacOsHelper(context);
Path appPath = macOsHelper.findAppDir(macOsHelper.findRootToolPath(this.commandlet, context));

if (this.context.getSystemInfo().isMac()) {
MacOsHelper macOsHelper = new MacOsHelper(this.context);
Path appPath = macOsHelper.findAppDir(
macOsHelper.findRootToolPath(this.commandlet, this.context));

buildFile = appPath.resolve("Contents/Resources").resolve(BUILD_FILE);
}

try {
return Files.readString(buildFile);
} catch (IOException e) {
throw new IllegalStateException("Failed to read " + this.commandlet.getName() + " build version: " + buildFile, e);
throw new IllegalStateException(
"Failed to read " + this.commandlet.getName() + " build version: " + buildFile,
e);
}
}

private void ensureInstallationPathExists(Path installationPath) throws IOException {

if (!Files.exists(installationPath)) {
try {
Files.createDirectories(installationPath);
} catch (IOException e) {
throw new IllegalStateException("Failed to create directory " + installationPath, e);
throw new IllegalStateException(
"Failed to create directory " + installationPath,
e);
}
}
}

private Path downloadPlugin(FileAccess fileAccess, String downloadUrl, Path tmpDir, String pluginId) throws IOException {
private Path downloadPlugin(FileAccess fileAccess, String downloadUrl, Path tmpDir,
String pluginId) throws IOException {

String extension = getFileExtensionFromUrl(downloadUrl);

if (extension.isEmpty()) {
throw new IllegalStateException("Unknown file type for URL: " + downloadUrl);
}
String fileName = String.format("%s-plugin-%s%s", this.commandlet.getName(), pluginId, extension);

String fileName = String.format(
"%s-plugin-%s%s",
this.commandlet.getName(),
pluginId,
extension);

Path downloadedFile = tmpDir.resolve(fileName);

fileAccess.download(downloadUrl, downloadedFile);

return downloadedFile;
}

private void extractDownloadedPlugin(FileAccess fileAccess, Path downloadedFile, String pluginId) throws IOException {
Path targetDir = this.commandlet.getPluginsInstallationPath().resolve(pluginId);
if (Files.exists(targetDir)) {
LOG.info("Plugin already installed, target directory already existing: {}", targetDir);
private void extractDownloadedPlugin(FileAccess fileAccess, Path downloadedFile,
String pluginId, Path installationPath) throws IOException {

String fileName = downloadedFile.getFileName().toString();

if (fileName.endsWith(".zip")) {
// Marketplace ZIP already contains the actual plugin root folder,
// e.g. plantuml4idea/lib/...
fileAccess.extractZip(downloadedFile, installationPath);

} else if (fileName.endsWith(".jar")) {
// A standalone plugin JAR has no surrounding plugin directory.
Path targetDir = installationPath.resolve(pluginId);

if (Files.exists(targetDir)) {
LOG.info(
"Plugin already installed, target directory already existing: {}",
targetDir);
} else {
fileAccess.extractJar(downloadedFile, targetDir);
}

} else {
fileAccess.extract(downloadedFile, targetDir);
throw new IllegalStateException(
"Unsupported plugin archive: " + downloadedFile);
}
}

private String getFileExtensionFromUrl(String urlString) throws RuntimeException {

URI uri = null;
HttpRequest request;

try (HttpClient client = HttpClientFactory.create()) {

uri = URI.create(urlString);
request = HttpRequest.newBuilder().uri(uri)
.method("HEAD", HttpRequest.BodyPublishers.noBody()).timeout(Duration.ofSeconds(5)).build();

HttpResponse<?> res = client.send(request, HttpResponse.BodyHandlers.ofString());
HttpRequest request = HttpRequest.newBuilder()
.uri(uri)
.method("HEAD", HttpRequest.BodyPublishers.noBody())
.timeout(Duration.ofSeconds(5))
.build();

HttpResponse<?> response = client.send(
request,
HttpResponse.BodyHandlers.ofString());

int responseCode = response.statusCode();

int responseCode = res.statusCode();
if (responseCode != HttpURLConnection.HTTP_OK) {
throw new RuntimeException("Failed to fetch file headers: HTTP " + responseCode);
throw new RuntimeException(
"Failed to fetch file headers: HTTP " + responseCode);
}

Optional<String> contentType = res.headers().firstValue("content-type");
Optional<String> contentType = response.headers().firstValue("content-type");

if (contentType.isEmpty()) {
return "";
}

return switch (contentType.get()) {
case "application/zip" -> ".zip";
case "application/java-archive" -> ".jar";
default -> "";
};

} catch (Exception e) {
throw new RuntimeException("Failed to perform HEAD request of URL " + uri, e);
throw new RuntimeException(
"Failed to perform HEAD request of URL " + uri,
e);
}
}
}
Loading