-
Notifications
You must be signed in to change notification settings - Fork 65
Rust Integration #1827
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Rust Integration #1827
Changes from all commits
04eda6d
579a189
cc9b301
22bbef5
45d27ba
610f368
848545c
876ec24
3eb83ca
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,190 @@ | ||
| 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.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.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; | ||
| } | ||
|
|
||
| private void installDependencies() { | ||
|
|
||
| if (this.context.getSystemInfo().isWindows()) { | ||
| installWindowsMsvcBuildTools(); | ||
| } | ||
| } | ||
|
|
||
| protected String getMsvcSetupUrl() { | ||
|
|
||
| return MSVC_SETUP_URL; | ||
| } | ||
|
|
||
| protected List<String> getMsvcInstallerArgs() { | ||
|
|
||
| return List.of("--quiet", "--wait", "--norestart", "--nocache", "--add", "Microsoft.VisualStudio.Workload.VCTools"); | ||
| } | ||
|
|
||
| protected List<String> getRustupInstallerArgs(VersionIdentifier version) { | ||
|
|
||
| return List.of("-y", "--no-modify-path", "--profile", "default", "--default-toolchain", version.toString()); | ||
| } | ||
|
|
||
| private void installWindowsMsvcBuildTools() { | ||
|
|
||
| FileAccess fileAccess = this.context.getFileAccess(); | ||
| Path tempDir = fileAccess.createTempDir("msvc-setup"); | ||
| Path installer = tempDir.resolve("vs_BuildTools.exe"); | ||
| fileAccess.download(getMsvcSetupUrl(), installer); | ||
|
|
||
| ProcessContext process = this.context.newProcess().errorHandling(ProcessErrorHandling.THROW_CLI).executable(installer) | ||
| .withExitCodeAcceptor(code -> (code == 0) || (code == 3010)).addArgs(getMsvcInstallerArgs()); | ||
| process.run(); | ||
| } | ||
|
hohwille marked this conversation as resolved.
|
||
|
|
||
| @Override | ||
| protected void performToolInstallation(ToolInstallRequest request, Path installationPath) { | ||
|
|
||
| installDependencies(); | ||
| VersionIdentifier resolvedVersion = request.getRequested().getResolvedVersion(); | ||
| FileAccess fileAccess = this.context.getFileAccess(); | ||
|
|
||
| if (Files.isDirectory(installationPath)) { | ||
| fileAccess.backup(installationPath); | ||
| } | ||
| fileAccess.mkdirs(installationPath); | ||
|
|
||
| Path cargoHome = installationPath.resolve(".cargo"); | ||
| Path rustupHome = installationPath.resolve(".rustup"); | ||
| fileAccess.mkdirs(cargoHome); | ||
| fileAccess.mkdirs(rustupHome); | ||
|
|
||
| Path installerScript = downloadTool(request.getRequested().getEdition().edition(), resolvedVersion); | ||
| if (Files.isDirectory(installerScript)) { | ||
| // ToolRepositoryMock may provide an unpacked folder instead of a single download file. | ||
| installerScript = installerScript.resolve("content.sh"); | ||
| } | ||
|
|
||
| List<String> installerArgs = getRustupInstallerArgs(resolvedVersion); | ||
| ProcessContext process = request.getProcessContext().createChild().errorHandling(ProcessErrorHandling.THROW_CLI).directory(installationPath) | ||
| .withEnvVar("CARGO_HOME", cargoHome.toString()).withEnvVar("RUSTUP_HOME", rustupHome.toString()); | ||
| if (isWindowsExeInstaller(installerScript)) { | ||
| Path installerExecutable = installerScript; | ||
| String fileName = installerScript.getFileName().toString(); | ||
| if (!WINDOWS_RUSTUP_INIT_EXE.equalsIgnoreCase(fileName)) { | ||
| Path canonicalInstaller = installerScript.resolveSibling(WINDOWS_RUSTUP_INIT_EXE); | ||
|
|
||
| // Handle corrupted installations where rustup-init.exe might exist as a file or directory | ||
| 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); | ||
|
|
||
| try { | ||
| // First attempt: delete using fileAccess which handles files, directories, and symlinks | ||
| fileAccess.delete(canonicalInstaller); | ||
| LOG.debug("Successfully deleted existing installer at {}", canonicalInstaller); | ||
| } catch (IllegalStateException e) { | ||
| LOG.warn("First deletion attempt failed for {}, retrying: {}", canonicalInstaller, e.getMessage()); | ||
| // Retry in case of transient lock issues | ||
| try { | ||
| fileAccess.delete(canonicalInstaller); | ||
| LOG.debug("Successfully deleted existing installer on retry at {}", canonicalInstaller); | ||
| } catch (IllegalStateException e2) { | ||
| LOG.error("Failed to delete {} after retry: {}", canonicalInstaller, e2.getMessage(), e2); | ||
| throw new IllegalStateException("Failed to clean up corrupted installer at " + canonicalInstaller | ||
| + " (may be locked by another process or permission denied)", e2); | ||
| } | ||
| } | ||
|
|
||
| // Verify deletion was successful | ||
| if (Files.exists(canonicalInstaller, LinkOption.NOFOLLOW_LINKS)) { | ||
| boolean stillIsDirectory = Files.isDirectory(canonicalInstaller, LinkOption.NOFOLLOW_LINKS); | ||
| throw new IllegalStateException("Failed to delete corrupted installer at " + canonicalInstaller | ||
| + " (is directory: " + stillIsDirectory + ", may be locked by another process)"); | ||
| } | ||
| } | ||
|
|
||
| fileAccess.copy(installerScript, canonicalInstaller, FileCopyMode.COPY_FILE_TO_TARGET_OVERRIDE); | ||
| installerExecutable = canonicalInstaller; | ||
| } | ||
| process.executable(installerExecutable).addArgs(installerArgs); | ||
| } else { | ||
| String installerScriptArg = installerScript.toAbsolutePath().toString(); | ||
| process.executable(this.context.findBashRequired()).addArgs(installerScriptArg).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); | ||
| } | ||
|
|
||
| this.context.writeVersionFile(resolvedVersion, installationPath); | ||
| LOG.debug("Installed {} in version {} at {}", this.tool, resolvedVersion, installationPath); | ||
| } | ||
|
Comment on lines
+97
to
+180
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This looks like a lot of copy&paste from the overridden method. |
||
|
|
||
| private boolean isWindowsExeInstaller(Path installerPath) { | ||
|
|
||
| if (!this.context.getSystemInfo().isWindows()) { | ||
| return false; | ||
| } | ||
| Path fileName = installerPath.getFileName(); | ||
| return (fileName != null) && fileName.toString().toLowerCase().endsWith(".exe"); | ||
| } | ||
| } | ||
| 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 | ||
|
|
| 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(); | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.