diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CommandletManagerImpl.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CommandletManagerImpl.java
index 537196461e..1a18e534ae 100644
--- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CommandletManagerImpl.java
+++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CommandletManagerImpl.java
@@ -46,6 +46,7 @@
import com.devonfw.tools.ide.tool.kubectl.KubeCtl;
import com.devonfw.tools.ide.tool.lazydocker.LazyDocker;
import com.devonfw.tools.ide.tool.mvn.Mvn;
+import com.devonfw.tools.ide.tool.msvc.Msvc;
import com.devonfw.tools.ide.tool.nest.Nest;
import com.devonfw.tools.ide.tool.ng.Ng;
import com.devonfw.tools.ide.tool.node.Node;
@@ -56,6 +57,7 @@
import com.devonfw.tools.ide.tool.pycharm.Pycharm;
import com.devonfw.tools.ide.tool.python.Python;
import com.devonfw.tools.ide.tool.quarkus.Quarkus;
+import com.devonfw.tools.ide.tool.rust.Rust;
import com.devonfw.tools.ide.tool.sonar.Sonar;
import com.devonfw.tools.ide.tool.spring.Spring;
import com.devonfw.tools.ide.tool.squirrelsql.SquirrelSql;
@@ -126,12 +128,14 @@ public CommandletManagerImpl(IdeContext context) {
add(new Node(context));
add(new Npm(context));
add(new Mvn(context));
+ add(new Msvc(context));
add(new GcViewer(context));
add(new Gradle(context));
add(new Eclipse(context));
add(new Terraform(context));
add(new Oc(context));
add(new Quarkus(context));
+ add(new Rust(context));
add(new Kotlinc(context));
add(new KotlincNative(context));
add(new KubeCtl(context));
diff --git a/cli/src/main/java/com/devonfw/tools/ide/tool/LocalToolCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/tool/LocalToolCommandlet.java
index 0a5d043bda..1b286ac4ea 100644
--- a/cli/src/main/java/com/devonfw/tools/ide/tool/LocalToolCommandlet.java
+++ b/cli/src/main/java/com/devonfw/tools/ide/tool/LocalToolCommandlet.java
@@ -223,8 +223,8 @@ public ToolInstallation installTool(ToolInstallRequest request) {
}
/**
- * Performs the actual installation of the {@link #getName() tool} by downloading its binary, optionally extracting it, backing up any existing installation,
- * and writing the version file.
+ * Performs the installation of the {@link #getName() tool} by using {@link #onInstall(ToolInstallRequest, Path, Path)}
+ * for tool-specific logic, backing up any existing installation, and writing the version file.
*
* This method assumes that the version has already been resolved and dependencies installed. It handles the final steps of placing the tool into the
* appropriate installation directory.
@@ -237,11 +237,8 @@ protected void performToolInstallation(ToolInstallRequest request, Path installa
FileAccess fileAccess = this.context.getFileAccess();
ToolEditionAndVersion requested = request.getRequested();
VersionIdentifier resolvedVersion = requested.getResolvedVersion();
- Path downloadedToolFile = downloadTool(requested.getEdition().edition(), resolvedVersion);
- boolean extract = isExtract();
- if (!extract) {
- LOG.trace("Extraction is disabled for '{}' hence just moving the downloaded file {}.", this.tool, downloadedToolFile);
- }
+ Path downloadedToolFile = getDownloadedToolFile(request);
+
if (Files.isDirectory(installationPath)) {
if (this.tool.equals(IdeasyCommandlet.TOOL_NAME)) {
LOG.warn("Your IDEasy installation is missing the version file.");
@@ -250,13 +247,42 @@ protected void performToolInstallation(ToolInstallRequest request, Path installa
}
}
fileAccess.mkdirs(installationPath.getParent());
- fileAccess.extract(downloadedToolFile, installationPath, this::postExtract, extract);
+
+ onInstall(request, installationPath, downloadedToolFile);
+
this.context.writeVersionFile(resolvedVersion, installationPath);
// fix macOS Gatekeeper blocking - must run after version file is written but before any executables are launched
getMacOsHelper().removeQuarantineAttribute(installationPath);
LOG.debug("Installed {} in version {} at {}", this.tool, resolvedVersion, installationPath);
}
+ /**
+ * Performs the actual installation of the tool bits.
+ *
+ * @param request the {@link ToolInstallRequest}.
+ * @param installationPath the target {@link Path} where the tool should be installed.
+ * @param downloadedToolFile the {@link Path} to the downloaded tool file.
+ */
+ protected void onInstall(ToolInstallRequest request, Path installationPath, Path downloadedToolFile) {
+
+ boolean extract = isExtract();
+ if (!extract) {
+ LOG.trace("Extraction is disabled for '{}' hence just moving the downloaded file {}.", this.tool, downloadedToolFile);
+ }
+ this.context.getFileAccess().extract(downloadedToolFile, installationPath, this::postExtract, extract);
+ }
+
+ /**
+ * @param request the {@link ToolInstallRequest}.
+ * @return the {@link Path} to the downloaded tool file.
+ */
+ protected Path getDownloadedToolFile(ToolInstallRequest request) {
+
+ ToolEditionAndVersion requested = request.getRequested();
+ VersionIdentifier resolvedVersion = requested.getResolvedVersion();
+ return downloadTool(requested.getEdition().edition(), resolvedVersion);
+ }
+
/**
* @param edition the {@link #getConfiguredEdition() tool edition} to download.
* @param resolvedVersion the resolved {@link VersionIdentifier version} to download.
diff --git a/cli/src/main/java/com/devonfw/tools/ide/tool/msvc/Msvc.java b/cli/src/main/java/com/devonfw/tools/ide/tool/msvc/Msvc.java
new file mode 100644
index 0000000000..1cd1f65c69
--- /dev/null
+++ b/cli/src/main/java/com/devonfw/tools/ide/tool/msvc/Msvc.java
@@ -0,0 +1,40 @@
+package com.devonfw.tools.ide.tool.msvc;
+
+import java.nio.file.Path;
+import java.util.Set;
+
+import com.devonfw.tools.ide.context.IdeContext;
+import com.devonfw.tools.ide.process.ProcessErrorHandling;
+import com.devonfw.tools.ide.tool.LocalToolCommandlet;
+import com.devonfw.tools.ide.tool.ToolInstallRequest;
+
+public class Msvc extends LocalToolCommandlet {
+
+ private static final String MSVC_SETUP_URL = "https://aka.ms/vs/17/release/vs_BuildTools.exe";
+
+ public Msvc(IdeContext context) {
+ super(context, "msvc", Set.of());
+ }
+
+ @Override
+ protected void performToolInstallation(ToolInstallRequest request, Path installationPath) {
+
+ this.context.getFileAccess().mkdirs(installationPath);
+
+ Path installer = this.context.getDownloadPath().resolve("vs_BuildTools.exe");
+ this.context.newProcess().errorHandling(ProcessErrorHandling.THROW_CLI)
+ .executable("curl.exe")
+ .addArgs("-fSL", "-o", installer.toString(), MSVC_SETUP_URL)
+ .run();
+
+ this.context.writeVersionFile(request.getRequested().getResolvedVersion(), installationPath);
+
+ this.context.newProcess().errorHandling(ProcessErrorHandling.THROW_CLI)
+ .executable(installer)
+ .withExitCodeAcceptor(code -> (code == 0) || (code == 3010))
+ .addArgs("--installPath", installationPath.toString(),
+ "--add", "Microsoft.VisualStudio.Workload.VCTools",
+ "--quiet", "--wait", "--norestart", "--nocache")
+ .run();
+ }
+}
diff --git a/cli/src/main/java/com/devonfw/tools/ide/tool/rust/Rust.java b/cli/src/main/java/com/devonfw/tools/ide/tool/rust/Rust.java
new file mode 100644
index 0000000000..79af7cafbb
--- /dev/null
+++ b/cli/src/main/java/com/devonfw/tools/ide/tool/rust/Rust.java
@@ -0,0 +1,104 @@
+package com.devonfw.tools.ide.tool.rust;
+
+import java.nio.file.Files;
+import java.nio.file.LinkOption;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Set;
+
+import com.devonfw.tools.ide.common.Tag;
+import com.devonfw.tools.ide.context.IdeContext;
+import com.devonfw.tools.ide.io.FileAccess;
+import com.devonfw.tools.ide.process.EnvironmentContext;
+import com.devonfw.tools.ide.process.ProcessContext;
+import com.devonfw.tools.ide.process.ProcessErrorHandling;
+import com.devonfw.tools.ide.tool.LocalToolCommandlet;
+import com.devonfw.tools.ide.tool.ToolInstallRequest;
+import com.devonfw.tools.ide.tool.ToolInstallation;
+import com.devonfw.tools.ide.version.VersionIdentifier;
+
+/**
+ * {@link LocalToolCommandlet} for Rust.
+ */
+public class Rust extends LocalToolCommandlet {
+
+ /**
+ * The constructor.
+ *
+ * @param context the {@link IdeContext}.
+ */
+ public Rust(IdeContext context) {
+
+ super(context, "rust", Set.of(Tag.RUST));
+ }
+
+ @Override
+ public String getBinaryName() {
+
+ return "rustc";
+ }
+
+ @Override
+ public String getToolHelpArguments() {
+
+ return "--help";
+ }
+
+ @Override
+ protected boolean isExtract() {
+
+ // The rustup installer script is an executable script and must not be extracted.
+ return false;
+ }
+
+ /**
+ * Performs the actual installation of the tool bits.
+ *
+ * @param request the {@link ToolInstallRequest}.
+ * @param installationPath the target {@link Path} where the tool should be installed.
+ * @param installerScript the {@link Path} to the downloaded tool file.
+ */
+ @Override
+ protected void onInstall(ToolInstallRequest request, Path installationPath, Path installerScript) {
+
+ VersionIdentifier resolvedVersion = request.getRequested().getResolvedVersion();
+ FileAccess fileAccess = this.context.getFileAccess();
+
+ Path cargoHome = installationPath.resolve(".cargo");
+ Path rustupHome = installationPath.resolve(".rustup");
+ fileAccess.mkdirs(cargoHome);
+ fileAccess.mkdirs(rustupHome);
+
+ if (Files.isDirectory(installerScript)) {
+ // ToolRepositoryMock may provide an unpacked folder instead of a single download file.
+ installerScript = installerScript.resolve("content.sh");
+ }
+
+ ProcessContext process = request.getProcessContext().createChild().errorHandling(ProcessErrorHandling.THROW_CLI).directory(installationPath)
+ .withEnvVar("CARGO_HOME", cargoHome.toString()).withEnvVar("RUSTUP_HOME", rustupHome.toString());
+
+ List installerArgs = List.of("-y", "--no-modify-path", "--profile", "default", "--default-toolchain", resolvedVersion.toString());
+
+ process.executable(this.context.findBashRequired()).addArgs(installerScript.toAbsolutePath().toString()).addArgs(installerArgs);
+ process.run();
+
+ Path cargoBin = cargoHome.resolve("bin");
+ Path toolBin = installationPath.resolve("bin");
+ if (Files.exists(toolBin, LinkOption.NOFOLLOW_LINKS)) {
+ fileAccess.delete(toolBin);
+ }
+ if (Files.isDirectory(cargoBin)) {
+ fileAccess.symlink(cargoBin, toolBin);
+ }
+ }
+
+ @Override
+ public void setEnvironment(EnvironmentContext environmentContext, ToolInstallation toolInstallation, boolean additionalInstallation) {
+
+ super.setEnvironment(environmentContext, toolInstallation, additionalInstallation);
+ Path rootDir = toolInstallation.rootDir();
+ environmentContext.withEnvVar("CARGO_HOME", rootDir.resolve(".cargo").toString());
+ environmentContext.withEnvVar("RUSTUP_HOME", rootDir.resolve(".rustup").toString());
+ }
+
+}
diff --git a/cli/src/main/resources/nls/Help.properties b/cli/src/main/resources/nls/Help.properties
index 191e8a9d5c..e8a6f1a8ac 100644
--- a/cli/src/main/resources/nls/Help.properties
+++ b/cli/src/main/resources/nls/Help.properties
@@ -87,6 +87,8 @@ cmd.list-versions=List the available versions of the selected tool.
cmd.list-versions.detail=To list all available versions of e.g. 'intellij' simply type: 'ide list-versions intellij'.
cmd.ln=Create a symbolic or hard link.
cmd.ln.detail=Creates a link similar to the command "ln" but working cross-platform (unlike in git-bash on Windows that typically only creates a copy).
+cmd.msvc=Tool commandlet for MSVC (Microsoft Visual C++ Build Tools).
+cmd.msvc.detail=MSVC provides the C++ compiler and build tools required by Rust on Windows. Detailed documentation can be found at https://visualstudio.microsoft.com/visual-cpp-build-tools/
cmd.mvn=Tool commandlet for Maven (Build-Tool).
cmd.mvn.detail=Apache Maven is a build automation and dependency management tool for Java projects. Detailed documentation can be found at https://maven.apache.org/guides/index.html
cmd.nest=Tool commandlet for Nest CLI.
@@ -112,6 +114,8 @@ cmd.quarkus.detail=Quarkus is a Kubernetes-native Java framework for building cl
cmd.repository=Set up pre-configured git repositories using 'ide repository setup '
cmd.repository.detail=Without further arguments this will set up all pre-configured git repositories.\nAlso, you can provide an explicit git repo as `` argument and IDEasy will automatically clone, build and set up your project based on the existing property file.\nRepositories are configured in 'settings/repository/.properties' and can therefore be shared with your project team for automatic or optional setup.
cmd.repository.val.repository=The name of the properties file of the pre-configured git repository to set up, omit to set up all active repositories.
+cmd.rust=Tool commandlet for Rust (programming language).
+cmd.rust.detail=Rust is a programming-language focused on safety and performance. Detailed documentation can be found at https://www.rust-lang.org/learn
cmd.set-edition=Set the edition of the selected tool.
cmd.set-edition.detail=This will set the according tool edition variable in your configuration file. If you want to roll out such change and share it with your team, you can commit and push your settings git repository.\nBy default these changes are saved in the project specific settings. Use --conf --home or --workspace to specify otherwise.
cmd.set-edition.opt.--cfg=Selection of the configuration file (settings | home | conf | workspace).
diff --git a/cli/src/main/resources/nls/Help_de.properties b/cli/src/main/resources/nls/Help_de.properties
index 951d0204e4..861ea6a082 100644
--- a/cli/src/main/resources/nls/Help_de.properties
+++ b/cli/src/main/resources/nls/Help_de.properties
@@ -87,6 +87,8 @@ cmd.list-versions=Listet die verfügbaren Versionen des selektierten Werkzeugs a
cmd.list-versions.detail=Um alle verfügbaren Versionen von z.B. 'intellij' aufzulisten, geben Sie einfach 'ide list-versions intellij' in die Konsole ein.
cmd.ln=Erstellt einen symbolischen oder harten Link
cmd.ln.detail=Erstellt einen Link wie das Kommando "ln" jedoch plattform-übergreifend (anders als in Git-Bash unter Windows wo typischerweise nur eine Kopie erstellt wird).
+cmd.msvc=Werkzeug Kommando für MSVC (Microsoft Visual C++ Build-Werkzeuge).
+cmd.msvc.detail=MSVC stellt den C++ Compiler und Build-Werkzeuge bereit, die von Rust unter Windows benötigt werden. Detaillierte Dokumentation ist zu finden unter https://visualstudio.microsoft.com/visual-cpp-build-tools/
cmd.mvn=Werkzeug Kommando für Maven (Build-Werkzeug).
cmd.mvn.detail=Apache Maven ist ein Build-Automatisierungs- und Abhängigkeitsverwaltungstool für Java-Projekte. Detaillierte Dokumentation ist zu finden unter https://maven.apache.org/guides/index.html
cmd.nest=Werkzeug Kommando für Nest CLI.
@@ -112,6 +114,8 @@ cmd.quarkus.detail=Quarkus ist ein Kubernetes-native Java-Framework zur Entwickl
cmd.repository=Richtet das vorkonfigurierte Git Repository ein mittels 'ide repository setup '.
cmd.repository.detail=Dies wird alle vorkonfigurierten Repositories einrichten. Rufen Sie einfach 'ide repository setup ' auf, ersetzen Sie durch den Namen Ihrer Projektkonfigurationsdatei, die sich in 'settings/repository/your_project_name' befindet und IDEasy wird Ihr Projekt basierend auf der vorhandenen Eigenschaftsdatei automatisch klonen, bauen und einrichten.\nWenn Sie den Projektnamen weglassen, werden alle im Repository-Verzeichnis gefundenen Projekte vorkonfiguriert.
cmd.repository.val.repository=Der Name der Properties-Datei des vorkonfigurierten Git Repositories zum Einrichten. Falls nicht angegeben, werden alle aktiven Projekte eingerichtet.
+cmd.rust=Werkzeug Kommando für Rust (Programmiersprache).
+cmd.rust.detail=Rust ist eine Programmiersprache mit Fokus auf Sicherheit und Performance. Detaillierte Dokumentation findet sich unter https://www.rust-lang.org/learn
cmd.set-edition=Setzt die Edition des selektierten Werkzeugs.
cmd.set-edition.detail=Dies setzt die entsprechende Werkzeug-Edition Variable in der Konfigurationsdatei. Um solche Anpassungen auszurollen und mit dem Team zu teilen, kann diese im Settings git repository committed und gepushed werden.\nDiese Änderungen werden in der projektspezifischen Konfiguration gespeichert, es sei denn es wird mit --conf --home oder --workspace einen anderer Benutzer- oder Workspace-spezifischer Speicherort definiert.
cmd.set-edition.opt.--cfg=Auswahl der Konfigurationsdatei (settings | home | conf | workspace).
diff --git a/cli/src/test/java/com/devonfw/tools/ide/tool/rust/RustTest.java b/cli/src/test/java/com/devonfw/tools/ide/tool/rust/RustTest.java
new file mode 100644
index 0000000000..b78526c97c
--- /dev/null
+++ b/cli/src/test/java/com/devonfw/tools/ide/tool/rust/RustTest.java
@@ -0,0 +1,32 @@
+package com.devonfw.tools.ide.tool.rust;
+
+import org.junit.jupiter.api.Test;
+
+import com.devonfw.tools.ide.context.AbstractIdeContextTest;
+import com.devonfw.tools.ide.context.IdeTestContext;
+
+/**
+ * Test of {@link Rust}.
+ */
+class RustTest extends AbstractIdeContextTest {
+
+ private static final String PROJECT_RUST = "rust";
+
+ private static final String RUST_VERSION = "1.80.1";
+
+ @Test
+ void testRustInstallViaRustupScript() {
+
+ // arrange
+ IdeTestContext context = newContext(PROJECT_RUST);
+ Rust rust = context.getCommandletManager().getCommandlet(Rust.class);
+
+ // act
+ rust.install();
+
+ // assert
+ assertThat(context.getSoftwarePath().resolve("rust/.ide.software.version")).exists().hasContent(RUST_VERSION);
+ assertThat(context).logAtSuccess().hasMessageContaining("Successfully installed rust in version " + RUST_VERSION);
+ }
+}
+
diff --git a/cli/src/test/resources/ide-projects/rust/_ide/urls/rust/rust/1.80.1/urls b/cli/src/test/resources/ide-projects/rust/_ide/urls/rust/rust/1.80.1/urls
new file mode 100644
index 0000000000..d763e4b8fe
--- /dev/null
+++ b/cli/src/test/resources/ide-projects/rust/_ide/urls/rust/rust/1.80.1/urls
@@ -0,0 +1,2 @@
+https://sh.rustup.rs
+
diff --git a/cli/src/test/resources/ide-projects/rust/project/settings/ide.properties b/cli/src/test/resources/ide-projects/rust/project/settings/ide.properties
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/cli/src/test/resources/ide-projects/rust/project/workspaces/main/.gitkeep b/cli/src/test/resources/ide-projects/rust/project/workspaces/main/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/cli/src/test/resources/ide-projects/rust/repository/rust/rust/default/content.sh b/cli/src/test/resources/ide-projects/rust/repository/rust/rust/default/content.sh
new file mode 100644
index 0000000000..c1664d9c76
--- /dev/null
+++ b/cli/src/test/resources/ide-projects/rust/repository/rust/rust/default/content.sh
@@ -0,0 +1,17 @@
+#!/usr/bin/env bash
+set -eu
+
+mkdir -p "${CARGO_HOME}/bin"
+mkdir -p "${RUSTUP_HOME}"
+
+cat > "${CARGO_HOME}/bin/rustc" <<'EOF'
+#!/usr/bin/env bash
+echo rustc "$@"
+EOF
+chmod +x "${CARGO_HOME}/bin/rustc"
+
+cat > "${CARGO_HOME}/bin/rustc.cmd" <<'EOF'
+@echo off
+echo rustc %*
+EOF
+
diff --git a/url-updater/src/test/java/com/devonfw/tools/ide/url/tool/rust/RustGithubUrlTagUpdaterTest.java b/url-updater/src/test/java/com/devonfw/tools/ide/url/tool/rust/RustGithubUrlTagUpdaterTest.java
new file mode 100644
index 0000000000..8d2dd6aaec
--- /dev/null
+++ b/url-updater/src/test/java/com/devonfw/tools/ide/url/tool/rust/RustGithubUrlTagUpdaterTest.java
@@ -0,0 +1,45 @@
+package com.devonfw.tools.ide.url.tool.rust;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.client.WireMock.any;
+import static com.github.tomakehurst.wiremock.client.WireMock.get;
+import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlMatching;
+
+import java.nio.file.Path;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import com.devonfw.tools.ide.url.model.folder.UrlRepository;
+import com.devonfw.tools.ide.url.updater.AbstractUrlUpdaterTest;
+import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo;
+import com.github.tomakehurst.wiremock.junit5.WireMockTest;
+
+/**
+ * Test of {@link RustUrlUpdater}.
+ */
+@WireMockTest
+class RustGithubUrlTagUpdaterTest extends AbstractUrlUpdaterTest {
+
+ @Test
+ void testRustGithubUrlUpdater(@TempDir Path tempDir, WireMockRuntimeInfo wmRuntimeInfo) {
+
+ // arrange
+ stubFor(get(urlMatching("/repos/rust-lang/rustup/git/refs/tags")).willReturn(aResponse().withStatus(200)
+ .withBody(readAndResolve(PATH_INTEGRATION_TEST.resolve("RustUrlUpdater").resolve("github-tags.json"), wmRuntimeInfo))));
+ stubFor(any(urlMatching("/rustup\\.sh")).willReturn(aResponse().withStatus(200).withHeader("content-type", "text/plain").withBody(DOWNLOAD_CONTENT)));
+
+ UrlRepository urlRepository = UrlRepository.load(tempDir);
+ RustUrlUpdaterMock updater = new RustUrlUpdaterMock(wmRuntimeInfo);
+
+ // act
+ updater.update(urlRepository);
+
+ // assert
+ Path rustEditionDir = tempDir.resolve("rust").resolve("rust");
+ assertUrlVersionAgnostic(rustEditionDir.resolve("1.79.0"));
+ assertUrlVersionAgnostic(rustEditionDir.resolve("1.80.1"));
+ assertThat(rustEditionDir.resolve("release-0.7")).doesNotExist();
+ }
+}