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
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,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;
Expand Down Expand Up @@ -129,6 +130,7 @@ public CommandletManagerImpl(IdeContext 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));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
* <p>
* 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.
Expand All @@ -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.");
Expand All @@ -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.
Expand Down
161 changes: 161 additions & 0 deletions cli/src/main/java/com/devonfw/tools/ide/tool/rust/Rust.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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.io.FileCopyMode;
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 <a href="https://www.rust-lang.org/">Rust</a>.
*/
public class Rust extends LocalToolCommandlet {

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

private static final String MSVC_SETUP_URL = "https://aka.ms/vs/17/release/vs_BuildTools.exe";

private static final String WINDOWS_RUSTUP_INIT_EXE = "rustup-init.exe";

/**
* 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) {

if (this.context.getSystemInfo().isWindows()) {
installWindowsMsvcBuildTools();
}

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<String> installerArgs = List.of("-y", "--no-modify-path", "--profile", "default", "--default-toolchain", resolvedVersion.toString());

if (isWindowsExeInstaller(installerScript)) {
Path installerExecutable = prepareWindowsInstaller(fileAccess, installerScript);
process.executable(installerExecutable).addArgs(installerArgs);
} else {
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());
}

private void installWindowsMsvcBuildTools() {

FileAccess fileAccess = this.context.getFileAccess();
Path tempDir = fileAccess.createTempDir("msvc-setup");
Path installer = tempDir.resolve("vs_BuildTools.exe");
fileAccess.download(MSVC_SETUP_URL, installer);

this.context.newProcess().errorHandling(ProcessErrorHandling.THROW_CLI).executable(installer)
.withExitCodeAcceptor(code -> (code == 0) || (code == 3010))
.addArgs("--quiet", "--wait", "--norestart", "--nocache", "--add", "Microsoft.VisualStudio.Workload.VCTools")
.run();
}

private Path prepareWindowsInstaller(FileAccess fileAccess, Path installerScript) {

String fileName = installerScript.getFileName().toString();
if (WINDOWS_RUSTUP_INIT_EXE.equalsIgnoreCase(fileName)) {
return installerScript;
}
Path canonicalInstaller = installerScript.resolveSibling(WINDOWS_RUSTUP_INIT_EXE);
if (Files.exists(canonicalInstaller, LinkOption.NOFOLLOW_LINKS)) {
LOG.info("Found existing installer at {}, checking type", canonicalInstaller);
boolean isDirectory = Files.isDirectory(canonicalInstaller, LinkOption.NOFOLLOW_LINKS);
LOG.info("Existing installer is {} (directory: {}), deleting it", canonicalInstaller, isDirectory);
fileAccess.delete(canonicalInstaller);
}
fileAccess.copy(installerScript, canonicalInstaller, FileCopyMode.COPY_FILE_TO_TARGET_OVERRIDE);
return canonicalInstaller;
}

private boolean isWindowsExeInstaller(Path installerPath) {

if (!this.context.getSystemInfo().isWindows()) {
return false;
}
Path fileName = installerPath.getFileName();
return (fileName != null) && fileName.toString().toLowerCase().endsWith(".exe");
}
}
2 changes: 2 additions & 0 deletions cli/src/main/resources/nls/Help.properties
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,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 <repository>'
cmd.repository.detail=Without further arguments this will set up all pre-configured git repositories.\nAlso, you can provide an explicit git repo as `<repository>` argument and IDEasy will automatically clone, build and set up your project based on the existing property file.\nRepositories are configured in 'settings/repository/<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).
Expand Down
2 changes: 2 additions & 0 deletions cli/src/main/resources/nls/Help_de.properties
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,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 <repository>'.
cmd.repository.detail=Dies wird alle vorkonfigurierten Repositories einrichten. Rufen Sie einfach 'ide repository setup <your_project_name>' auf, ersetzen Sie <your_project_name> 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).
Expand Down
32 changes: 32 additions & 0 deletions cli/src/test/java/com/devonfw/tools/ide/tool/rust/RustTest.java
Original file line number Diff line number Diff line change
@@ -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);
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
https://sh.rustup.rs

Empty file.
Original file line number Diff line number Diff line change
@@ -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

Original file line number Diff line number Diff line change
@@ -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();
}
}
Loading