diff --git a/.github/workflows/installers.yml b/.github/workflows/installers.yml
new file mode 100644
index 0000000..2c26ebb
--- /dev/null
+++ b/.github/workflows/installers.yml
@@ -0,0 +1,95 @@
+# Builds the Windows installers (installer/build-installer.ps1): the self-contained
+# install-intellij-ib-plugin.ps1 and the intellij-ib-plugin.exe self-extracting installer.
+# Windows-only: the .exe needs csc.exe and 7-Zip, and the standalone script is run with Windows
+# PowerShell, which is also what assembles it here (see installer/README.md).
+#
+# - Called by the Release workflow with the release's tag: packages the exact plugin zip that
+# release published and attaches both installers to it.
+# - Run by hand (Actions > Installers > Run workflow) to try it out before a release:
+# - with no tag, it builds the plugin from the selected branch and only uploads the installers
+# as a workflow artifact - no release is touched;
+# - with an existing release's tag, it does exactly what the Release workflow does for that
+# release, e.g. to re-attach installers after a failed run.
+# (GitHub only offers "Run workflow" once this file is on the default branch.)
+# - On pull requests that change the installers, the same as a manual run without a tag - so a
+# change to them is exercised before it ever reaches a release.
+
+name: Installers
+on:
+ workflow_call:
+ inputs:
+ tag:
+ description: Release tag whose plugin zip to package, and to attach the installers to.
+ required: true
+ type: string
+ pull_request:
+ paths:
+ - installer/**
+ - .github/workflows/installers.yml
+ workflow_dispatch:
+ inputs:
+ tag:
+ description: Release tag to package and attach to. Leave empty to build from this branch and upload a workflow artifact only.
+ required: false
+ type: string
+
+jobs:
+ installers:
+ name: Build Installers
+ runs-on: windows-latest
+ permissions:
+ contents: write
+ env:
+ TAG: ${{ inputs.tag }}
+ GITHUB_TOKEN: ${{ github.token }}
+ steps:
+
+ # Check out the release's tag, or the branch the workflow was run from
+ - name: Fetch Sources
+ uses: actions/checkout@v7
+ with:
+ ref: ${{ inputs.tag || github.ref }}
+
+ # Only needed to build the plugin from source (no tag)
+ - name: Setup Java
+ if: ${{ !inputs.tag }}
+ uses: actions/setup-java@v5
+ with:
+ distribution: zulu
+ java-version: 21
+
+ - name: Setup Gradle
+ if: ${{ !inputs.tag }}
+ uses: gradle/actions/setup-gradle@v6.2.0
+ with:
+ cache-read-only: true
+
+ # The zip the release's "Upload Release Asset" step attached, not a fresh build
+ - name: Download Published Plugin
+ if: ${{ inputs.tag }}
+ shell: powershell
+ run: gh release download "$env:TAG" --repo "${{ github.repository }}" --pattern intellij-ib-plugin.zip --dir build/published
+
+ - name: Build Installers
+ shell: powershell
+ run: |
+ if ($env:TAG) {
+ .\installer\build-installer.ps1 -PluginZip build\published\intellij-ib-plugin.zip
+ } else {
+ # Runs gradlew buildPlugin itself first
+ .\installer\build-installer.ps1
+ }
+
+ - name: Upload Installers to Release
+ if: ${{ inputs.tag }}
+ shell: powershell
+ run: gh release upload "$env:TAG" --repo "${{ github.repository }}" --clobber build/distributions/install-intellij-ib-plugin.ps1 build/distributions/intellij-ib-plugin.exe
+
+ # Always kept as an artifact too, so a manual run's output can be downloaded and tried
+ - name: Upload Installers Artifact
+ uses: actions/upload-artifact@v7
+ with:
+ name: installers
+ path: |
+ build/distributions/install-intellij-ib-plugin.ps1
+ build/distributions/intellij-ib-plugin.exe
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index d725f9b..56a5aa2 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -98,3 +98,15 @@ jobs:
--body "Current pull request contains patched \`CHANGELOG.md\` file for the \`$VERSION\` version." \
--label "$LABEL" \
--head $BRANCH
+
+ # Build the Windows installers from the exact plugin zip the job above just published, and attach
+ # them to the same release - so they're reproducible from CI rather than built locally and
+ # committed. Its own workflow so it can also be run by hand - see installers.yml.
+ installers:
+ name: Build Installers
+ needs: [ release ]
+ permissions:
+ contents: write
+ uses: ./.github/workflows/installers.yml
+ with:
+ tag: ${{ github.event.release.tag_name }}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 13d3b50..e512983 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,38 @@
## [Unreleased]
+## [1.1.0]
+
+### Added
+
+- CLion support for CMake projects: **Build**, **Rebuild** and **Build All** in the **Incredibuild** menu run
+ `cmake --build` through Incredibuild, against the build directory, toolchain and generator the active CMake profile
+ is already configured with.
+- Rider support for .NET solutions: **Build Solution with Incredibuild**, **Rebuild Solution with Incredibuild** and
+ **Build Selected Projects with Incredibuild** in the **Build** menu, the Solution Explorer context menu and the
+ **Incredibuild** menu, bindable to shortcuts in **Settings/Preferences > Keymap**. They build the solution as it is
+ configured, in its active configuration and platform, through MSBuild accelerated by Incredibuild. While one runs,
+ the status bar shows the accelerated build in progress, and a notification reports the result.
+- **Settings/Preferences > Tools > Incredibuild** can set the MSBuild that Rider builds use, when the automatically
+ detected one isn't the right one.
+- The installers now also install the plugin into CLion and Rider.
+
+### Changed
+
+- Only one Incredibuild build runs at a time, in every IDE. Starting a build - from the **Incredibuild** menu or,
+ with IDE build acceleration on, from the IDE's own build actions - while another is still running offers to stop
+ the running one, then starts the new build as soon as it has stopped. Previously a second build started alongside
+ the first, replacing its output in the **Incredibuild** tool window.
+- The Windows installers (`install-intellij-ib-plugin.ps1` and `intellij-ib-plugin.exe`) are now attached to each
+ GitHub release, built by the release workflow from the published plugin, instead of the script being committed to
+ the repository.
+
+### Fixed
+
+- With **Accelerate the IDE's own build actions** on, **Build Project** in RustRover 2026.1 (and IntelliJ IDEA / CLion
+ 2026.1 with the Rust plugin) failed with `NoSuchMethodError` instead of building, because it relied on a Rust plugin
+ API that only exists from 2026.2. The plugin is now verified against both 2026.1 and 2026.2 of every supported IDE.
+
## [1.0.3]
### Added
diff --git a/README.md b/README.md
index 4c339c5..e87d870 100644
--- a/README.md
+++ b/README.md
@@ -33,10 +33,35 @@ The setting is off by default. While it is off - or if Incredibuild isn't instal
those actions behave exactly as they do without this plugin. The actions in the **Incredibuild** menu
always use Incredibuild, whichever way the setting is set.
+### Rider (.NET)
+
+In Rider, the **Build** menu gets dedicated Incredibuild actions right after the standard ones (they are
+also in the **Incredibuild** menu):
+
+- **Build Solution with Incredibuild** - builds the solution in its active configuration and platform
+- **Rebuild Solution with Incredibuild** - cleans and rebuilds it
+- **Build Selected Projects with Incredibuild** - builds the projects selected in the Solution Explorer,
+ along with the projects they reference
+
+The same actions are in the Solution Explorer's context menu on the solution, solution folders and
+projects, and each can be bound to a shortcut under **Settings/Preferences > Keymap** (search for
+"Incredibuild"). They have no default shortcuts, so none of Rider's own are taken over.
+
+They run MSBuild on the solution exactly as it is configured: the active configuration and platform
+are respected (for selected projects, through the solution's own project configuration mapping), and
+an open solution filter (`.slnf`) limits the build the same way it limits Rider's own. Nothing about the
+solution or its projects is changed. While an accelerated build runs, the status bar shows
+*Building with Incredibuild*, the **Incredibuild** tool window shows its output under a banner
+naming the configuration and MSBuild used, and a notification reports the result.
+
+MSBuild is detected automatically: the newest Visual Studio / Build Tools `MSBuild.exe` with the .NET SDK component on Windows,
+otherwise the .NET SDK's `dotnet msbuild`. Rider's own MSBuild selection lives in its backend settings,
+which plugins can't read, so a different one can be set under **Settings/Preferences > Tools > Incredibuild**.
+
### Settings
**Settings/Preferences > Tools > Incredibuild** configures the number of parallel jobs (`-j`)
-accelerated builds request, and the opt-in above.
+accelerated Cargo/CMake builds request, the opt-in above, and (in Rider) the MSBuild to use.
If Incredibuild isn't installed on the machine, the menu commands offer to open
[incredibuild.com](https://www.incredibuild.com/) to download it.
@@ -46,6 +71,8 @@ If Incredibuild isn't installed on the machine, the menu commands offer to open
- RustRover, or IntelliJ IDEA Ultimate with the Rust plugin - 2026.1 or 2026.2 (builds 261-262).
The Rust plugin is released for one IDE line at a time, and this plugin depends on it, so support
for a newer line follows a Rust plugin release for it.
+- Or CLion 2026.1 or 2026.2, with a CMake project.
+- Or Rider 2026.1 or 2026.2, with MSBuild (Visual Studio / Build Tools) or the .NET SDK installed.
- [Incredibuild](https://www.incredibuild.com/) installed, on Windows or Linux:
- Windows: 10.37.1 or later
- Linux: 4.29.3 or later, installed under `/opt/incredibuild`
@@ -69,6 +96,12 @@ If Incredibuild isn't installed on the machine, the menu commands offer to open
Download the [latest release](https://github.com/Incredibuild-RND/intellij-ib-plugin/releases/latest) and install it manually using
Settings/Preferences > Plugins > ⚙️ > Install plugin from disk...
+- On Windows, with an installer:
+
+ The same [release page](https://github.com/Incredibuild-RND/intellij-ib-plugin/releases/latest) also has
+ `intellij-ib-plugin.exe` and `install-intellij-ib-plugin.ps1`, which install the plugin into every supported
+ JetBrains IDE found on the machine. Close those IDEs first.
+
## License
Licensed under the [Apache License, Version 2.0](./LICENSE). See [CONTRIBUTING.md](./CONTRIBUTING.md) for contribution terms.
diff --git a/build.gradle.kts b/build.gradle.kts
index 334b430..b4c2ef2 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -2,6 +2,7 @@ import org.gradle.api.tasks.bundling.AbstractArchiveTask
import org.gradle.api.tasks.bundling.Zip
import org.jetbrains.intellij.platform.gradle.IntelliJPlatformType
import org.jetbrains.intellij.platform.gradle.TestFrameworkType
+import org.jetbrains.intellij.platform.gradle.tasks.VerifyPluginTask
plugins {
id("org.jetbrains.kotlin.jvm")
@@ -52,6 +53,9 @@ dependencies {
// Verifier against a plain 2026.2 target (only 262.8665+ builds have it).
plugin("com.intellij.nativeDebug", "262.8665.176")
bundledPlugin("intellij.testRunner.plugin")
+ // The Rider-only .NET adapter (see rider/build.gradle.kts), merged into this plugin's
+ // own jar rather than shipped as a separate one - still a single plugin to install.
+ pluginComposedModule(implementation(project(":rider")))
testFramework(TestFrameworkType.Platform)
}
}
@@ -69,10 +73,11 @@ tasks.withType().configureEach {
isPreserveFileTimestamps = true
}
-// Compiled against com.jetbrains.rust 262.8665.323 (2026.2 line), but the specific APIs this
-// plugin uses (CargoCommandConfiguration.parametersHolder, RustProjectSettingsService.toolchain,
-// RsToolchainBase.pathToCargoExecutable, etc.) are verified identical in the 261.x (2026.1) line
-// too, so declare compatibility down to build 261 rather than the auto-derived 262.
+// Compiled against com.jetbrains.rust 262.8665.323 (2026.2 line), but declares compatibility down
+// to build 261 rather than the auto-derived 262: the APIs this plugin uses are the same in the
+// 261.x (2026.1) line, except CargoBuildAdapter's attach API, which IncredibuildProjectTaskRunner
+// resolves for whichever version is loaded. The Plugin Verifier checks both lines (see
+// pluginVerification below), so a 262-only API used directly fails the build.
intellijPlatform {
pluginConfiguration {
ideaVersion {
@@ -107,21 +112,93 @@ intellijPlatform {
// resolve CMake-only classes - not a bug. Rather than dropping COMPATIBILITY_PROBLEMS
// from failureLevel entirely (which would also hide a real, future compatibility
// break, e.g. a 2026.x IDE removing an API this plugin uses), those 4 specific,
- // already-reviewed problems are listed in ignoredProblemsFile instead - everything
+ // already-reviewed problems are listed in an ignored-problems file instead - everything
// else in that category still fails the task. failureLevel is left at its default
// (COMPATIBILITY_PROBLEMS + INTERNAL_API_USAGES; confirmed by observing which
// categories actually failed the task even though deprecated/experimental/missing-
// optional-dependency problems were also present and reported) - it already caught a
// real, fixable internal-API usage (PluginManagerCore.getPlugin(), replaced with the
// public isLoaded() instead) and should keep gating future ones the same way.
- ignoredProblemsFile.set(rootProject.layout.projectDirectory.file("gradle/pluginVerifier-ignoredProblems.txt"))
+ //
+ // The ignored-problems file is set per IDE, not here - see the per-IDE verification
+ // tasks below.
+ //
+ // Both ends of the declared sinceBuild..untilBuild range (261-262), not just the 262 line
+ // everything is compiled against - otherwise 2026.1 would accept the plugin without ever
+ // having been verified, which matters most for the Rider half (rider/ is compiled against
+ // Rider 2026.2 only, and leans on Rider's own solution-model APIs).
ides {
- create(IntelliJPlatformType.CLion, "2026.2")
- create(IntelliJPlatformType.RustRover, "2026.2")
+ for (version in listOf("2026.1", "2026.2")) {
+ create(IntelliJPlatformType.CLion, version)
+ create(IntelliJPlatformType.RustRover, version)
+ // The only IDE that ever loads the .NET half (rider-support.xml).
+ // Rider is only published as a Maven artifact, not a regular installer - see
+ // https://github.com/JetBrains/intellij-platform-gradle-plugin/issues/1852.
+ create(IntelliJPlatformType.Rider, version) {
+ useInstaller = false
+ }
+ }
}
}
}
+// One verification run per IDE, each with only the ignored problems that IDE is expected to
+// report. A Plugin Verifier ignored-problems file applies to every IDE in a run - its conditions
+// can name a plugin but not an IDE - so a single shared file would also hide those same messages
+// where they *would* be real problems: "Package 'org.rust' is not found" is expected against
+// Rider (no Rust plugin) but would mean a broken build against RustRover, and the CMake-only
+// symbols are expected against RustRover but never against CLion. Splitting the runs keeps that
+// coverage.
+//
+// All three take the IDEs pluginVerification.ides resolves above and keep only their own, by the
+// productCode in each IDE's product-info.json. `verifyPlugin` itself verifies CLion and runs the
+// other two, so CI's `./gradlew verifyPlugin` still covers all three. Reports go to one
+// subdirectory each of build/reports/pluginVerifier, which CI uploads as a whole.
+data class PerIdeVerification(val productCode: String, val reportsSubdirectory: String, val ignoredProblems: String?)
+
+val perIdeVerifications = mapOf(
+ "verifyPlugin" to PerIdeVerification("CL", "clion", null),
+ "verifyPluginRustRover" to PerIdeVerification("RR", "rustrover", "gradle/pluginVerifier-ignoredProblems-rustrover.txt"),
+ "verifyPluginRider" to PerIdeVerification("RD", "rider", "gradle/pluginVerifier-ignoredProblems-rider.txt"),
+)
+
+for ((taskName, verification) in perIdeVerifications) {
+ val configure: VerifyPluginTask.() -> Unit = {
+ // A snapshot of the sources the plugin wired in, not `ides` itself - filtering a
+ // collection into its own sources would make it depend on itself.
+ val allIdes = files(ides.from.toList())
+ val productCode = verification.productCode
+ ides.setFrom(allIdes.filter { ide ->
+ val productInfo = ide.resolve("product-info.json")
+ productInfo.isFile &&
+ Regex(""""productCode"\s*:\s*"([A-Z]+)"""").find(productInfo.readText())?.groupValues?.get(1) == productCode
+ })
+ verification.ignoredProblems?.let { ignoredProblemsFile.set(layout.projectDirectory.file(it)) }
+ verificationReportsDirectory.set(layout.buildDirectory.dir("reports/pluginVerifier/${verification.reportsSubdirectory}"))
+ // A home directory of its own for each run. These tasks can run in parallel (the
+ // configuration cache allows it), and every Plugin Verifier run starts by deleting and
+ // recreating /extracted-plugins - with the shared default home (~/.pluginVerifier),
+ // one run deleting it while another walks it fails that run with NoSuchFileException.
+ systemProperty(
+ "plugin.verifier.home.dir",
+ layout.buildDirectory.dir("pluginVerifier/${verification.reportsSubdirectory}").get().asFile.absolutePath
+ )
+ }
+ if (taskName == "verifyPlugin") {
+ tasks.named(taskName, configure)
+ } else {
+ tasks.register(taskName) {
+ group = "intellij platform"
+ description = "Verifies the plugin against ${verification.reportsSubdirectory} only - see build.gradle.kts."
+ configure()
+ }
+ }
+}
+
+tasks.named("verifyPlugin") {
+ dependsOn("verifyPluginRustRover", "verifyPluginRider")
+}
+
// Plain `runIde` launches intellijIdeaUltimate (the platform this plugin's dependencies block
// above declares), which never loads the CMake half at all (see plugin.xml's optional
// ) - there's nothing to manually exercise there. Add a
@@ -132,12 +209,21 @@ intellijPlatformTesting {
type = IntelliJPlatformType.CLion
version = "2026.2"
}
+ // Likewise for the .NET/Rider actions (rider-support.xml).
+ register("runIdeForRider") {
+ type = IntelliJPlatformType.Rider
+ version = "2026.2"
+ useInstaller = false
+ }
}
}
// buildSearchableOptions launches a sandboxed IDE to index Settings UI labels for search.
// That launcher doesn't support ARM64 hosts ("Unsupported JVM architecture: aarch64") - it's a
// build-time search-indexing convenience only, not required for the plugin to function.
-tasks.named("buildSearchableOptions") {
- enabled = false
+// Switched off through the plugin's own flag rather than by disabling just that one task: the
+// follow-up prepareJarSearchableOptions task still expects the output folder buildSearchableOptions
+// would have created, so after a `clean` the task-only approach fails the build.
+intellijPlatform {
+ buildSearchableOptions = false
}
diff --git a/gradle.properties b/gradle.properties
index 2b55b6c..18ab9c2 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,5 +1,5 @@
group = com.incredibuild.ibplugin
-version = 1.0.3
+version = 1.1.0
pluginRepositoryUrl = https://github.com/Incredibuild-RND/intellij-ib-plugin
diff --git a/gradle/pluginVerifier-ignoredProblems-rider.txt b/gradle/pluginVerifier-ignoredProblems-rider.txt
new file mode 100644
index 0000000..5b5f092
--- /dev/null
+++ b/gradle/pluginVerifier-ignoredProblems-rider.txt
@@ -0,0 +1,12 @@
+// Plugin Verifier "ignored problems" file for the Rider run only (verifyPluginRider) - see the
+// per-IDE verification tasks in build.gradle.kts. Each IDE gets its own file because a file's
+// conditions apply to every IDE in a run: this line must not hide a real org.rust problem when
+// verifying against RustRover. Format: see pluginVerifier-ignoredProblems-rustrover.txt.
+//
+// The optional-dependency split seen from Rider, which has no Rust plugin:
+// RunWithIncredibuildAction is kept in the source tree but not registered anywhere (see the
+// commented-out registration in rust-support.xml), and everything that IS registered against
+// org.rust only loads through rust-support.xml. Confirmed by the Verifier's own "Missing
+// dependencies: com.jetbrains.rust (optional): Unavailable" against Rider. (The CMake problems
+// ignored for RustRover don't occur here: Rider bundles com.intellij.cmake itself.)
+com.incredibuild.ibplugin:Package 'org\.rust' is not found
diff --git a/gradle/pluginVerifier-ignoredProblems.txt b/gradle/pluginVerifier-ignoredProblems-rustrover.txt
similarity index 57%
rename from gradle/pluginVerifier-ignoredProblems.txt
rename to gradle/pluginVerifier-ignoredProblems-rustrover.txt
index 3920afc..86f6ec3 100644
--- a/gradle/pluginVerifier-ignoredProblems.txt
+++ b/gradle/pluginVerifier-ignoredProblems-rustrover.txt
@@ -1,6 +1,7 @@
-// Plugin Verifier "ignored problems" file - see pluginVerification.ignoredProblemsFile in
-// build.gradle.kts for how this is wired in, and its comment for why these specific problems
-// are expected rather than bugs.
+// Plugin Verifier "ignored problems" file for the RustRover run only (verifyPluginRustRover) - see
+// the per-IDE verification tasks in build.gradle.kts for how it's wired in, and the
+// pluginVerification comment there for why these specific problems are expected rather than bugs.
+// Each IDE gets its own file because a file's conditions apply to every IDE in a run.
//
// Format (one condition per line, blank lines and "//" comments skipped): either
// ":" or just "". No version field, so these keep applying across
@@ -11,8 +12,14 @@
// (com.jetbrains.cidr.cpp.toolchains.*, com.jetbrains.cidr.cpp.cmake.*) showing up as
// unresolved when verified against RustRover, which has no CMake plugin - the optional-
// dependency split (plugin.xml's cmake-support.xml) working as intended, confirmed by the
-// Verifier's own "Missing dependencies: com.intellij.cmake (optional): Unavailable". They can
-// never match when verifying against CLion, which does have com.intellij.cmake.
+// Verifier's own "Missing dependencies: com.intellij.cmake (optional): Unavailable".
+//
+// Caveat: these package-level lines also hide a CMake class reached from code that DOES run in
+// RustRover - the Verifier reports every such use under the same message. That's how
+// disambiguatedLabel calling hasCMakeWorkspace from the Cargo actions' update() got past it and
+// threw NoClassDefFoundError in RustRover. Code shared by the Rust and CMake halves must only
+// touch the other half's classes behind a PluginManagerCore.isLoaded check (see
+// WorkspaceLabels.kt).
com.incredibuild.ibplugin:Package 'com\.jetbrains\.cidr\.cpp\.toolchains' is not found
com.incredibuild.ibplugin:Package 'com\.jetbrains\.cidr\.cpp\.cmake' is not found
com.incredibuild.ibplugin:Access to unresolved class com\.jetbrains\.cidr\.cpp\.execution\.CMakeAppRunConfiguration\.BuildAndRunConfigurations
diff --git a/installer/README.md b/installer/README.md
index 8fc2eb0..da3d9c9 100644
--- a/installer/README.md
+++ b/installer/README.md
@@ -2,12 +2,20 @@
Produces both distributable installers from `build/distributions/intellij-ib-plugin.zip`:
-- `scripts/install-intellij-ib-plugin.ps1` - a single, self-contained PowerShell
- script (the plugin zip is embedded as base64) for anyone who'd rather not run
- an unsigned `.exe`.
+- `build/distributions/install-intellij-ib-plugin.ps1` - a single, self-contained
+ PowerShell script (the plugin zip is embedded as base64) for anyone who'd
+ rather not run an unsigned `.exe`.
- `build/distributions/intellij-ib-plugin.exe` - a self-extracting installer
with zero visible windows, for double-click installs.
+Neither is committed to the repo. Publishing a GitHub release runs the
+**Release** workflow, whose `installers` job ([installers.yml](../.github/workflows/installers.yml)) builds both on a Windows runner from
+the exact plugin zip the release published, and attaches them to the release.
+The same workflow runs on pull requests that change the installers, and can be run
+by hand (**Actions > Installers > Run workflow**, once it's on the default branch):
+without a tag it builds the plugin from the branch and only uploads the installers
+as a workflow artifact.
+
## Usage
```
@@ -19,6 +27,13 @@ This runs `.\gradlew.bat buildPlugin` itself first, so
source before either installer is packaged - it can never silently package a
stale zip left over from an earlier build.
+To package an already-built zip instead (what the release workflow does), pass it
+with `-PluginZip`:
+
+```
+powershell -ExecutionPolicy Bypass -File .\installer\build-installer.ps1 -PluginZip path\to\intellij-ib-plugin.zip
+```
+
Prerequisites (both are standard on a Windows dev machine, neither is bundled):
- **7-Zip** (`7z.exe` on `PATH`, or installed at `C:\Program Files\7-Zip`) - used
@@ -36,7 +51,7 @@ plugin zip and report progress:
| | `install-common.ps1` sandwiched between... | plugin zip | output |
|---|---|---|---|
-| Standalone script | `standalone-header.ps1` / `standalone-footer.ps1` | embedded as base64 | `scripts/install-intellij-ib-plugin.ps1` (committed to the repo) |
+| Standalone script | `standalone-header.ps1` / `standalone-footer.ps1` | embedded as base64 | `build/distributions/install-intellij-ib-plugin.ps1` (attached to each GitHub release) |
| SFX installer | `sfx-header.ps1` / `sfx-footer.ps1` | packaged alongside as a sibling file | assembled at build time, only shipped inside the `.exe` |
### The `.exe`'s pieces
diff --git a/installer/build-installer.ps1 b/installer/build-installer.ps1
index 7811b3f..81b5960 100644
--- a/installer/build-installer.ps1
+++ b/installer/build-installer.ps1
@@ -1,7 +1,7 @@
<#
.SYNOPSIS
-Builds both distributable installers from the current build/distributions/intellij-ib-plugin.zip:
- - scripts/install-intellij-ib-plugin.ps1 (self-contained, base64-embedded script)
+Builds both distributable installers from the plugin zip:
+ - build/distributions/install-intellij-ib-plugin.ps1 (self-contained, base64-embedded script)
- build/distributions/intellij-ib-plugin.exe (self-extracting installer, zero visible windows)
.DESCRIPTION
@@ -9,45 +9,68 @@ Just run this script - it runs `.\gradlew buildPlugin` itself first, so
build/distributions/intellij-ib-plugin.zip is always current:
powershell -ExecutionPolicy Bypass -File .\installer\build-installer.ps1
+Neither output is committed: the release workflow (.github/workflows/release.yml) runs this
+with -PluginZip pointing at the zip it just published, so the installers attached to a GitHub
+release always carry exactly that build.
+
+.PARAMETER PluginZip
+An already-built plugin zip to package, instead of running `gradlew buildPlugin`.
+
Both generated scripts share the IDE-detection/validation logic in
install-common.ps1 (edit that ONE file for both, rather than either generated
script directly - regenerate by re-running this script). See installer/README.md
for the full pipeline explanation and prerequisites.
#>
+param(
+ [string]$PluginZip
+)
+
$ErrorActionPreference = "Stop"
$installerDir = $PSScriptRoot
$repoRoot = Split-Path -Parent $installerDir
-$pluginZip = Join-Path $repoRoot "build\distributions\intellij-ib-plugin.zip"
-$standaloneScriptOut = Join-Path $repoRoot "scripts\install-intellij-ib-plugin.ps1"
-$exeOut = Join-Path $repoRoot "build\distributions\intellij-ib-plugin.exe"
+$outDir = Join-Path $repoRoot "build\distributions"
+$standaloneScriptOut = Join-Path $outDir "install-intellij-ib-plugin.ps1"
+$exeOut = Join-Path $outDir "intellij-ib-plugin.exe"
# --- 0. Rebuild the plugin so the zip can never be stale -------------------
-$gradlew = Join-Path $repoRoot "gradlew.bat"
-if (-not (Test-Path $gradlew)) {
- Write-Error "$gradlew not found - expected at the repo root."
- exit 1
-}
+if ($PluginZip) {
+ if (-not (Test-Path $PluginZip)) {
+ Write-Error "$PluginZip not found."
+ exit 1
+ }
+ $pluginZip = (Resolve-Path $PluginZip).Path
+} else {
+ $pluginZip = Join-Path $outDir "intellij-ib-plugin.zip"
-Write-Host "Running '.\gradlew.bat buildPlugin' ..."
-Push-Location $repoRoot
-try {
- & $gradlew buildPlugin --console=plain
- if ($LASTEXITCODE -ne 0) {
- Write-Error "gradlew buildPlugin failed (exit $LASTEXITCODE)."
+ $gradlew = Join-Path $repoRoot "gradlew.bat"
+ if (-not (Test-Path $gradlew)) {
+ Write-Error "$gradlew not found - expected at the repo root."
exit 1
}
-} finally {
- Pop-Location
-}
-if (-not (Test-Path $pluginZip)) {
- Write-Error "$pluginZip still not found after 'gradlew buildPlugin' - check the buildPlugin task's output archive name/path."
- exit 1
+ Write-Host "Running '.\gradlew.bat buildPlugin' ..."
+ Push-Location $repoRoot
+ try {
+ & $gradlew buildPlugin --console=plain
+ if ($LASTEXITCODE -ne 0) {
+ Write-Error "gradlew buildPlugin failed (exit $LASTEXITCODE)."
+ exit 1
+ }
+ } finally {
+ Pop-Location
+ }
+
+ if (-not (Test-Path $pluginZip)) {
+ Write-Error "$pluginZip still not found after 'gradlew buildPlugin' - check the buildPlugin task's output archive name/path."
+ exit 1
+ }
}
+New-Item -ItemType Directory -Path $outDir -Force | Out-Null
+
# --- Locate prerequisites ---------------------------------------------------
$sevenZip = Get-Command "7z.exe" -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Source
@@ -86,7 +109,7 @@ try {
exit 1
}
- # --- 2. Assemble scripts/install-intellij-ib-plugin.ps1 (standalone) ---
+ # --- 2. Assemble install-intellij-ib-plugin.ps1 (standalone) -----------
Write-Host "Assembling $standaloneScriptOut ..."
$pluginZipBase64 = [System.Convert]::ToBase64String([System.IO.File]::ReadAllBytes($pluginZip))
diff --git a/installer/install-common.ps1 b/installer/install-common.ps1
index b923260..3f09292 100644
--- a/installer/install-common.ps1
+++ b/installer/install-common.ps1
@@ -1,12 +1,14 @@
# Shared by both installer/standalone-footer.ps1 (assembled into the
-# self-contained scripts/install-intellij-ib-plugin.ps1) and
+# self-contained install-intellij-ib-plugin.ps1) and
# installer/sfx-footer.ps1 (assembled into the SFX installer's own
# install.ps1) via installer/build-installer.ps1 - edit this ONE file rather
# than both generated scripts.
-# RR = RustRover, IU = IntelliJ IDEA Ultimate. Community editions (IC) don't
-# carry com.intellij.modules.ultimate, which the Rust plugin requires there.
-$targetProductCodes = @("RR", "IU")
+# RR = RustRover, IU = IntelliJ IDEA Ultimate, CL = CLion (the CMake actions -
+# see cmake-support.xml), RD = Rider (the .NET actions - see
+# rider-support.xml). Community editions (IC) don't carry
+# com.intellij.modules.ultimate, which the Rust plugin requires there.
+$targetProductCodes = @("RR", "IU", "CL", "RD")
# Installs can land in quite a few places, so several roots are searched
# recursively rather than assuming one fixed layout:
@@ -30,7 +32,7 @@ $registryInstallLocations = @(
} | ForEach-Object {
Get-ItemProperty -Path $_.PSPath -ErrorAction SilentlyContinue
} | Where-Object {
- $_.DisplayName -match "RustRover|IntelliJ IDEA" -and $_.InstallLocation
+ $_.DisplayName -match "RustRover|IntelliJ IDEA|CLion|Rider" -and $_.InstallLocation
} | ForEach-Object { $_.InstallLocation } | Select-Object -Unique
$searchRoots = @(
@@ -80,7 +82,7 @@ function Stop-InstallerTranscriptIfRunning {
if (-not $installs) {
$searched = ($searchRoots | Select-Object -Unique) -join "; "
- $message = "No supported JetBrains IDE (RustRover or IntelliJ IDEA Ultimate) found. Searched: $searched."
+ $message = "No supported JetBrains IDE (RustRover, IntelliJ IDEA Ultimate, CLion or Rider) found. Searched: $searched."
if ($allProducts) {
$found = ($allProducts | ForEach-Object { "$($_.ProductName) [$($_.ProductCode)] at $($_.InfoPath)" }) -join "; "
$message += " Found (but not supported by this plugin): $found."
diff --git a/installer/sfx-header.ps1 b/installer/sfx-header.ps1
index de73806..6b47f6f 100644
--- a/installer/sfx-header.ps1
+++ b/installer/sfx-header.ps1
@@ -1,7 +1,7 @@
<#
Installs (or updates) the Incredibuild Build Acceleration plugin into every
-supported JetBrains IDE found on this machine (RustRover, and IntelliJ IDEA
-Ultimate). Meant to be run from inside the self-extracting installer,
+supported JetBrains IDE found on this machine (RustRover, IntelliJ IDEA
+Ultimate, CLion, and Rider). Meant to be run from inside the self-extracting installer,
alongside intellij-ib-plugin.zip.
Detects installs by reading each product's own product-info.json rather than
diff --git a/installer/standalone-header.ps1 b/installer/standalone-header.ps1
index 2defd0e..ef28994 100644
--- a/installer/standalone-header.ps1
+++ b/installer/standalone-header.ps1
@@ -1,8 +1,8 @@
<#
.SYNOPSIS
Installs (or updates) the "Incredibuild Build Acceleration" plugin into every
-supported JetBrains IDE found on this machine (RustRover, and IntelliJ IDEA
-Ultimate).
+supported JetBrains IDE found on this machine (RustRover, IntelliJ IDEA
+Ultimate, CLion, and Rider).
.DESCRIPTION
Fully self-contained: the plugin zip is embedded below as base64, so this is
diff --git a/rider/build.gradle.kts b/rider/build.gradle.kts
new file mode 100644
index 0000000..ee2252d
--- /dev/null
+++ b/rider/build.gradle.kts
@@ -0,0 +1,47 @@
+// The Rider-only adapter for this plugin's .NET support: a thin layer over Rider's frontend
+// solution model (which solution is open, its active configuration|platform, what's selected in
+// the Solution Explorer), compiled against Rider itself because those classes only exist in the
+// Rider product, never as a Marketplace plugin the root module could compile against alongside
+// the Rust/CMake ones. It is merged into the root plugin's jar (see pluginComposedModule in the
+// root build.gradle.kts), so this is still one plugin and one install - and only ever called
+// from code that rider-support.xml registers, i.e. only when running inside Rider.
+//
+// Deliberately depends on nothing in the root module (that would be a dependency cycle - the
+// root module depends on this one), and exposes only platform types in its API so the root
+// module never needs Rider classes on its own compile classpath.
+
+import org.gradle.api.tasks.bundling.AbstractArchiveTask
+
+plugins {
+ id("org.jetbrains.kotlin.jvm")
+ id("org.jetbrains.intellij.platform.module")
+}
+
+kotlin {
+ jvmToolchain(25)
+ // Same reason as the root module's identical setting.
+ compilerOptions {
+ freeCompilerArgs.add("-Xjvm-default=all")
+ }
+}
+
+dependencies {
+ intellijPlatform {
+ // Same 2026.2 (262) line as the root module's other compile targets.
+ rider("2026.2") {
+ useInstaller = false
+ }
+ // Rider's solution-host extensions (project.solutionFile, isExistingSolution, ...) and its
+ // Solution Explorer workspace model (ProjectModelEntity) live in this core content module,
+ // which isn't on the default compile classpath. It's a "required" module of Rider's core
+ // plugin, so it's always loaded at runtime in Rider.
+ bundledModule("intellij.rider.rdclient.dotnet")
+ bundledModule("intellij.rd.ide.model.generated")
+ }
+}
+
+// Same as the root build.gradle.kts: keep real timestamps on this module's classes, which end up
+// merged into the plugin's own jar, instead of Gradle's fixed 1980-02-01 placeholder.
+tasks.withType().configureEach {
+ isPreserveFileTimestamps = true
+}
diff --git a/rider/src/main/kotlin/com/incredibuild/ibplugin/rider/RiderSolutionModel.kt b/rider/src/main/kotlin/com/incredibuild/ibplugin/rider/RiderSolutionModel.kt
new file mode 100644
index 0000000..46c97e5
--- /dev/null
+++ b/rider/src/main/kotlin/com/incredibuild/ibplugin/rider/RiderSolutionModel.kt
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2026 Incredibuild
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.incredibuild.ibplugin.rider
+
+import com.intellij.openapi.actionSystem.DataContext
+import com.intellij.openapi.project.Project
+import com.jetbrains.rider.projectView.SolutionConfigurationManager
+import com.jetbrains.rider.projectView.isExistingSolution
+import com.jetbrains.rider.projectView.solutionFile
+import com.jetbrains.rider.projectView.solutionFilterPath
+import com.jetbrains.rider.projectView.workspace.ProjectModelEntity
+import com.jetbrains.rider.projectView.workspace.getProjectModelEntities
+import com.jetbrains.rider.projectView.workspace.isProject
+import com.jetbrains.rider.projectView.workspace.isSolution
+import com.jetbrains.rider.projectView.workspace.isSolutionFolder
+import com.jetbrains.rider.projectView.workspace.isUnloadedProject
+
+/** The solution configuration Rider's own build would use, e.g. "Debug" / "Any CPU". */
+class SolutionConfiguration(val configuration: String, val platform: String)
+
+/** What the Solution Explorer selection asks to be built - see [RiderSolutionModel.selection]. */
+sealed interface SolutionSelection {
+ /** The solution node itself is selected: build everything. */
+ data object WholeSolution : SolutionSelection
+
+ /** [projectFiles] are absolute paths of project files (.csproj, .vbproj, .fsproj, ...). */
+ class Projects(val projectFiles: List) : SolutionSelection
+
+ /** Nothing buildable is selected (e.g. a plain file or folder, or no Solution Explorer focus). */
+ data object Nothing : SolutionSelection
+}
+
+/**
+ * Read-only access to the parts of Rider's frontend solution model the Incredibuild .NET actions
+ * need. Everything here mirrors what Rider's own build actions consult
+ * ([com.jetbrains.rider.build.actions.BuildSolutionActionBase],
+ * [com.jetbrains.rider.build.actions.BuildSelectionActionBase]), so the accelerated build targets
+ * exactly what Rider's own one would - but deliberately stops short of Rider's
+ * [com.jetbrains.rider.build.BuildHost]: that hands the build to the ReSharper backend, which runs
+ * MSBuild in its own process tree, where Incredibuild can't wrap it.
+ *
+ * Only platform types cross this API, so the root module (compiled against IntelliJ IDEA, not
+ * Rider) can call it without Rider classes on its own classpath.
+ */
+object RiderSolutionModel {
+
+ /**
+ * Whether [project] is a real .sln/.slnx solution - not a folder opened as a "directory
+ * solution", a CMake project opened in Rider, or the temporary placeholder solution Rider
+ * opens with no solution at all, none of which have a solution file MSBuild could build.
+ */
+ fun hasSolution(project: Project): Boolean = project.isExistingSolution
+
+ /**
+ * The file to hand MSBuild for a whole-solution build. A solution filter (.slnf) wins when one
+ * is open, because that's what Rider's own build is scoped to then - and MSBuild accepts a
+ * .slnf directly, building only the projects it includes.
+ */
+ fun buildEntryPoint(project: Project): String? {
+ if (!hasSolution(project)) return null
+ project.solutionFilterPath?.let { return it.toString() }
+ return project.solutionFile.absolutePath
+ }
+
+ /** The actual .sln/.slnx file, even when a filter is open - see [buildEntryPoint]. */
+ fun solutionFile(project: Project): String? =
+ if (hasSolution(project)) project.solutionFile.absolutePath else null
+
+ /** The configuration|platform selected in Rider's toolbar, the one its own Build uses. */
+ fun activeConfiguration(project: Project): SolutionConfiguration? {
+ val active = SolutionConfigurationManager.tryGetInstance(project)?.activeConfigurationAndPlatform ?: return null
+ return SolutionConfiguration(active.configuration, active.platform)
+ }
+
+ /**
+ * What the selection in [dataContext] asks to build, the same way Rider's own "Build Selection"
+ * reads it: the solution node means the whole solution, project nodes mean those projects,
+ * and a solution folder means every project nested in it. Anything else (files, folders,
+ * references) isn't buildable by itself, matching Rider - which also only offers its own
+ * build-selection action on those three kinds of node.
+ */
+ fun selection(dataContext: DataContext): SolutionSelection {
+ val entities = dataContext.getProjectModelEntities()
+ if (entities.isEmpty()) return SolutionSelection.Nothing
+ if (entities.any { it.isSolution() }) return SolutionSelection.WholeSolution
+
+ val projectFiles = LinkedHashSet()
+ for (entity in entities) {
+ when {
+ entity.isProject() -> entity.projectFilePath()?.let(projectFiles::add)
+ entity.isSolutionFolder() -> collectProjects(entity, projectFiles)
+ }
+ }
+ return if (projectFiles.isEmpty()) SolutionSelection.Nothing else SolutionSelection.Projects(projectFiles.toList())
+ }
+
+ private fun collectProjects(folder: ProjectModelEntity, into: MutableSet) {
+ for (child in folder.childrenEntities) {
+ when {
+ child.isProject() -> child.projectFilePath()?.let(into::add)
+ child.isSolutionFolder() -> collectProjects(child, into)
+ }
+ }
+ }
+
+ /** An unloaded project can't be built by Rider either - skipped rather than handed to MSBuild. */
+ private fun ProjectModelEntity.projectFilePath(): String? =
+ if (isUnloadedProject()) null else url?.presentableUrl
+}
diff --git a/scripts/install-intellij-ib-plugin.ps1 b/scripts/install-intellij-ib-plugin.ps1
deleted file mode 100644
index 8db11a0..0000000
--- a/scripts/install-intellij-ib-plugin.ps1
+++ /dev/null
@@ -1,155 +0,0 @@
-<#
-.SYNOPSIS
-Installs (or updates) the "Incredibuild Build Acceleration" plugin into every
-supported JetBrains IDE found on this machine (RustRover, and IntelliJ IDEA
-Ultimate).
-
-.DESCRIPTION
-Fully self-contained: the plugin zip is embedded below as base64, so this is
-the only file needed. Run it with:
- powershell -ExecutionPolicy Bypass -File .\install-intellij-ib-plugin.ps1
-
-Detects installs by reading each product's own product-info.json rather than
-guessing the config folder name from the install folder's display name -
-that heuristic does NOT generalize across products (e.g. RustRover's config
-folder is "RustRover2026.2", a simple space-strip of its install folder name,
-but IntelliJ IDEA Ultimate's is "IntelliJIdea2026.2", which does not follow
-the same pattern at all).
-
-All detected target IDEs must be closed before running this script - it will
-refuse to proceed otherwise, since IntelliJ Platform IDEs load plugins at
-startup and don't reliably pick up a plugin folder replaced while already
-running.
-#>
-
-$ErrorActionPreference = "Stop"
-
-# Shared by both installer/standalone-footer.ps1 (assembled into the
-# self-contained scripts/install-intellij-ib-plugin.ps1) and
-# installer/sfx-footer.ps1 (assembled into the SFX installer's own
-# install.ps1) via installer/build-installer.ps1 - edit this ONE file rather
-# than both generated scripts.
-
-# RR = RustRover, IU = IntelliJ IDEA Ultimate. Community editions (IC) don't
-# carry com.intellij.modules.ultimate, which the Rust plugin requires there.
-$targetProductCodes = @("RR", "IU")
-
-# Installs can land in quite a few places, so several roots are searched
-# recursively rather than assuming one fixed layout:
-# - "C:\Program Files\JetBrains\" - the standalone installer's
-# default when run elevated.
-# - "%LOCALAPPDATA%\JetBrains\..." - covers both JetBrains Toolbox (which
-# nests several levels deeper, under "Toolbox\apps\\ch-0\")
-# and the standalone installer's own default when run WITHOUT admin
-# rights, which silently falls back to a per-user install here instead
-# of Program Files.
-# - Whatever the Windows registry's own Uninstall entries report as the
-# InstallLocation for a matching product - the authoritative source for
-# a custom install path (e.g. a different drive), which no fixed-path
-# guess can anticipate.
-$registryInstallLocations = @(
- "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall",
- "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall",
- "HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall"
-) | Where-Object { Test-Path $_ } | ForEach-Object {
- Get-ChildItem -Path $_ -ErrorAction SilentlyContinue
-} | ForEach-Object {
- Get-ItemProperty -Path $_.PSPath -ErrorAction SilentlyContinue
-} | Where-Object {
- $_.DisplayName -match "RustRover|IntelliJ IDEA" -and $_.InstallLocation
-} | ForEach-Object { $_.InstallLocation } | Select-Object -Unique
-
-$searchRoots = @(
- "C:\Program Files\JetBrains",
- (Join-Path $env:LOCALAPPDATA "JetBrains")
-) + $registryInstallLocations
-
-# Every JetBrains product found (any productCode), so a "not found" error can
-# report what actually IS here - e.g. a Community edition, which is filtered
-# out below but is useful to see rather than a bare "nothing found".
-$allProducts = $searchRoots | Where-Object { Test-Path $_ } | ForEach-Object {
- Get-ChildItem -Path $_ -Recurse -Filter "product-info.json" -ErrorAction SilentlyContinue
-} | ForEach-Object {
- $infoPath = $_.FullName
- try {
- $info = Get-Content $infoPath -Raw | ConvertFrom-Json
- } catch {
- return
- }
- [PSCustomObject]@{
- InfoPath = $infoPath
- ProductName = $info.name
- ProductCode = $info.productCode
- DataDirectoryName = $info.dataDirectoryName
- Launch = $info.launch
- }
-}
-
-$installs = $allProducts | Where-Object { $targetProductCodes -contains $_.ProductCode } | ForEach-Object {
- $product = $_
- $winLaunch = $product.Launch | Where-Object { $_.os -eq "Windows" } | Select-Object -First 1
- if (-not $winLaunch) { return }
-
- [PSCustomObject]@{
- Name = Split-Path -Leaf (Split-Path -Parent $product.InfoPath)
- DataDirectoryName = $product.DataDirectoryName
- ProcessName = [System.IO.Path]::GetFileNameWithoutExtension($winLaunch.launcherPath)
- }
-} | Sort-Object -Property DataDirectoryName -Unique
-
-# A no-op if no transcript is running (Start-Transcript is only called in the
-# SFX installer's copy of this logic, not the standalone script's), safe to
-# call unconditionally from both under $ErrorActionPreference = "Stop".
-function Stop-InstallerTranscriptIfRunning {
- try { Stop-Transcript | Out-Null } catch {}
-}
-
-if (-not $installs) {
- $searched = ($searchRoots | Select-Object -Unique) -join "; "
- $message = "No supported JetBrains IDE (RustRover or IntelliJ IDEA Ultimate) found. Searched: $searched."
- if ($allProducts) {
- $found = ($allProducts | ForEach-Object { "$($_.ProductName) [$($_.ProductCode)] at $($_.InfoPath)" }) -join "; "
- $message += " Found (but not supported by this plugin): $found."
- }
- Write-Error $message
- Stop-InstallerTranscriptIfRunning
- exit 1
-}
-
-$anyRunning = $false
-foreach ($install in $installs) {
- if (Get-Process -Name $install.ProcessName -ErrorAction SilentlyContinue) {
- Write-Error "$($install.Name) is currently running. Close it, then re-run this installer."
- $anyRunning = $true
- }
-}
-if ($anyRunning) {
- Stop-InstallerTranscriptIfRunning
- exit 1
-}
-
-$pluginZipBase64 = @'
-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
-'@
-
-$tempZip = Join-Path $env:TEMP "intellij-ib-plugin-install.zip"
-$pluginZipBytes = [System.Convert]::FromBase64String(($pluginZipBase64 -replace '\s', ''))
-[System.IO.File]::WriteAllBytes($tempZip, $pluginZipBytes)
-
-foreach ($install in $installs) {
- $pluginsDir = Join-Path $env:APPDATA "JetBrains\$($install.DataDirectoryName)\plugins"
- if (-not (Test-Path $pluginsDir)) {
- New-Item -ItemType Directory -Path $pluginsDir -Force | Out-Null
- }
-
- $targetPluginDir = Join-Path $pluginsDir "intellij-ib-plugin"
- if (Test-Path $targetPluginDir) {
- Remove-Item -Recurse -Force $targetPluginDir
- }
-
- Expand-Archive -Path $tempZip -DestinationPath $pluginsDir -Force
- Write-Host "Installed into $($install.Name) ($pluginsDir)."
-}
-
-Remove-Item $tempZip -Force
-Write-Host "Install complete. Start the IDE to use the Incredibuild plugin."
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 9841bdf..da28989 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -2,6 +2,12 @@ import org.jetbrains.intellij.platform.gradle.extensions.intellijPlatform
rootProject.name = "intellij-ib-plugin"
+// Rider's frontend API (solution model, active solution configuration) ships inside the Rider
+// product itself rather than as a separately published plugin, so unlike the Rust/CMake halves
+// it can't be added to the root module's IntelliJ IDEA Ultimate compile target - see
+// rider/build.gradle.kts.
+include(":rider")
+
pluginManagement {
plugins {
id("org.jetbrains.kotlin.jvm") version "2.4.10"
diff --git a/src/main/kotlin/com/incredibuild/ibplugin/actions/CargoBuildCommand.kt b/src/main/kotlin/com/incredibuild/ibplugin/actions/CargoBuildCommand.kt
index 7d6928b..6a2935a 100644
--- a/src/main/kotlin/com/incredibuild/ibplugin/actions/CargoBuildCommand.kt
+++ b/src/main/kotlin/com/incredibuild/ibplugin/actions/CargoBuildCommand.kt
@@ -15,15 +15,17 @@
*/
package com.incredibuild.ibplugin.actions
-import com.incredibuild.ibplugin.actions.cmake.hasCMakeWorkspace
import com.intellij.execution.ExecutionTargetManager
import com.intellij.execution.RunManager
import com.intellij.openapi.application.ReadAction
+import com.intellij.openapi.components.service
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.project.Project
+import com.intellij.openapi.util.SystemInfo
import com.intellij.openapi.util.registry.Registry
import com.incredibuild.ibplugin.settings.IncredibuildSettings
import org.rust.cargo.project.model.cargoProjectsIfCreated
+import org.rust.cargo.project.settings.RustProjectSettingsService
import org.rust.cargo.runconfig.buildtool.CargoBuildManager
import org.rust.cargo.runconfig.command.CargoCommandConfiguration
import org.rust.cargo.runconfig.profiles.CargoBuildProfile
@@ -31,6 +33,7 @@ import org.rust.cargo.runconfig.profiles.RsDefaultProfileExecutionTarget
import org.rust.cargo.toolchain.CargoCommandLine
import org.rust.cargo.toolchain.RustChannel
import org.rust.cargo.toolchain.tools.Cargo
+import java.io.File
private val LOG = Logger.getInstance("com.incredibuild.ibplugin.actions.CargoBuildCommand")
@@ -164,8 +167,46 @@ private fun replacingCargoFlag(
return cargoArgs + replacement + passthrough
}
-/** A cargo invocation to accelerate: the command to run and the directory to run it in. */
-internal data class CargoInvocation(val command: String, val workingDirectory: String)
+/**
+ * A cargo invocation to accelerate: the command to run and the directory to run it in.
+ *
+ * [command] starts with a bare `cargo` - BuildConsole's `/command="..."` can't safely carry a
+ * quoted executable path of its own - so [cargoBinDir], the directory of the cargo RustRover is
+ * configured with, is put first on PATH for the build instead (see IncredibuildRunner.prepareBuild).
+ * Without it the build only works where that directory happens to be on PATH already, which
+ * rustup doesn't guarantee and a toolchain configured elsewhere in RustRover never is: the
+ * build then fails with "'cargo' is not recognized". Null if no cargo could be found, leaving
+ * PATH as it is.
+ */
+internal data class CargoInvocation(
+ val command: String,
+ val workingDirectory: String,
+ val cargoBinDir: String? = null
+)
+
+internal val CARGO_EXECUTABLE_NAME = if (SystemInfo.isWindows) "cargo.exe" else "cargo"
+
+/**
+ * The cargo executable RustRover itself would run: the one in the project's configured Rust
+ * toolchain, falling back to rustup's default `~/.cargo/bin` - or null if neither exists.
+ */
+internal fun resolveCargoExecutable(project: Project): String? {
+ project.service().toolchain?.let { toolchain ->
+ val path = toolchain.pathToCargoExecutable("cargo")
+ if (File(path.toString()).exists()) {
+ return path.toString()
+ }
+ }
+
+ val defaultCargo = File(System.getProperty("user.home"), ".cargo${File.separator}bin${File.separator}$CARGO_EXECUTABLE_NAME")
+ if (defaultCargo.exists()) {
+ return defaultCargo.absolutePath
+ }
+
+ return null
+}
+
+private fun cargoBinDir(project: Project): String? = resolveCargoExecutable(project)?.let { File(it).parent }
/**
* Whether [project] has a Cargo workspace at all - used to hide the Cargo-flavoured
@@ -179,18 +220,6 @@ internal data class CargoInvocation(val command: String, val workingDirectory: S
internal fun hasCargoWorkspace(project: Project): Boolean =
project.cargoProjectsIfCreated?.hasAtLeastOneValidProject == true
-/**
- * [label] as-is, unless [project] has both a Cargo and a CMake workspace loaded at once (e.g.
- * Rust with a CMake-built C dependency, or CMake + Corrosion) - a case the plain per-workspace
- * `update()` checks don't disambiguate, since both the Cargo and CMake actions would otherwise
- * show the identical "Build"/"Rebuild" text side by side in the same Incredibuild menu. Only
- * reached once a caller's own `update()` has already confirmed its workspace is present, so
- * this never needs to decide which (if either) action should be hidden - just how to label the
- * one that is shown.
- */
-internal fun disambiguatedLabel(project: Project, label: String, suffix: String): String =
- if (hasCargoWorkspace(project) && hasCMakeWorkspace(project)) "$label ($suffix)" else label
-
/**
* The cargo invocation to accelerate, taken from what RustRover's own Build Project would
* run for the current state of the IDE rather than reconstructed from scratch.
@@ -310,7 +339,7 @@ private fun renderInvocation(
?: return null
// Blank entries are dropped rather than joined: the resolved argument list can contain
// empty strings, and those would render as stray double spaces in the command.
- return CargoInvocation(rendered.filter { it.isNotBlank() }.joinToString(" "), workingDirectory)
+ return CargoInvocation(rendered.filter { it.isNotBlank() }.joinToString(" "), workingDirectory, cargoBinDir(project))
}
/**
@@ -354,7 +383,7 @@ private fun workspaceBuildInvocation(project: Project, jobCount: Int): CargoInvo
if (Registry.`is`(COMPILE_ALL_TARGETS_REGISTRY_KEY, true)) args += "--all-targets"
selectedProfile(project)?.let { args += listOf("--profile", it) }
val command = "cargo build " + withJobCount(withAlwaysColor(args), jobCount).joinToString(" ")
- return CargoInvocation(command, project.basePath ?: ".")
+ return CargoInvocation(command, project.basePath ?: ".", cargoBinDir(project))
}
/**
diff --git a/src/main/kotlin/com/incredibuild/ibplugin/actions/IncredibuildProjectTaskRunner.kt b/src/main/kotlin/com/incredibuild/ibplugin/actions/IncredibuildProjectTaskRunner.kt
index e69cbac..1a3dc24 100644
--- a/src/main/kotlin/com/incredibuild/ibplugin/actions/IncredibuildProjectTaskRunner.kt
+++ b/src/main/kotlin/com/incredibuild/ibplugin/actions/IncredibuildProjectTaskRunner.kt
@@ -15,6 +15,7 @@
*/
package com.incredibuild.ibplugin.actions
+import com.intellij.build.BuildProgressListener
import com.intellij.build.BuildViewManager
import com.intellij.execution.process.ProcessEvent
import com.intellij.execution.process.ProcessListener
@@ -40,6 +41,7 @@ import org.rust.cargo.runconfig.buildtool.CargoBuildTaskRunner
import org.rust.cargo.runconfig.command.CargoCommandConfiguration
import org.rust.cargo.project.model.CargoProject
import java.io.File
+import java.lang.reflect.InvocationTargetException
import java.nio.file.Path
import java.util.concurrent.Future
@@ -138,6 +140,13 @@ class IncredibuildProjectTaskRunner : ProjectTaskRunner() {
// (several Cargo projects in one IDE project) they must not compete for the build
// tool window.
for (planned in plannedBuilds) {
+ // One Incredibuild build at a time, as for the Incredibuild menu's builds - this one
+ // reports into the Build tool window rather than ours, but would still overlap one
+ // running there. Checked before the clean too, which must not run under a build.
+ if (!IncredibuildRunner.awaitNoBuildRunning(project)) {
+ promise.setResult(TaskRunnerResults.ABORTED)
+ return
+ }
if (!planned.isIncrementalBuild && !cleanBeforeRebuild(project, planned.buildConfiguration)) {
promise.setResult(TaskRunnerResults.FAILURE)
return
@@ -225,10 +234,10 @@ class IncredibuildProjectTaskRunner : ProjectTaskRunner() {
*
* This reuses the Rust plugin's build machinery wholesale and substitutes only the
* process: [CargoBuildManager.execute] supplies the progress indicator, build queue and
- * result future, and [CargoBuildAdapter.attachToProcessHandler] parses cargo's JSON
- * output into build-tree events. The adapter requires the handler to already be on the
- * context, and starts the process itself - so it must be attached last and the handler
- * must not be started here.
+ * result future, and [CargoBuildAdapter] parses cargo's JSON output into build-tree
+ * events. Attaching the adapter starts the process (see [attachCargoBuildAdapter]), and the
+ * adapter requires the handler to already be on the context - so it must be attached last
+ * and the handler must not be started here.
*
* Returns null if the command could not be prepared, leaving the caller to fall back to
* an ordinary Cargo build.
@@ -245,7 +254,8 @@ class IncredibuildProjectTaskRunner : ProjectTaskRunner() {
project,
installFolder,
resolved.invocation.command,
- resolved.invocation.workingDirectory
+ resolved.invocation.workingDirectory,
+ resolved.invocation.cargoBinDir
)
if (prepared !is IncredibuildRunner.PreparedBuild.Ready) {
if (prepared is IncredibuildRunner.PreparedBuild.Failed) LOG.warn(prepared.message)
@@ -286,7 +296,7 @@ class IncredibuildProjectTaskRunner : ProjectTaskRunner() {
override fun processTerminated(event: ProcessEvent) = prepared.cleanUp()
})
runningContext.processHandler = processHandler
- CargoBuildAdapter.attachToProcessHandler(runningContext, project.service())
+ attachCargoBuildAdapter(runningContext, project.service())
}
}
@@ -329,6 +339,44 @@ class IncredibuildProjectTaskRunner : ProjectTaskRunner() {
.onError { promise.setError(it) }
}
+ /**
+ * Attaches the Rust plugin's [CargoBuildAdapter] to [context]'s process handler and starts the
+ * process, the way that plugin's own `CargoBuildManager` does in whichever version is loaded -
+ * the API changed within the IDE lines this plugin supports:
+ * - 2026.2 (262): `CargoBuildAdapter.attachToProcessHandler(context, listener)`, which does
+ * all of it; the constructor is private;
+ * - 2026.1 (261): no such method - the caller constructs the adapter (public constructor),
+ * adds it as a process listener and starts the process.
+ *
+ * Both are looked up reflectively so neither is referenced directly: a direct call to the 262
+ * method compiled fine against 262 but failed with NoSuchMethodError on 2026.1 IDEs, which
+ * the Plugin Verifier caught once 2026.1 was verified.
+ */
+ private fun attachCargoBuildAdapter(context: CargoBuildContext, listener: BuildProgressListener) {
+ try {
+ val attach = runCatching {
+ CargoBuildAdapter.Companion::class.java.getMethod(
+ "attachToProcessHandler",
+ CargoBuildContext::class.java,
+ BuildProgressListener::class.java
+ )
+ }.getOrNull()
+ if (attach != null) {
+ attach.invoke(CargoBuildAdapter.Companion, context, listener)
+ return
+ }
+
+ val processHandler = checkNotNull(context.processHandler) { "The process handler must be set first" }
+ val adapter = CargoBuildAdapter::class.java
+ .getConstructor(CargoBuildContext::class.java, BuildProgressListener::class.java)
+ .newInstance(context, listener)
+ processHandler.addProcessListener(adapter as ProcessListener)
+ processHandler.startNotify()
+ } catch (e: InvocationTargetException) {
+ throw e.targetException
+ }
+ }
+
/**
* The Rust plugin's own registered build runner, taken from the extension point rather
* than constructed, so it is the same instance the platform would otherwise have used.
diff --git a/src/main/kotlin/com/incredibuild/ibplugin/actions/IncredibuildRunner.kt b/src/main/kotlin/com/incredibuild/ibplugin/actions/IncredibuildRunner.kt
index f2f9285..d697b50 100644
--- a/src/main/kotlin/com/incredibuild/ibplugin/actions/IncredibuildRunner.kt
+++ b/src/main/kotlin/com/incredibuild/ibplugin/actions/IncredibuildRunner.kt
@@ -17,10 +17,12 @@ package com.incredibuild.ibplugin.actions
import com.incredibuild.ibplugin.actions.cmake.CMakeBuildKind
import com.incredibuild.ibplugin.actions.cmake.CMakeBuildResolution
-import com.incredibuild.ibplugin.actions.cmake.ResolvedCMakeBuild
import com.incredibuild.ibplugin.actions.cmake.deriveCMakeBuild
import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.execution.impl.ConsoleViewImpl
+import com.intellij.execution.ui.ConsoleViewContentType
+import com.intellij.notification.NotificationGroupManager
+import com.intellij.notification.NotificationType
import com.intellij.execution.process.ColoredProcessHandler
import com.intellij.execution.process.KillableProcessHandler
import com.intellij.execution.process.OSProcessHandler
@@ -29,6 +31,7 @@ import com.intellij.execution.process.ProcessListener
import com.intellij.execution.util.ExecUtil
import com.intellij.ide.BrowserUtil
import com.intellij.openapi.application.ApplicationManager
+import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.progress.Task
@@ -37,8 +40,11 @@ import com.intellij.openapi.ui.Messages
import com.intellij.openapi.util.Key
import com.intellij.openapi.util.SystemInfo
import com.intellij.openapi.wm.ToolWindowManager
+import com.intellij.util.concurrency.AppExecutorUtil
import com.incredibuild.ibplugin.analytics.PostHogClient
import java.io.File
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicBoolean
/** Matches the "Build ID: {}" line BuildConsole prints to stdout when a build starts.
* Windows only - ib_console on Linux never prints a build-id, so this simply never matches
@@ -60,6 +66,19 @@ private const val MINIMUM_RUST_BUILD_VERSION = "10.37.1"
// "10.37.0.12597" vs "3.18.0").
private const val MINIMUM_RUST_BUILD_VERSION_LINUX = "4.29.3"
+/** Declared in plugin.xml. */
+private const val NOTIFICATION_GROUP_ID = "Incredibuild"
+
+private const val STATUS_POLL_INTERVAL_MS = 250L
+
+/**
+ * How long a build waiting for a running one to stop gives it before giving up - see
+ * [IncredibuildRunner.whenNoBuildRunning]. Generous, because a graceful Incredibuild cancellation
+ * lets the build system wind down on its own rather than killing it, but finite: a process that
+ * never exits must not leave the new build - or the IDE build waiting on it - pending forever.
+ */
+private const val STOP_TIMEOUT_MS = 120_000L
+
/** A POSIX shell variable name - see [IncredibuildRunner]'s CMake Linux environment export. */
internal val VALID_SHELL_IDENTIFIER = Regex("[A-Za-z_][A-Za-z0-9_]*")
@@ -125,7 +144,7 @@ object IncredibuildRunner {
}
/** The "Incredibuild wasn't found" dialog, shared by [ensureIncredibuildReady] (Rust) and
- * [ensureIncredibuildInstalledForCMake] - identical either way, since it's the same
+ * [ensureIncredibuildInstalled] (CMake, MSBuild) - identical either way, since it's the same
* Incredibuild install being looked for regardless of which language triggered the build. */
private fun showNotFoundDialog(project: Project, actionType: String) {
PostHogClient.capture("incredibuild_not_found", mapOf("action" to actionType))
@@ -171,7 +190,14 @@ object IncredibuildRunner {
override fun run(indicator: ProgressIndicator) {
val installFolder = ensureIncredibuildReady(project, actionType) ?: return
val resolved = invocation()
- buildViaIncredibuild(project, installFolder, resolved.command, resolved.workingDirectory, actionType)
+ buildViaIncredibuild(
+ project,
+ installFolder,
+ resolved.command,
+ resolved.workingDirectory,
+ actionType,
+ cargoBinDir = resolved.cargoBinDir
+ )
}
})
}
@@ -190,14 +216,12 @@ object IncredibuildRunner {
cargoCommand: String,
workingDirectory: String,
actionType: String,
- onFinished: ((exitCode: Int) -> Unit)? = null
+ onFinished: ((exitCode: Int) -> Unit)? = null,
+ cargoBinDir: String? = null
) {
- when (val prepared = prepareBuild(project, installFolder, cargoCommand, workingDirectory)) {
+ when (val prepared = prepareBuild(project, installFolder, cargoCommand, workingDirectory, cargoBinDir)) {
is PreparedBuild.Failed -> showErrorOnEdt(project, prepared.message)
- is PreparedBuild.Ready -> launchBuild(project, prepared.commandLine, actionType) { exitCode ->
- prepared.cleanUp()
- onFinished?.invoke(exitCode)
- }
+ is PreparedBuild.Ready -> launchBuild(project, prepared, actionType, onFinished = onFinished)
}
}
@@ -214,17 +238,34 @@ object IncredibuildRunner {
class Failed(val message: String) : PreparedBuild
}
+ /**
+ * [cargoBinDir], if given, is put first on the build's PATH, so the bare `cargo` that
+ * [cargoCommand] starts with is the cargo RustRover is configured with - see
+ * [CargoInvocation.cargoBinDir]. BuildConsole/ib_console run the command in the environment
+ * they're launched with, so setting it on this process is enough.
+ */
internal fun prepareBuild(
project: Project,
installFolder: File,
cargoCommand: String,
- workingDirectory: String
- ): PreparedBuild =
- if (SystemInfo.isLinux) {
+ workingDirectory: String,
+ cargoBinDir: String? = null
+ ): PreparedBuild {
+ val prepared = if (SystemInfo.isLinux) {
prepareBuildLinux(project, installFolder, cargoCommand, workingDirectory)
} else {
prepareBuildWindows(project, installFolder, cargoCommand, workingDirectory)
}
+ if (prepared is PreparedBuild.Ready && cargoBinDir != null) {
+ val currentPath = prepared.commandLine.environment["PATH"] ?: System.getenv("PATH")
+ prepared.commandLine.withEnvironment("PATH", pathWithFirst(cargoBinDir, currentPath))
+ }
+ return prepared
+ }
+
+ /** [currentPath] with [directory] put first, using the OS's PATH separator. */
+ internal fun pathWithFirst(directory: String, currentPath: String?): String =
+ if (currentPath.isNullOrEmpty()) directory else directory + File.pathSeparator + currentPath
private fun prepareBuildWindows(
project: Project,
@@ -368,24 +409,86 @@ object IncredibuildRunner {
return PreparedBuild.Ready(commandLine) { scriptFile.delete() }
}
+ /**
+ * Runs [prepared] in the Incredibuild tool window - unless another build is still running
+ * (see [run]) - and deletes its temporary scripts once it ends, or straight away if it
+ * never starts. [onFinished] is only called for a build that actually ran.
+ */
private fun launchBuild(
project: Project,
- commandLine: GeneralCommandLine,
+ prepared: PreparedBuild.Ready,
actionType: String,
- onFinished: (exitCode: Int) -> Unit
+ presentation: AcceleratedBuildPresentation? = null,
+ onFinished: ((exitCode: Int) -> Unit)? = null
) {
- PostHogClient.capture("build_started", mapOf("action" to actionType))
- PostHogClient.captureOnce(
- "com.incredibuild.ibplugin.analytics.firstBuildSent",
- "first_build",
- mapOf("action" to actionType)
- )
-
ApplicationManager.getApplication().invokeLater {
- run(project, commandLine, "Build", onFinished)
+ run(
+ project,
+ prepared.commandLine,
+ presentation?.contentName ?: "Build",
+ presentation?.banner,
+ onNotStarted = prepared.cleanUp,
+ onStarted = { processHandler ->
+ PostHogClient.capture("build_started", mapOf("action" to actionType))
+ PostHogClient.captureOnce(
+ "com.incredibuild.ibplugin.analytics.firstBuildSent",
+ "first_build",
+ mapOf("action" to actionType)
+ )
+ if (presentation != null) {
+ trackInStatusBar(project, processHandler, presentation)
+ }
+ }
+ ) { exitCode ->
+ prepared.cleanUp()
+ onFinished?.invoke(exitCode)
+ }
}
}
+ /**
+ * Keeps a "[AcceleratedBuildPresentation.progressTitle]" progress in the status bar for as
+ * long as [processHandler] runs - the IDE's usual "something is building" signal, so an
+ * accelerated build is visible even with the Incredibuild tool window hidden - and reports
+ * the outcome as a notification once it ends. Cancelling the progress stops the build the
+ * same graceful way Stop Build does ([OSProcessHandler.destroyProcess] on our handler).
+ */
+ private fun trackInStatusBar(
+ project: Project,
+ processHandler: OSProcessHandler,
+ presentation: AcceleratedBuildPresentation
+ ) {
+ ProgressManager.getInstance().run(object : Task.Backgroundable(project, presentation.progressTitle, true) {
+ override fun run(indicator: ProgressIndicator) {
+ indicator.isIndeterminate = true
+ indicator.text = presentation.progressTitle
+ var cancelHandled = false
+ while (!processHandler.waitFor(STATUS_POLL_INTERVAL_MS)) {
+ if (indicator.isCanceled && !cancelHandled) {
+ cancelHandled = true
+ processHandler.destroyProcess()
+ }
+ // Also reflects a stop from Stop Build or the tool window, not just this
+ // progress's own cancel button.
+ if (processHandler.wasStopRequested()) {
+ indicator.text = "Stopping Incredibuild build..."
+ }
+ }
+ val exitCode = processHandler.exitCode
+ val (content, type) = when {
+ processHandler.wasStopRequested() -> "${presentation.contentName} was stopped." to NotificationType.INFORMATION
+ exitCode == 0 -> "${presentation.contentName} succeeded." to NotificationType.INFORMATION
+ else -> "${presentation.contentName} failed (exit code $exitCode). See the Incredibuild tool window for details." to
+ NotificationType.ERROR
+ }
+ NotificationGroupManager.getInstance()
+ .getNotificationGroup(NOTIFICATION_GROUP_ID)
+ .createNotification("Incredibuild", content, type)
+ .notify(project)
+ }
+ })
+ }
+
private fun showErrorOnEdt(project: Project, message: String) {
ApplicationManager.getApplication().invokeLater {
Messages.showErrorDialog(project, message, "Incredibuild")
@@ -407,17 +510,42 @@ object IncredibuildRunner {
* exit code - used both to chain a plain "cargo clean" into an Incredibuild-accelerated
* build for Rebuild, and to report build success back to the IDE's build pipeline when
* the build was started from there (see [IncredibuildProjectTaskRunner]).
+ *
+ * [banner], if given, is printed at the top of the console before any build output.
+ *
+ * Must be called on the EDT. Only one build runs at a time (see [whenNoBuildRunning]), so
+ * this may start the process later - once a running build the user chose to stop has
+ * stopped - or not at all: [onStarted] gets the running process, [onNotStarted] is called
+ * instead (and [onFinished] never is) if the user cancels, or the running build doesn't stop in time.
*/
fun run(
project: Project,
commandLine: GeneralCommandLine,
contentName: String,
+ banner: String? = null,
+ onNotStarted: () -> Unit = {},
+ onStarted: (OSProcessHandler) -> Unit = {},
onFinished: ((exitCode: Int) -> Unit)? = null
) {
+ whenNoBuildRunning(project, onNotStarted) {
+ onStarted(start(project, commandLine, contentName, banner, onFinished))
+ }
+ }
+
+ private fun start(
+ project: Project,
+ commandLine: GeneralCommandLine,
+ contentName: String,
+ banner: String?,
+ onFinished: ((exitCode: Int) -> Unit)?
+ ): OSProcessHandler {
val processHandler = createProcessHandler(commandLine)
val console = ConsoleViewImpl(project, true)
console.attachToProcess(processHandler)
+ if (banner != null) {
+ console.print(banner + "\n\n", ConsoleViewContentType.SYSTEM_OUTPUT)
+ }
val toolWindow = requireNotNull(ToolWindowManager.getInstance(project).getToolWindow("Incredibuild")) {
"The Incredibuild tool window is declared in plugin.xml and should always be registered."
@@ -436,6 +564,7 @@ object IncredibuildRunner {
})
}
processHandler.startNotify()
+ return processHandler
}
/**
@@ -494,9 +623,14 @@ object IncredibuildRunner {
*/
private class ColoredIncredibuildProcessHandler(
commandLine: GeneralCommandLine
- ) : ColoredProcessHandler(commandLine) {
+ ) : ColoredProcessHandler(commandLine), IncredibuildProcessHandler {
+
+ @Volatile
+ override var stopRequested = false
+ private set
override fun destroyProcessImpl() {
+ stopRequested = true
if (!requestGracefulStop()) super.destroyProcessImpl()
}
}
@@ -519,17 +653,36 @@ object IncredibuildRunner {
*/
private class RawIncredibuildProcessHandler(
commandLine: GeneralCommandLine
- ) : KillableProcessHandler(commandLine) {
+ ) : KillableProcessHandler(commandLine), IncredibuildProcessHandler {
+
+ @Volatile
+ override var stopRequested = false
+ private set
override fun destroyProcessImpl() {
+ stopRequested = true
if (!requestGracefulStop()) super.destroyProcessImpl()
}
override fun killProcess() {
+ stopRequested = true
if (!requestGracefulStop()) super.killProcess()
}
}
+ /**
+ * Implemented by both handlers above, so whoever reports a build's outcome can tell a build
+ * that was stopped from one that failed. Every stop route - Stop Build, the tool windows' own
+ * stop buttons, cancelling the status-bar progress - ends in [destroyProcessImpl] or
+ * `killProcess` on the handler, which is where it's recorded; a stopped build's exit code
+ * alone (-1, or whatever Incredibuild exits with when cancelled) looks just like a failure.
+ */
+ private interface IncredibuildProcessHandler {
+ val stopRequested: Boolean
+ }
+
+ private fun OSProcessHandler.wasStopRequested(): Boolean = (this as? IncredibuildProcessHandler)?.stopRequested == true
+
/**
* Asks Incredibuild to cancel the running build by build-id, returning false if there
* is no build-id to cancel by or the request could not be dispatched - in which case
@@ -556,6 +709,101 @@ object IncredibuildRunner {
* by [StopBuildAction] to disable itself when there's nothing to stop. */
fun isBuildRunning(): Boolean = activeProcessHandler?.isProcessTerminated == false
+ /**
+ * One build at a time: calls [start] straight away if nothing is running; otherwise offers to
+ * stop the running build, and if the user agrees, calls [start] once it has actually stopped -
+ * never while it's still winding down, so the two never overlap. [onDeclined] if they cancel,
+ * or if it hasn't stopped within [STOP_TIMEOUT_MS] (the user is told why).
+ * Without this, a second build replaces the first one's tool window tab (its output disappears
+ * while it's still running) and becomes the only one Stop Build can reach; and while Windows'
+ * BuildConsole would queue it behind the first ("Waiting for a build to complete..."), nothing
+ * guarantees ib_console does the same.
+ *
+ * Checked in [run], on the EDT, and [start] registers the build as the active one in the same
+ * EDT event - so two quick clicks can't both get past it. After a stop the check runs again
+ * before starting, in case something else started meanwhile. A Rebuild's chained build is
+ * unaffected: its clean has already ended by the time the build starts. Builds started from a
+ * background thread wait with [awaitNoBuildRunning] instead.
+ */
+ private fun whenNoBuildRunning(project: Project, onDeclined: () -> Unit, start: () -> Unit) {
+ val running = activeProcessHandler?.takeUnless { it.isProcessTerminated }
+ if (running == null) {
+ start()
+ return
+ }
+ if (!askToStopRunningBuild(project)) {
+ onDeclined()
+ return
+ }
+ // Whichever comes first - the old build ending, or the timeout - decides; the other is ignored.
+ val settled = AtomicBoolean()
+ val retry = {
+ if (settled.compareAndSet(false, true)) {
+ ApplicationManager.getApplication().invokeLater { whenNoBuildRunning(project, onDeclined, start) }
+ }
+ }
+ running.addProcessListener(object : ProcessListener {
+ override fun processTerminated(event: ProcessEvent) = retry()
+ })
+ // It may have ended between the check above and adding the listener, which then never fires.
+ if (running.isProcessTerminated) retry()
+ AppExecutorUtil.getAppScheduledExecutorService().schedule({
+ if (settled.compareAndSet(false, true)) {
+ ApplicationManager.getApplication().invokeLater {
+ notifyStopTimedOut(project)
+ onDeclined()
+ }
+ }
+ }, STOP_TIMEOUT_MS, TimeUnit.MILLISECONDS)
+ stopActiveProcess(project)
+ }
+
+ /**
+ * [whenNoBuildRunning] for a build started from a background thread - the IDE's build pipeline
+ * (see [IncredibuildProjectTaskRunner]), which reports its own outcome and so must wait here
+ * rather than be called back. Blocks until nothing is running and returns true, or returns
+ * false if the user chose not to stop the running build, or it didn't stop within
+ * [STOP_TIMEOUT_MS] - the caller then reports its own build as aborted.
+ */
+ internal fun awaitNoBuildRunning(project: Project): Boolean {
+ while (true) {
+ val running = activeProcessHandler?.takeUnless { it.isProcessTerminated } ?: return true
+ var stop = false
+ ApplicationManager.getApplication().invokeAndWait({ stop = askToStopRunningBuild(project) }, ModalityState.any())
+ if (!stop) return false
+ PostHogClient.capture("stop_build_clicked", mapOf("hadActiveProcess" to true))
+ // Graceful where possible - see stopActiveProcess.
+ running.destroyProcess()
+ if (!running.waitFor(STOP_TIMEOUT_MS)) {
+ notifyStopTimedOut(project)
+ return false
+ }
+ }
+ }
+
+ /** Tells the user a build waiting for the running one to stop gave up - see [STOP_TIMEOUT_MS]. */
+ private fun notifyStopTimedOut(project: Project) {
+ NotificationGroupManager.getInstance()
+ .getNotificationGroup(NOTIFICATION_GROUP_ID)
+ .createNotification(
+ "Incredibuild",
+ "The running Incredibuild build didn't stop within ${STOP_TIMEOUT_MS / 1000} seconds, so the new " +
+ "build wasn't started. Stop the running build and try again.",
+ NotificationType.WARNING
+ )
+ .notify(project)
+ }
+
+ private fun askToStopRunningBuild(project: Project): Boolean =
+ Messages.showYesNoDialog(
+ project,
+ "An Incredibuild build is already running. Stop it and start this one once it has stopped?",
+ "Incredibuild",
+ "Stop Running Build",
+ "Cancel",
+ Messages.getWarningIcon()
+ ) == Messages.YES
+
/**
* Stops whichever build/clean process is currently running, if any.
*
@@ -599,28 +847,80 @@ object IncredibuildRunner {
}
// -----------------------------------------------------------------------------------
- // CMake (CLion) support - mirrors the Cargo/Rust flow above, but a cmake invocation is
- // built from a resolved executable + argument list + environment (see
- // com.incredibuild.ibplugin.actions.cmake.CMakeBuildCommand) rather than a single
- // command string, and needs no custom Incredibuild profile: unlike Rust/cargo,
- // Incredibuild's own default profile already covers C/C++ builds.
+ // Native builds - CMake (CLion) and MSBuild (Rider). Mirrors the Cargo/Rust flow above,
+ // but each invocation is built from resolved executables + argument lists + environment
+ // (see [NativeBuild]) rather than a single command string, and needs no custom
+ // Incredibuild profile: unlike Rust/cargo, Incredibuild's own default profile already
+ // covers the tools these builds launch.
// -----------------------------------------------------------------------------------
/**
* Checks that Incredibuild is installed, showing the "not found" dialog and returning
* null if the caller should not proceed. Unlike [ensureIncredibuildReady], this has no
* minimum-version gate: there is no known minimum Incredibuild version required for its
- * native CMake/C++ build support, unlike the specific version bump Rust build support
- * needed.
+ * native CMake/C++ or MSBuild build support, unlike the specific version bump Rust build
+ * support needed.
*/
- internal fun ensureIncredibuildInstalledForCMake(project: Project): File? {
+ internal fun ensureIncredibuildInstalled(project: Project, actionType: String): File? {
val installFolder = IncredibuildLocator.findInstallFolder()
if (installFolder == null) {
- showNotFoundDialog(project, "cmake")
+ showNotFoundDialog(project, actionType)
}
return installFolder
}
+ /** One process to run inside an accelerated [NativeBuild]. */
+ internal class NativeCommand(val executable: String, val arguments: List)
+
+ /**
+ * Everything an accelerated native build runs: [commands] in order (stopping at the first
+ * that fails), from [workingDirectory], with [environment] set on top of the inherited one.
+ * Kept as executable + argument lists rather than pre-joined strings so quoting is only ever
+ * done once, by whichever wrapper script actually launches them.
+ *
+ * [buildCache] opts the build into Incredibuild's Build Cache - `/BUILDCACHELOCAL=ON` on
+ * Windows, `--build-cache-local-shared` on Linux. On for the C/C++ compilers a CMake build
+ * runs, which the cache supports; off for MSBuild, since Build Cache doesn't support .NET
+ * builds yet, so .NET builds are accelerated without it rather than relying on the cache
+ * passing their tools through untouched.
+ */
+ internal class NativeBuild(
+ val commands: List,
+ val workingDirectory: String,
+ val environment: Map = emptyMap(),
+ val buildCache: Boolean = true
+ )
+
+ /**
+ * How an accelerated build announces itself in the IDE - so it's unmistakable that this
+ * build is going through Incredibuild rather than the IDE's own builder: [contentName] names
+ * the Incredibuild tool window tab, [banner] is printed at the top of its console, and
+ * [progressTitle] is shown in the status bar (with a cancel button that stops the build
+ * gracefully) for as long as the build runs, followed by a success/failure notification.
+ */
+ internal class AcceleratedBuildPresentation(
+ val contentName: String,
+ val banner: String,
+ val progressTitle: String
+ )
+
+ /**
+ * Runs an already-resolved [build] through Incredibuild. Must be called from a background
+ * thread with an already-verified [installFolder] (see [ensureIncredibuildInstalled]).
+ */
+ internal fun launchNativeBuild(
+ project: Project,
+ installFolder: File,
+ build: NativeBuild,
+ actionType: String,
+ presentation: AcceleratedBuildPresentation? = null
+ ) {
+ when (val prepared = prepareNativeBuild(project, installFolder, build)) {
+ is PreparedBuild.Failed -> showErrorOnEdt(project, prepared.message)
+ is PreparedBuild.Ready -> launchBuild(project, prepared, actionType, presentation)
+ }
+ }
+
/**
* Resolves [kind]'s cmake invocation and, if Incredibuild is installed, runs it through
* Incredibuild. Everything here - locating Incredibuild, reading the CMake project model -
@@ -630,7 +930,7 @@ object IncredibuildRunner {
internal fun buildCMakeViaIncredibuild(project: Project, actionType: String, kind: CMakeBuildKind) {
ProgressManager.getInstance().run(object : Task.Backgroundable(project, "Locating Incredibuild") {
override fun run(indicator: ProgressIndicator) {
- val installFolder = ensureIncredibuildInstalledForCMake(project) ?: return
+ val installFolder = ensureIncredibuildInstalled(project, "cmake") ?: return
val resolution = deriveCMakeBuild(project, kind)
val resolved = when (resolution) {
null -> {
@@ -647,48 +947,49 @@ object IncredibuildRunner {
}
is CMakeBuildResolution.Ready -> resolution.build
}
- when (val prepared = prepareCMakeBuild(project, installFolder, resolved)) {
- is PreparedBuild.Failed -> showErrorOnEdt(project, prepared.message)
- is PreparedBuild.Ready -> launchBuild(project, prepared.commandLine, actionType) { _ ->
- prepared.cleanUp()
- }
- }
+ val build = NativeBuild(
+ listOf(NativeCommand(resolved.executable, resolved.arguments)),
+ resolved.workingDirectory,
+ resolved.environment
+ )
+ launchNativeBuild(project, installFolder, build, actionType)
}
})
}
- private fun prepareCMakeBuild(
+ private fun prepareNativeBuild(
project: Project,
installFolder: File,
- resolved: ResolvedCMakeBuild
+ build: NativeBuild
): PreparedBuild =
if (SystemInfo.isLinux) {
- prepareCMakeBuildLinux(project, installFolder, resolved)
+ prepareNativeBuildLinux(project, installFolder, build)
} else {
- prepareCMakeBuildWindows(project, installFolder, resolved)
+ prepareNativeBuildWindows(project, installFolder, build)
}
/**
* BuildConsole needs "/command=" with a single quoted value. Rather than hand it the whole
- * `cmake --build ... --target ...` invocation as one string - which would need its own
- * space-containing arguments (the build directory, a target name) individually quoted
- * *inside* that already-quoted value, an ambiguous nested-quoting shape BuildConsole's own
- * parsing for /command= is not documented to support - this writes a tiny wrapper .cmd that
- * runs the real invocation with ordinary single-level quoting, and points /command= at just
- * that script's path (one simple quoted path, the same shape /profile= above already uses
- * uncontroversially). The toolchain environment (PATH/INCLUDE/LIB for an MSVC profile, ...)
- * is set inside the same script via `set`, so it doesn't depend on whether BuildConsole
- * passes its own parent environment through to what it launches.
+ * `cmake --build ... --target ...` (or `MSBuild.exe ...`) invocation as one
+ * string - which would need its own space-containing arguments (the build directory, a
+ * target name, a solution path) individually quoted *inside* that already-quoted value, an
+ * ambiguous nested-quoting shape BuildConsole's own parsing for /command= is not documented
+ * to support - this writes a tiny wrapper .cmd that runs the real invocation with ordinary
+ * single-level quoting, and points /command= at just that script's path (one simple quoted
+ * path, the same shape /profile= above already uses uncontroversially). The toolchain
+ * environment (PATH/INCLUDE/LIB for an MSVC profile, ...) is set inside the same script via
+ * `set`, so it doesn't depend on whether BuildConsole passes its own parent environment
+ * through to what it launches.
*
* No `/profile=`/`/BuildCacheProfile=` here, unlike [prepareBuildWindows]: those point at a
- * bespoke Incredibuild profile built specifically for Cargo/rustc, which C/C++ needs no
- * equivalent of - Incredibuild's own default profile already covers cmake/ninja/make/nmake
- * and the compilers they invoke.
+ * bespoke Incredibuild profile built specifically for Cargo/rustc, which C/C++ and MSBuild
+ * need no equivalent of - Incredibuild's own default profile already covers
+ * cmake/ninja/make/nmake/MSBuild and the compilers they invoke.
*/
- private fun prepareCMakeBuildWindows(
+ private fun prepareNativeBuildWindows(
project: Project,
installFolder: File,
- resolved: ResolvedCMakeBuild
+ build: NativeBuild
): PreparedBuild {
val buildConsolePath = File(installFolder, "BuildConsole.exe")
if (!buildConsolePath.exists()) {
@@ -697,39 +998,19 @@ object IncredibuildRunner {
)
}
- val wrapperCmd = File.createTempFile("incredibuild-cmake-", ".cmd")
+ val wrapperCmd = File.createTempFile("incredibuild-native-", ".cmd")
wrapperCmd.deleteOnExit()
- val cmdLines = StringBuilder("@echo off\r\n")
- for ((key, value) in resolved.environment) {
- cmdLines.append("set \"").append(batchEscaped(key)).append('=').append(batchEscaped(value)).append("\"\r\n")
- }
- cmdLines.append(cmdQuoted(resolved.executable))
- for (arg in resolved.arguments) {
- cmdLines.append(' ').append(cmdQuoted(arg))
- }
- cmdLines.append("\r\nexit /b %ERRORLEVEL%\r\n")
- wrapperCmd.writeText(cmdLines.toString())
+ wrapperCmd.writeText(windowsWrapperScript(build))
val scriptFile = File.createTempFile("incredibuild-build-", ".ps1")
scriptFile.deleteOnExit()
scriptFile.writeText(
- "& \"${buildConsolePath.absolutePath}\" " +
- "${powerShellSingleQuoted("/command=\"${wrapperCmd.absolutePath}\"")} " +
- "${powerShellSingleQuoted("/title=\"${project.name}\"")} " +
- // Explicitly opts this session into Build Cache, the same way the Linux
- // ib_console invocation below already does via --build-cache-local-shared - no
- // /BuildCacheProfile= is passed alongside it because cl.exe/clang/gcc are
- // already covered by BuildConsole's own built-in default profile (the bespoke
- // *.xml sample profiles in the Incredibuild install's "BuildCache Profiles"
- // folder are only for tools that default profile doesn't already know, e.g.
- // rustc/lib for the Cargo build above, or nvcc/perl).
- "/BUILDCACHELOCAL=ON\n" +
- "exit \$LASTEXITCODE\n"
+ windowsLauncherScript(buildConsolePath.absolutePath, wrapperCmd.absolutePath, project.name, build.buildCache)
)
val commandLine = GeneralCommandLine("powershell")
.withParameters("-NoProfile", "-ExecutionPolicy", "Bypass", "-File", scriptFile.absolutePath)
- .withWorkDirectory(resolved.workingDirectory)
+ .withWorkDirectory(build.workingDirectory)
return PreparedBuild.Ready(commandLine) {
scriptFile.delete()
@@ -737,17 +1018,65 @@ object IncredibuildRunner {
}
}
+ /**
+ * The PowerShell script [prepareNativeBuildWindows] runs: BuildConsole with /command= pointed
+ * at [wrapperCmd] (see [windowsWrapperScript]), exiting with BuildConsole's own exit code.
+ */
+ internal fun windowsLauncherScript(buildConsole: String, wrapperCmd: String, title: String, buildCache: Boolean): String =
+ "& \"$buildConsole\" " +
+ "${powerShellSingleQuoted("/command=\"$wrapperCmd\"")} " +
+ powerShellSingleQuoted("/title=\"$title\"") +
+ // Explicitly opts this session into Build Cache, the same way the Linux ib_console
+ // invocation does via --build-cache-local-shared - no /BuildCacheProfile= is passed
+ // alongside it because cl.exe/clang/gcc are already covered by BuildConsole's own
+ // built-in default profile (the bespoke *.xml sample profiles in the Incredibuild
+ // install's "BuildCache Profiles" folder are only for tools that default profile
+ // doesn't already know, e.g. rustc/lib for the Cargo build above, or nvcc/perl).
+ // Left off where the build doesn't want it - see NativeBuild.buildCache.
+ (if (buildCache) " /BUILDCACHELOCAL=ON" else "") + "\n" +
+ "exit \$LASTEXITCODE\n"
+
+ /**
+ * The .cmd [prepareNativeBuildWindows] points BuildConsole's /command= at: [NativeBuild.environment]
+ * via `set`, then each command in turn, stopping at (and exiting with) the first non-zero
+ * exit code - so a multi-command build reports failure the same way a single command does.
+ */
+ internal fun windowsWrapperScript(build: NativeBuild): String {
+ val cmdLines = StringBuilder("@echo off\r\n")
+ for ((key, value) in build.environment) {
+ cmdLines.append("set \"").append(batchEscaped(key)).append('=').append(batchEscaped(value)).append("\"\r\n")
+ }
+ for ((index, command) in build.commands.withIndex()) {
+ cmdLines.append(cmdQuoted(command.executable))
+ for (arg in command.arguments) {
+ cmdLines.append(' ').append(cmdQuoted(arg))
+ }
+ cmdLines.append("\r\n")
+ if (index < build.commands.lastIndex) {
+ // Not "if errorlevel 1": that only matches codes >= 1, and a crashed process's
+ // exit code (e.g. 0xC0000005) reads as negative, which would let the chain go on.
+ cmdLines.append("if not \"%ERRORLEVEL%\"==\"0\" exit /b %ERRORLEVEL%\r\n")
+ }
+ }
+ cmdLines.append("exit /b %ERRORLEVEL%\r\n")
+ return cmdLines.toString()
+ }
+
/**
* ib_console has no `/command=`-style text field - the build command is trailing argv
* after its own recognized options - so, unlike Windows, there's no nested-quoting
- * concern to work around here: the whole invocation (environment exports, then the cmake
+ * concern to work around here: the whole invocation (environment exports, then the build
* command) is written into one script, each token quoted individually and safely via
* [shellSingleQuoted].
*
+ * ib_console runs exactly one command, so a [NativeBuild] with more than one has them
+ * chained inside a `/bin/sh -c` that ib_console runs instead -
+ * every process they start is still a descendant of ib_console, so all of it is accelerated.
+ *
* No custom `--profile` here, unlike [prepareBuildLinux]'s RustRover-specific one: C/C++
- * needs no equivalent, ib_console's own shipped default profile already covers it.
+ * and MSBuild need no equivalent, ib_console's own shipped default profile already covers them.
*/
- private fun prepareCMakeBuildLinux(project: Project, installFolder: File, resolved: ResolvedCMakeBuild): PreparedBuild {
+ private fun prepareNativeBuildLinux(project: Project, installFolder: File, build: NativeBuild): PreparedBuild {
val consolePath = File(installFolder, "bin/ib_console")
if (!consolePath.exists()) {
return PreparedBuild.Failed(
@@ -757,8 +1086,18 @@ object IncredibuildRunner {
val scriptFile = File.createTempFile("incredibuild-build-", ".sh")
scriptFile.deleteOnExit()
+ scriptFile.writeText(linuxWrapperScript(project.name, consolePath.absolutePath, build))
+
+ val commandLine = GeneralCommandLine("/bin/sh", scriptFile.absolutePath)
+ .withWorkDirectory(build.workingDirectory)
+
+ return PreparedBuild.Ready(commandLine) { scriptFile.delete() }
+ }
+
+ /** The script [prepareNativeBuildLinux] runs - see there. */
+ internal fun linuxWrapperScript(caption: String, consolePath: String, build: NativeBuild): String {
val script = StringBuilder("#!/bin/sh\n")
- for ((key, value) in resolved.environment) {
+ for ((key, value) in build.environment) {
// Not every entry CPPBuildUtil.buildCommandLine hands back is necessarily a valid
// shell identifier (e.g. an inherited "BASH_FUNC_foo%%" from an exported bash
// function) - "export" on such a name fails and takes the whole script down with
@@ -767,30 +1106,47 @@ object IncredibuildRunner {
if (!VALID_SHELL_IDENTIFIER.matches(key)) continue
script.append("export ").append(key).append('=').append(shellSingleQuoted(value)).append('\n')
}
- script.append("exec ").append(shellSingleQuoted(consolePath.absolutePath))
- .append(" --caption ").append(shellSingleQuoted(project.name))
- .append(" --ib-quiet --build-cache-local-shared --build-cache-basedir=\"\$PWD\" -- ")
- .append(shellSingleQuoted(resolved.executable))
- for (arg in resolved.arguments) {
- script.append(' ').append(shellSingleQuoted(arg))
+ script.append("exec ").append(shellSingleQuoted(consolePath))
+ .append(" --caption ").append(shellSingleQuoted(caption))
+ .append(" --ib-quiet")
+ // Left off where the build doesn't want it - see NativeBuild.buildCache.
+ if (build.buildCache) script.append(" --build-cache-local-shared --build-cache-basedir=\"\$PWD\"")
+ script.append(" -- ")
+ val commandTexts = build.commands.map { command ->
+ (listOf(command.executable) + command.arguments).joinToString(" ") { shellSingleQuoted(it) }
+ }
+ if (commandTexts.size == 1) {
+ script.append(commandTexts.single())
+ } else {
+ script.append("/bin/sh -c ").append(shellSingleQuoted(commandTexts.joinToString(" && ")))
}
// Merges stderr into stdout, for the same reason prepareBuildLinux's Cargo build does.
script.append(" 2>&1\n")
- scriptFile.writeText(script.toString())
-
- val commandLine = GeneralCommandLine("/bin/sh", scriptFile.absolutePath)
- .withWorkDirectory(resolved.workingDirectory)
-
- return PreparedBuild.Ready(commandLine) { scriptFile.delete() }
+ return script.toString()
}
- /** Wraps [value] as a cmd.exe double-quoted token (after [batchEscaped]), needed only when
- * it contains whitespace. */
+ /**
+ * Wraps [value] as a cmd.exe double-quoted token (after [batchEscaped]), needed only when it
+ * is empty, contains whitespace, or contains a character cmd.exe would otherwise treat as an
+ * operator (`&`, `|`, `<`, `>`, `^`, `(`, `)` - e.g. a solution under `C:\R&D\`).
+ *
+ * Inside the quotes, the receiving program (MSBuild, cmake, ... - anything parsing its
+ * command line the MSVC runtime / CommandLineToArgvW way) treats backslashes right before a
+ * `"` as escapes, so a value ending in backslashes - `-p:SolutionDir=C:\My Projects\App\` -
+ * would otherwise turn the closing quote into a literal one and swallow every argument after
+ * it. Doubling those trailing backslashes is that same parser's escape for them.
+ */
internal fun cmdQuoted(value: String): String {
val escaped = batchEscaped(value)
- return if (escaped.any { it.isWhitespace() }) "\"$escaped\"" else escaped
+ if (escaped.isNotEmpty() && escaped.none { it.isWhitespace() || it in CMD_SPECIAL_CHARACTERS }) {
+ return escaped
+ }
+ val trailingBackslashes = escaped.length - escaped.trimEnd('\\').length
+ return "\"" + escaped + "\\".repeat(trailingBackslashes) + "\""
}
+ private const val CMD_SPECIAL_CHARACTERS = "&|<>^()"
+
/** Escapes [value] for literal use in a .cmd file: cmd.exe expands `%...%` variable
* references anywhere on a line it parses - including inside a `set "KEY=VALUE"` value and
* inside an already-quoted argument - so a stray `%` in a path or environment value (e.g.
diff --git a/src/main/kotlin/com/incredibuild/ibplugin/actions/RebuildProjectWithIncredibuildAction.kt b/src/main/kotlin/com/incredibuild/ibplugin/actions/RebuildProjectWithIncredibuildAction.kt
index 74b1801..a14fd38 100644
--- a/src/main/kotlin/com/incredibuild/ibplugin/actions/RebuildProjectWithIncredibuildAction.kt
+++ b/src/main/kotlin/com/incredibuild/ibplugin/actions/RebuildProjectWithIncredibuildAction.kt
@@ -20,18 +20,13 @@ import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.application.ApplicationManager
-import com.intellij.openapi.components.service
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.progress.Task
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.Messages
-import com.intellij.openapi.util.SystemInfo
-import org.rust.cargo.project.settings.RustProjectSettingsService
import java.io.File
-private val CARGO_EXECUTABLE_NAME = if (SystemInfo.isWindows) "cargo.exe" else "cargo"
-
/**
* Cleans the whole Cargo workspace (plain "cargo clean", not accelerated -
* it only deletes files) then builds it through Incredibuild.
@@ -97,22 +92,6 @@ class RebuildProjectWithIncredibuildAction : AnAction() {
* PATH). Prefers the Rust plugin's configured toolchain if present, then
* falls back to rustup's well-known default install location.
*/
- private fun resolveCargoExecutable(project: Project): String? {
- project.service().toolchain?.let { toolchain ->
- val path = toolchain.pathToCargoExecutable("cargo")
- if (File(path.toString()).exists()) {
- return path.toString()
- }
- }
-
- val defaultCargo = File(System.getProperty("user.home"), ".cargo${File.separator}bin${File.separator}$CARGO_EXECUTABLE_NAME")
- if (defaultCargo.exists()) {
- return defaultCargo.absolutePath
- }
-
- return null
- }
-
private fun startClean(
project: Project,
installFolder: File,
@@ -130,7 +109,8 @@ class RebuildProjectWithIncredibuildAction : AnAction() {
installFolder,
invocation.command,
invocation.workingDirectory,
- "rebuild"
+ "rebuild",
+ cargoBinDir = invocation.cargoBinDir
)
}
}
diff --git a/src/main/kotlin/com/incredibuild/ibplugin/actions/WorkspaceLabels.kt b/src/main/kotlin/com/incredibuild/ibplugin/actions/WorkspaceLabels.kt
new file mode 100644
index 0000000..e35cc91
--- /dev/null
+++ b/src/main/kotlin/com/incredibuild/ibplugin/actions/WorkspaceLabels.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2026 Incredibuild
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.incredibuild.ibplugin.actions
+
+import com.incredibuild.ibplugin.actions.cmake.hasCMakeWorkspace
+import com.intellij.ide.plugins.PluginManagerCore
+import com.intellij.openapi.extensions.PluginId
+import com.intellij.openapi.project.Project
+
+private val RUST_PLUGIN_ID = PluginId.getId("com.jetbrains.rust")
+private val CMAKE_PLUGIN_ID = PluginId.getId("com.intellij.cmake")
+
+/**
+ * [label] as-is, unless [project] has both a Cargo and a CMake workspace loaded at once (e.g.
+ * Rust with a CMake-built C dependency, or CMake + Corrosion) - a case the plain per-workspace
+ * `update()` checks don't disambiguate, since both the Cargo and CMake actions would otherwise
+ * show the identical "Build"/"Rebuild" text side by side in the same Incredibuild menu. Only
+ * reached once a caller's own `update()` has already confirmed its workspace is present, so
+ * this never needs to decide which (if either) action should be hidden - just how to label the
+ * one that is shown.
+ *
+ * Shared by the Cargo actions (rust-support.xml) and the CMake ones (cmake-support.xml), so it
+ * lives here, with no Rust or CMake imports of its own, and asks about the *other* kind of
+ * workspace only once that kind's plugin is known to be loaded. Without that, RustRover (no CMake
+ * plugin) threw NoClassDefFoundError for CMakeWorkspace from every update() of the Cargo actions;
+ * the JVM only resolves those classes when the guarded call actually runs.
+ */
+internal fun disambiguatedLabel(project: Project, label: String, suffix: String): String {
+ val bothLoaded = PluginManagerCore.isLoaded(RUST_PLUGIN_ID) && PluginManagerCore.isLoaded(CMAKE_PLUGIN_ID)
+ return if (bothLoaded && hasCargoWorkspace(project) && hasCMakeWorkspace(project)) "$label ($suffix)" else label
+}
diff --git a/src/main/kotlin/com/incredibuild/ibplugin/actions/dotnet/DotNetBuildActions.kt b/src/main/kotlin/com/incredibuild/ibplugin/actions/dotnet/DotNetBuildActions.kt
new file mode 100644
index 0000000..270b54c
--- /dev/null
+++ b/src/main/kotlin/com/incredibuild/ibplugin/actions/dotnet/DotNetBuildActions.kt
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2026 Incredibuild
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.incredibuild.ibplugin.actions.dotnet
+
+import com.incredibuild.ibplugin.rider.RiderSolutionModel
+import com.incredibuild.ibplugin.rider.SolutionSelection
+import com.intellij.openapi.actionSystem.ActionUpdateThread
+import com.intellij.openapi.actionSystem.AnAction
+import com.intellij.openapi.actionSystem.AnActionEvent
+import com.intellij.openapi.project.DumbAware
+import com.intellij.openapi.ui.Messages
+
+/**
+ * The shared shape of the three Rider .NET "... with Incredibuild" actions: each captures what
+ * Rider's own equivalent would build (solution, active configuration|platform, selection) and
+ * hands it to [DotNetBuildRunner], which runs it through Incredibuild instead of Rider's builder.
+ *
+ * Registered only in Rider (rider-support.xml), so [RiderSolutionModel] - which needs Rider's
+ * own classes - is only ever reached there.
+ */
+abstract class DotNetBuildWithIncredibuildActionBase internal constructor(
+ private val kind: DotNetBuildKind,
+ private val actionType: String
+) : AnAction(), DumbAware {
+
+ override fun getActionUpdateThread() = ActionUpdateThread.BGT
+
+ override fun update(e: AnActionEvent) {
+ val project = e.project
+ if (project == null || !RiderSolutionModel.hasSolution(project)) {
+ e.presentation.isEnabledAndVisible = false
+ return
+ }
+
+ // The selection is only read where it matters - the context menu, and Build Selected
+ // Projects - since this runs on every menu and toolbar update.
+ if (e.isFromContextMenu) {
+ // Same rule as Rider's own Build Selection: offered on the solution, solution
+ // folders and projects in the Solution Explorer, not on files/references inside them.
+ e.presentation.isEnabledAndVisible = RiderSolutionModel.selection(e.dataContext) != SolutionSelection.Nothing
+ return
+ }
+
+ e.presentation.isVisible = true
+ e.presentation.isEnabled = kind != DotNetBuildKind.BUILD_PROJECTS ||
+ RiderSolutionModel.selection(e.dataContext) != SolutionSelection.Nothing
+ }
+
+ override fun actionPerformed(e: AnActionEvent) {
+ val project = e.project ?: return
+ val entryPoint = RiderSolutionModel.buildEntryPoint(project)
+ val solutionFile = RiderSolutionModel.solutionFile(project)
+ val configuration = RiderSolutionModel.activeConfiguration(project)
+ if (entryPoint == null || solutionFile == null || configuration == null) {
+ Messages.showErrorDialog(
+ project,
+ "Could not determine what to build. Is a solution loaded, with an active configuration selected?",
+ "Incredibuild"
+ )
+ return
+ }
+
+ var effectiveKind = kind
+ var projectFiles = emptyList()
+ if (kind == DotNetBuildKind.BUILD_PROJECTS) {
+ when (val selection = RiderSolutionModel.selection(e.dataContext)) {
+ // The solution node itself selected: like Rider's own Build Selection, that's
+ // just a solution build.
+ SolutionSelection.WholeSolution -> effectiveKind = DotNetBuildKind.BUILD_SOLUTION
+ is SolutionSelection.Projects -> projectFiles = selection.projectFiles
+ SolutionSelection.Nothing -> {
+ Messages.showInfoMessage(
+ project,
+ "Select one or more projects in the Solution Explorer to build them with Incredibuild.",
+ "Incredibuild"
+ )
+ return
+ }
+ }
+ }
+
+ DotNetBuildRunner.build(
+ project,
+ DotNetBuildRequest(
+ effectiveKind,
+ entryPoint,
+ solutionFile,
+ configuration.configuration,
+ configuration.platform,
+ projectFiles
+ ),
+ actionType
+ )
+ }
+}
+
+/** The accelerated equivalent of Rider's own Build Solution. */
+class BuildSolutionWithIncredibuildAction :
+ DotNetBuildWithIncredibuildActionBase(DotNetBuildKind.BUILD_SOLUTION, "dotnet_build_solution")
+
+/** The accelerated equivalent of Rider's own Rebuild Solution. */
+class RebuildSolutionWithIncredibuildAction :
+ DotNetBuildWithIncredibuildActionBase(DotNetBuildKind.REBUILD_SOLUTION, "dotnet_rebuild_solution")
+
+/** The accelerated equivalent of Rider's own Build Selection. */
+class BuildSelectedProjectsWithIncredibuildAction :
+ DotNetBuildWithIncredibuildActionBase(DotNetBuildKind.BUILD_PROJECTS, "dotnet_build_projects")
diff --git a/src/main/kotlin/com/incredibuild/ibplugin/actions/dotnet/DotNetBuildRunner.kt b/src/main/kotlin/com/incredibuild/ibplugin/actions/dotnet/DotNetBuildRunner.kt
new file mode 100644
index 0000000..5b2e7ea
--- /dev/null
+++ b/src/main/kotlin/com/incredibuild/ibplugin/actions/dotnet/DotNetBuildRunner.kt
@@ -0,0 +1,180 @@
+/*
+ * Copyright 2026 Incredibuild
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.incredibuild.ibplugin.actions.dotnet
+
+import com.incredibuild.ibplugin.actions.IncredibuildRunner
+import com.incredibuild.ibplugin.actions.IncredibuildRunner.AcceleratedBuildPresentation
+import com.intellij.openapi.application.ApplicationManager
+import com.intellij.openapi.diagnostic.Logger
+import com.intellij.openapi.fileEditor.FileDocumentManager
+import com.intellij.openapi.progress.ProgressIndicator
+import com.intellij.openapi.progress.ProgressManager
+import com.intellij.openapi.progress.Task
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.ui.Messages
+import java.io.File
+
+private val LOG = Logger.getInstance("com.incredibuild.ibplugin.actions.dotnet.DotNetBuildRunner")
+
+/**
+ * Everything about Rider's own state an accelerated .NET build needs, captured up front by the
+ * action (on the thread it runs on) so the background part never touches Rider's models.
+ */
+internal class DotNetBuildRequest(
+ val kind: DotNetBuildKind,
+ /** The .sln/.slnx, or open .slnf - see [solutionBuild]. */
+ val entryPoint: String,
+ /** The actual .sln/.slnx even when a filter is open - see [projectsBuild]. */
+ val solutionFile: String,
+ val configuration: String,
+ val platform: String,
+ /** Only for [DotNetBuildKind.BUILD_PROJECTS]. */
+ val projectFiles: List = emptyList(),
+)
+
+internal object DotNetBuildRunner {
+
+ fun build(project: Project, request: DotNetBuildRequest, actionType: String) {
+ // Rider's own build saves unsaved editors first, so the build sees what's on screen;
+ // MSBuild reads from disk, so this has to as well.
+ FileDocumentManager.getInstance().saveAllDocuments()
+
+ ProgressManager.getInstance().run(object : Task.Backgroundable(project, "Locating Incredibuild") {
+ override fun run(indicator: ProgressIndicator) {
+ val installFolder = IncredibuildRunner.ensureIncredibuildInstalled(project, actionType) ?: return
+
+ val tool = when (val location = MsBuildLocator.locate()) {
+ is MsBuildLocation.NotFound -> return showError(project, location.reason)
+ is MsBuildLocation.Found -> location.tool
+ }
+
+ val build = when (request.kind) {
+ DotNetBuildKind.BUILD_SOLUTION, DotNetBuildKind.REBUILD_SOLUTION -> solutionBuild(
+ tool,
+ request.entryPoint,
+ request.kind,
+ request.configuration,
+ request.platform,
+ restore = needsRestore(readEntryPointProjectFiles(request.entryPoint))
+ )
+
+ DotNetBuildKind.BUILD_PROJECTS -> {
+ val targetNames = readProjectTargets(request.solutionFile)
+ val missing = request.projectFiles.filter { normalizedPath(it) !in targetNames }
+ if (missing.isNotEmpty()) {
+ return showError(
+ project,
+ "Could not find these projects in ${File(request.solutionFile).name}, so they can't be " +
+ "built through it:\n" + missing.joinToString("\n")
+ )
+ }
+ projectsBuild(
+ tool,
+ request.solutionFile,
+ request.projectFiles,
+ request.configuration,
+ request.platform,
+ targetNames,
+ restore = needsRestore(request.projectFiles)
+ )
+ }
+ }
+
+ IncredibuildRunner.launchNativeBuild(
+ project,
+ installFolder,
+ build,
+ actionType,
+ presentation(request, tool)
+ )
+ }
+ })
+ }
+
+ /** The solution's per-project build targets - see [solutionProjectTargets]. Empty if the
+ * solution can't be read, which then reports every selected project as not found. */
+ private fun readProjectTargets(solutionFile: String): Map {
+ val file = File(solutionFile)
+ return try {
+ solutionProjectTargets(
+ file.readText(),
+ file.absoluteFile.parentFile,
+ isSlnx = file.extension.equals("slnx", ignoreCase = true)
+ )
+ } catch (e: Exception) {
+ LOG.warn("Could not read the project list from $solutionFile", e)
+ emptyMap()
+ }
+ }
+
+ /**
+ * The project files a whole-solution build of [entryPoint] builds, for [needsRestore]: the
+ * solution's, or only those an open solution filter includes - so a project the filter leaves
+ * out can't add `-restore` to every filtered build. Empty if they can't be read, which then
+ * leaves restore to Rider, as it is for every already-restored solution.
+ */
+ private fun readEntryPointProjectFiles(entryPoint: String): List {
+ val file = File(entryPoint).absoluteFile
+ return try {
+ if (file.extension.equals("slnf", ignoreCase = true)) {
+ solutionFilterProjectFiles(file.readText(), file.parentFile)
+ } else {
+ solutionProjectFiles(file.readText(), file.parentFile)
+ }
+ } catch (e: Exception) {
+ LOG.warn("Could not read the project list from $entryPoint", e)
+ emptyList()
+ }
+ }
+
+ private fun presentation(request: DotNetBuildRequest, tool: MsBuildTool): AcceleratedBuildPresentation {
+ val solutionName = File(request.solutionFile).nameWithoutExtension
+ val actionName = when (request.kind) {
+ DotNetBuildKind.BUILD_SOLUTION -> "Build Solution"
+ DotNetBuildKind.REBUILD_SOLUTION -> "Rebuild Solution"
+ DotNetBuildKind.BUILD_PROJECTS -> "Build Selected Projects"
+ }
+ val target = when (request.kind) {
+ DotNetBuildKind.BUILD_PROJECTS ->
+ cappedProjectList(request.projectFiles.map { File(it).nameWithoutExtension })
+ else -> File(request.entryPoint).name
+ }
+ val progressVerb = if (request.kind == DotNetBuildKind.REBUILD_SOLUTION) "Rebuilding" else "Building"
+ return AcceleratedBuildPresentation(
+ contentName = actionName,
+ banner = "Incredibuild: $actionName (accelerated)\n" +
+ " Building: $target\n" +
+ " Configuration: ${request.configuration} | ${request.platform}\n" +
+ " MSBuild: ${tool.description}",
+ progressTitle = "$progressVerb $solutionName with Incredibuild (${request.configuration} | ${request.platform})"
+ )
+ }
+
+ /**
+ * [names] for the banner, cut short after [shown] so a large selection doesn't make it
+ * unreadable: "App, Lib +12 more".
+ */
+ internal fun cappedProjectList(names: List, shown: Int = 2): String {
+ if (names.size <= shown + 1) return names.joinToString(", ")
+ return names.take(shown).joinToString(", ") + " +${names.size - shown} more"
+ }
+
+ private fun showError(project: Project, message: String) {
+ ApplicationManager.getApplication().invokeLater {
+ Messages.showErrorDialog(project, message, "Incredibuild")
+ }
+ }
+}
diff --git a/src/main/kotlin/com/incredibuild/ibplugin/actions/dotnet/MsBuildCommand.kt b/src/main/kotlin/com/incredibuild/ibplugin/actions/dotnet/MsBuildCommand.kt
new file mode 100644
index 0000000..273c7fe
--- /dev/null
+++ b/src/main/kotlin/com/incredibuild/ibplugin/actions/dotnet/MsBuildCommand.kt
@@ -0,0 +1,334 @@
+/*
+ * Copyright 2026 Incredibuild
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.incredibuild.ibplugin.actions.dotnet
+
+import com.google.gson.JsonParser
+import com.incredibuild.ibplugin.actions.IncredibuildRunner.NativeBuild
+import com.incredibuild.ibplugin.actions.IncredibuildRunner.NativeCommand
+import org.w3c.dom.Document
+import org.w3c.dom.Element
+import org.xml.sax.InputSource
+import java.io.File
+import java.io.IOException
+import java.io.StringReader
+import javax.xml.parsers.DocumentBuilderFactory
+
+/** Which of the three .NET "... with Incredibuild" actions is asking for a command. */
+internal enum class DotNetBuildKind(val msBuildTarget: String) {
+ /** "Build Solution with Incredibuild". */
+ BUILD_SOLUTION("Build"),
+
+ /** "Rebuild Solution with Incredibuild". */
+ REBUILD_SOLUTION("Rebuild"),
+
+ /** "Build Selected Projects with Incredibuild". */
+ BUILD_PROJECTS("Build"),
+}
+
+/**
+ * The MSBuild to run: [executable] plus any [leadingArguments] that must come before the
+ * project/solution (`msbuild` for `dotnet msbuild`), and a [description] for the console banner.
+ */
+internal class MsBuildTool(val executable: String, val leadingArguments: List, val description: String) {
+ companion object {
+ /** MSBuild.exe itself, or the dotnet host - which needs its `msbuild` verb first. */
+ fun fromPath(path: String): MsBuildTool {
+ // Split on either separator rather than through File, whose notion of one depends on
+ // the OS this runs on - so a Windows path reads the same everywhere (e.g. in tests).
+ val name = path.substringAfterLast('/').substringAfterLast('\\').substringBeforeLast('.')
+ return if (name.equals("dotnet", ignoreCase = true)) {
+ MsBuildTool(path, listOf("msbuild"), "dotnet msbuild ($path)")
+ } else {
+ MsBuildTool(path, emptyList(), path)
+ }
+ }
+ }
+}
+
+/**
+ * Arguments shared by every accelerated MSBuild invocation, on top of the target and the active
+ * configuration:
+ * - No `-restore` (see [RESTORE_ARGUMENT] for when it is added): Rider restores NuGet packages
+ * itself - on solution load, on project changes and before its own builds - and watches each
+ * project's restore outputs (obj\*.nuget.dgspec.json, *.nuget.g.props, project.assets.json).
+ * Any restore by another process rewrites those files, which Rider treats as "restore inputs
+ * changed": it re-restores every project on its next build, regenerating *.nuget.g.props - an
+ * import of every project, so an input of every compile - and recompiles the whole solution
+ * even though this build just did. Leaving restore to Rider keeps the accelerated build and
+ * Rider's own builds incremental with each other.
+ * - `-graph`: MSBuild's static graph mode - evaluate the whole project graph up front, then
+ * build it bottom-up, every project only once its references are done. Without it, a project
+ * that references many others (an app referencing all of a solution's libraries) builds those
+ * references itself, from whichever MSBuild node it happens to run on - and MSBuild pins each
+ * project to the first node that touches it, so every library that project reached first then
+ * builds on that one node, one after another. Observed on a 100-library solution: 16 libraries
+ * in parallel, then the remaining 84 strictly one at a time (4 min vs 1:48 with `-graph`) -
+ * leaving Incredibuild one compiler process at a time to distribute.
+ * - `-m`: lets MSBuild build independent projects concurrently, which is what gives Incredibuild
+ * more than one compiler process at a time to distribute. Deliberately unbounded (one node per
+ * local core) rather than the "Parallel jobs" setting: every MSBuild node is a full,
+ * long-lived process, and the hundreds of jobs that setting defaults to for Cargo/CMake would
+ * mean hundreds of them.
+ * - `-nodeReuse:false`: otherwise MSBuild leaves its worker nodes running after the build for the
+ * next one to reuse - processes that outlive the build they were started from, which
+ * Incredibuild would then keep waiting on before it considers the build finished.
+ * - `-p:UseSharedCompilation=false`: otherwise the C#/VB compilers don't run as their own
+ * processes at all, but as requests to one shared, long-lived VBCSCompiler server - which
+ * Incredibuild can't see, let alone distribute. Only changes how the compiler is launched, not
+ * what it compiles.
+ * - `-nologo`/`-v:minimal`: the same console verbosity Rider's own build output defaults to.
+ *
+ * None of these change anything about how the solution or its projects are configured - they
+ * only affect this one command-line invocation.
+ */
+internal val COMMON_MSBUILD_ARGUMENTS = listOf(
+ "-graph",
+ "-m",
+ "-nodeReuse:false",
+ "-p:UseSharedCompilation=false",
+ "-nologo",
+ "-v:minimal",
+)
+
+/**
+ * Added only when some project being built has never been restored (see [needsRestore]) - then
+ * the build can't succeed without it, and there's no restore state of Rider's to disturb yet.
+ */
+internal const val RESTORE_ARGUMENT = "-restore"
+
+/**
+ * Whether any of [projectFiles] is a PackageReference-restored project with no NuGet assets file
+ * yet - i.e. was never restored, so building it would fail with NETSDK1004.
+ *
+ * Only such projects ever get a project.assets.json, so everything else is skipped rather than
+ * counted as unrestored: shared projects (.shproj), C++ projects (.vcxproj), classic projects
+ * using packages.config, ... Counting those would add `-restore` to every build of a solution
+ * that contains one - exactly the restore-state churn [COMMON_MSBUILD_ARGUMENTS] explains this
+ * is meant to avoid. A project file that can't be read is skipped too, leaving restore to Rider.
+ */
+internal fun needsRestore(projectFiles: List): Boolean =
+ projectFiles.any { isUnrestoredPackageReferenceProject(File(it).absoluteFile) }
+
+/** Every SDK-style project restores through PackageReference implicitly, as does a classic one
+ * that lists a PackageReference or opts in with RestoreProjectStyle. */
+private val PACKAGE_REFERENCE_PROJECT_MARKERS = listOf(
+ Regex("""\bSdk\s*=\s*""""), // or
+ Regex("""
+ Regex("""\ in the project's folder or any folder above it, with the
+ * artifacts output layout (UseArtifactsOutput) - whose root defaults to the folder holding
+ * Directory.Build.props, which can be anywhere above the project.
+ * A project that moves its intermediate folder anywhere else (a custom BaseIntermediateOutputPath)
+ * shows up as unrestored, which only costs it a restore.
+ */
+private fun assetsFileCandidates(projectFile: File): Sequence {
+ val projectDir = projectFile.parentFile
+ val artifactsLayout = generateSequence(projectDir) { it.parentFile }
+ .map { File(it, "artifacts${File.separator}obj${File.separator}${projectFile.nameWithoutExtension}") }
+ return (sequenceOf(File(projectDir, "obj")) + artifactsLayout).map { File(it, "project.assets.json") }
+}
+
+private val SLNX_PROJECT_PATH = Regex("""]*Path="([^"]+)"""")
+
+/**
+ * The project files a .sln or .slnx lists, resolved against its directory - for [needsRestore].
+ * Solution folders and non-project entries (anything not ending in "proj") are skipped.
+ */
+internal fun solutionProjectFiles(solutionText: String, solutionDir: File): List {
+ val relativePaths = SLN_PROJECT_LINE.findAll(solutionText).map { it.groupValues[2] } +
+ SLNX_PROJECT_PATH.findAll(solutionText).map { it.groupValues[1] }
+ return relativePaths
+ .filter { it.endsWith("proj", ignoreCase = true) }
+ .map { File(solutionDir, nativeSeparators(it)).path }
+ .distinct()
+ .toList()
+}
+
+/**
+ * The project files a solution filter (.slnf) includes - for [needsRestore], so a filtered build
+ * only restores when one of *its* projects needs it, not when some project the filter leaves out
+ * does. A filter is JSON, `{"solution": {"path": ..., "projects": [...]}}`: `path` is the .sln
+ * relative to the filter's own directory ([filterDir]), and each project is relative to that
+ * solution's directory, as MSBuild resolves them.
+ */
+internal fun solutionFilterProjectFiles(filterText: String, filterDir: File): List {
+ val solution = JsonParser.parseString(filterText).asJsonObject.getAsJsonObject("solution")
+ val solutionDir = File(filterDir, nativeSeparators(solution.get("path").asString)).normalize().parentFile
+ return solution.getAsJsonArray("projects")
+ .map { File(solutionDir, nativeSeparators(it.asString)).path }
+ .filter { it.endsWith("proj", ignoreCase = true) }
+ .distinct()
+}
+
+/**
+ * The accelerated build for a whole solution - the equivalent of Rider's own Build/Rebuild
+ * Solution. [entryPoint] is the .sln/.slnx, or the .slnf when a solution filter is open (MSBuild
+ * builds a filter directly, honoring which projects it includes).
+ */
+internal fun solutionBuild(
+ tool: MsBuildTool,
+ entryPoint: String,
+ kind: DotNetBuildKind,
+ configuration: String,
+ platform: String,
+ restore: Boolean = false
+): NativeBuild {
+ val arguments = tool.leadingArguments + listOf(
+ entryPoint,
+ "-t:${kind.msBuildTarget}",
+ "-p:Configuration=$configuration",
+ "-p:Platform=$platform",
+ ) + restoreArguments(restore) + COMMON_MSBUILD_ARGUMENTS
+ return NativeBuild(listOf(NativeCommand(tool.executable, arguments)), File(entryPoint).absoluteFile.parent, buildCache = false)
+}
+
+/**
+ * The accelerated build for individually selected projects - the equivalent of Rider's own
+ * Build Selection, which also builds each selected project together with the projects it
+ * references.
+ *
+ * Built through the solution, asking it for just those projects by the per-project targets its
+ * build defines ([targetNames], from [solutionProjectTargets] - every one of [projectFiles] must
+ * be in it), rather than by building each project file directly. Only a solution build maps each
+ * project, and each project it references, to the configuration|platform the solution's active
+ * configuration assigns it: a project built directly passes its own -p:Configuration/-p:Platform
+ * on to its references unchanged, so a library the solution builds as Debug|Any CPU would be
+ * built as the app's Release|x64, into a different bin\ than Rider's own build uses. Going through
+ * the solution also sets the SolutionDir/SolutionPath/... properties build logic relies on, and
+ * builds all the selected projects in one parallel invocation.
+ */
+internal fun projectsBuild(
+ tool: MsBuildTool,
+ solutionFile: String,
+ projectFiles: List,
+ configuration: String,
+ platform: String,
+ targetNames: Map,
+ restore: Boolean = false
+): NativeBuild {
+ val targets = projectFiles.map { targetNames.getValue(normalizedPath(it)) }
+ val arguments = tool.leadingArguments + listOf(
+ solutionFile,
+ "-t:${targets.joinToString(";")}",
+ "-p:Configuration=$configuration",
+ "-p:Platform=$platform",
+ ) + restoreArguments(restore) + COMMON_MSBUILD_ARGUMENTS
+ return NativeBuild(listOf(NativeCommand(tool.executable, arguments)), File(solutionFile).absoluteFile.parent, buildCache = false)
+}
+
+private fun restoreArguments(restore: Boolean): List = if (restore) listOf(RESTORE_ARGUMENT) else emptyList()
+
+/** `Project("{type}") = "name", "path", "{guid}"` - solution folders included. */
+private val SLN_PROJECT_LINE = Regex(
+ """^Project\("\{[^}]*}"\)\s*=\s*"([^"]*)"\s*,\s*"([^"]*)"\s*,\s*"\{([^}]*)}"""",
+ RegexOption.MULTILINE
+)
+
+private val SLN_NESTED_PROJECTS_SECTION =
+ Regex("""GlobalSection\(NestedProjects\)(.*?)EndGlobalSection""", RegexOption.DOT_MATCHES_ALL)
+
+/** `{child} = {parent}` - only meaningful inside [SLN_NESTED_PROJECTS_SECTION]: a project's
+ * ProjectDependencies section has lines of exactly the same shape. */
+private val SLN_NESTED_LINE = Regex("""\{([^}]*)}\s*=\s*\{([^}]*)}""")
+
+/** The characters MSBuild replaces with `_` in a solution's per-project target names. */
+private const val TARGET_NAME_CHARACTERS_TO_CLEANSE = "%$@;.()'"
+
+private fun cleansedTargetName(name: String): String =
+ name.map { if (it in TARGET_NAME_CHARACTERS_TO_CLEANSE) '_' else it }.joinToString("")
+
+/**
+ * The target a solution build defines for building just one of its projects, keyed by
+ * [normalizedPath] of each project file: the project's solution-folder path and its name, joined
+ * with `\`, each part with [TARGET_NAME_CHARACTERS_TO_CLEANSE] replaced by `_` - e.g. the "My.Lib"
+ * project in the "src" folder is `src\My_Lib`. That's the name MSBuild's solution build gives it,
+ * for a .sln (project names as written in it, folders from its NestedProjects section) and a .slnx
+ * (project file names, folders from each ``) alike.
+ */
+internal fun solutionProjectTargets(solutionText: String, solutionDir: File, isSlnx: Boolean): Map =
+ if (isSlnx) slnxProjectTargets(solutionText, solutionDir) else slnProjectTargets(solutionText, solutionDir)
+
+private fun slnProjectTargets(solutionText: String, solutionDir: File): Map {
+ class Entry(val name: String, val path: String)
+
+ val entries = SLN_PROJECT_LINE.findAll(solutionText)
+ .associate { it.groupValues[3].uppercase() to Entry(it.groupValues[1], it.groupValues[2]) }
+ val parentOf = SLN_NESTED_PROJECTS_SECTION.find(solutionText)?.groupValues?.get(1)
+ ?.let { section -> SLN_NESTED_LINE.findAll(section).associate { it.groupValues[1].uppercase() to it.groupValues[2].uppercase() } }
+ .orEmpty()
+
+ fun targetName(guid: String): String {
+ val parts = ArrayList()
+ var current: String? = guid
+ // Bounded by the number of entries, so a malformed (cyclic) NestedProjects can't hang this.
+ while (current != null && parts.size <= entries.size) {
+ parts += cleansedTargetName(entries.getValue(current).name)
+ current = parentOf[current]?.takeIf { it in entries }
+ }
+ return parts.asReversed().joinToString("\\")
+ }
+
+ return entries.filterValues { it.path.endsWith("proj", ignoreCase = true) }
+ .entries.associate { (guid, entry) -> normalizedPath(File(solutionDir, nativeSeparators(entry.path)).path) to targetName(guid) }
+}
+
+private fun slnxProjectTargets(solutionText: String, solutionDir: File): Map {
+ val projects = parseXml(solutionText).documentElement.getElementsByTagName("Project")
+ val result = HashMap()
+ for (i in 0 until projects.length) {
+ val project = projects.item(i) as Element
+ val path = project.attributeOrNull("Path") ?: continue
+ val projectFile = File(solutionDir, nativeSeparators(path))
+ val folder = (project.parentNode as? Element)?.takeIf { it.tagName == "Folder" }?.attributeOrNull("Name")
+ val parts = folder?.split('/')?.filter { it.isNotEmpty() }.orEmpty() + projectFile.nameWithoutExtension
+ result[normalizedPath(projectFile.path)] = parts.joinToString("\\") { cleansedTargetName(it) }
+ }
+ return result
+}
+
+private fun nativeSeparators(path: String): String = path.replace('\\', File.separatorChar).replace('/', File.separatorChar)
+
+private fun parseXml(text: String): Document {
+ val factory = DocumentBuilderFactory.newInstance().apply {
+ // A solution file never needs a DTD - refusing one rules out entity-expansion tricks.
+ setFeature("http://apache.org/xml/features/disallow-doctype-decl", true)
+ isExpandEntityReferences = false
+ }
+ return factory.newDocumentBuilder().parse(InputSource(StringReader(text)))
+}
+
+private fun Element.attributeOrNull(name: String): String? = getAttribute(name).takeIf { it.isNotEmpty() }
+
+/** Canonical, case-insensitive form of a path, so a project path from Rider's model matches the
+ * one resolved from the solution regardless of `..`/separator/casing differences. */
+internal fun normalizedPath(path: String): String =
+ File(path).absoluteFile.normalize().path.replace('\\', '/').lowercase()
diff --git a/src/main/kotlin/com/incredibuild/ibplugin/actions/dotnet/MsBuildLocator.kt b/src/main/kotlin/com/incredibuild/ibplugin/actions/dotnet/MsBuildLocator.kt
new file mode 100644
index 0000000..24aa626
--- /dev/null
+++ b/src/main/kotlin/com/incredibuild/ibplugin/actions/dotnet/MsBuildLocator.kt
@@ -0,0 +1,155 @@
+/*
+ * Copyright 2026 Incredibuild
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.incredibuild.ibplugin.actions.dotnet
+
+import com.incredibuild.ibplugin.settings.IncredibuildSettings
+import com.intellij.execution.configurations.GeneralCommandLine
+import com.intellij.execution.configurations.PathEnvironmentVariableUtil
+import com.intellij.execution.util.ExecUtil
+import com.intellij.openapi.diagnostic.Logger
+import com.intellij.openapi.util.SystemInfo
+import java.io.File
+
+private val LOG = Logger.getInstance("com.incredibuild.ibplugin.actions.dotnet.MsBuildLocator")
+
+private const val VSWHERE_TIMEOUT_MS = 15_000
+
+private const val MSBUILD_COMPONENT = "Microsoft.Component.MSBuild"
+
+/**
+ * The ".NET SDK" Visual Studio component. Without it, a Visual Studio install's MSBuild has no
+ * .NET SDK resolver (Microsoft.DotNet.MSBuildSdkResolver), so it can't build any SDK-style
+ * project: every `` fails with MSB4236 "The SDK
+ * 'Microsoft.NET.Sdk' specified could not be found". A C++-only Visual Studio install still has
+ * Microsoft.Component.MSBuild, though, so requiring MSBuild alone isn't enough to tell them apart.
+ */
+private const val DOTNET_SDK_COMPONENT = "Microsoft.NetCore.Component.SDK"
+
+/** What [MsBuildLocator.locate] came back with. */
+internal sealed interface MsBuildLocation {
+ class Found(val tool: MsBuildTool) : MsBuildLocation
+
+ /** [reason] is shown to the user as-is. */
+ class NotFound(val reason: String) : MsBuildLocation
+}
+
+/**
+ * Finds the MSBuild to run accelerated .NET builds with.
+ *
+ * Rider picks its own MSBuild in its backend settings (Build, Execution, Deployment | Toolset and
+ * Build), which the frontend this plugin runs in has no API to read - so this follows the same
+ * preference Rider's own automatic choice does instead, and lets the user override it in
+ * Settings | Tools | Incredibuild when that isn't the one they use:
+ * 1. The MSBuild configured in Settings | Tools | Incredibuild, if any.
+ * 2. Windows: the newest Visual Studio / Build Tools MSBuild.exe that has the .NET SDK component,
+ * found the way Microsoft documents for it (vswhere). It builds both SDK-style and classic
+ * .NET Framework projects - and mixed solutions containing C++ ones - which `dotnet msbuild`
+ * can't. Just "the newest Visual Studio" isn't enough: a C++-only install can be newer than
+ * the one with .NET in it - see [DOTNET_SDK_COMPONENT].
+ * 3. The .NET SDK's `dotnet msbuild`, from PATH or a standard install location.
+ * 4. Windows: any Visual Studio MSBuild.exe, for classic .NET Framework-only solutions.
+ */
+internal object MsBuildLocator {
+
+ /**
+ * What [detect] last found, so a build doesn't pay for vswhere (up to [VSWHERE_TIMEOUT_MS])
+ * every time. Kept for the session, but dropped by [clearCache] when the MSBuild setting
+ * changes, and re-detected if the executable has since disappeared (e.g. Visual Studio was
+ * uninstalled). "Not found" is never cached, so installing MSBuild takes effect on the next
+ * build without restarting the IDE.
+ */
+ @Volatile
+ private var detected: MsBuildTool? = null
+
+ fun clearCache() {
+ detected = null
+ }
+
+ fun locate(): MsBuildLocation {
+ val configured = IncredibuildSettings.getInstance().msBuildPath
+ if (configured.isNotBlank()) {
+ return if (File(configured).isFile) {
+ MsBuildLocation.Found(MsBuildTool.fromPath(configured))
+ } else {
+ MsBuildLocation.NotFound(
+ "The MSBuild executable configured in Settings | Tools | Incredibuild doesn't exist:\n$configured"
+ )
+ }
+ }
+
+ detected?.takeIf { File(it.executable).isFile }?.let { return MsBuildLocation.Found(it) }
+ return detect().also { if (it is MsBuildLocation.Found) detected = it.tool }
+ }
+
+ private fun detect(): MsBuildLocation {
+ if (SystemInfo.isWindows) {
+ findVisualStudioMsBuild(MSBUILD_COMPONENT, DOTNET_SDK_COMPONENT)
+ ?.let { return MsBuildLocation.Found(MsBuildTool(it, emptyList(), it)) }
+ }
+ findDotNet()?.let { return MsBuildLocation.Found(MsBuildTool.fromPath(it)) }
+ // Last resort: a Visual Studio without the .NET SDK component still builds classic
+ // (non-SDK-style) .NET Framework projects, which is better than finding nothing at all.
+ if (SystemInfo.isWindows) {
+ findVisualStudioMsBuild(MSBUILD_COMPONENT)
+ ?.let { return MsBuildLocation.Found(MsBuildTool(it, emptyList(), it)) }
+ }
+
+ return MsBuildLocation.NotFound(
+ "No MSBuild was found on this machine: neither Visual Studio / Build Tools MSBuild nor the .NET SDK's " +
+ "dotnet command. Install one, or point Settings | Tools | Incredibuild at the MSBuild to use."
+ )
+ }
+
+ /** The newest Visual Studio / Build Tools MSBuild.exe whose install has all [requiredComponents]. */
+ private fun findVisualStudioMsBuild(vararg requiredComponents: String): String? {
+ val programFilesX86 = System.getenv("ProgramFiles(x86)") ?: return null
+ val vswhere = File(programFilesX86, "Microsoft Visual Studio\\Installer\\vswhere.exe")
+ if (!vswhere.isFile) return null
+ return try {
+ val output = ExecUtil.execAndGetOutput(
+ GeneralCommandLine(vswhere.absolutePath, "-latest", "-prerelease", "-products", "*", "-requires")
+ // vswhere's -requires takes several ids and, without -requiresAny, matches only
+ // installs that have every one of them.
+ .withParameters(*requiredComponents)
+ .withParameters("-find", "MSBuild\\**\\Bin\\MSBuild.exe"),
+ VSWHERE_TIMEOUT_MS
+ )
+ if (output.exitCode != 0) return null
+ output.stdoutLines.map { it.trim() }.firstOrNull { it.isNotEmpty() && File(it).isFile }
+ } catch (e: Exception) {
+ LOG.warn("Could not query vswhere for MSBuild", e)
+ null
+ }
+ }
+
+ private fun findDotNet(): String? {
+ val executableName = if (SystemInfo.isWindows) "dotnet.exe" else "dotnet"
+ PathEnvironmentVariableUtil.findInPath(executableName)?.let { return it.absolutePath }
+
+ val candidates = buildList {
+ System.getenv("DOTNET_ROOT")?.let { add(File(it, executableName)) }
+ if (SystemInfo.isWindows) {
+ System.getenv("ProgramFiles")?.let { add(File(it, "dotnet\\$executableName")) }
+ } else {
+ add(File("/usr/share/dotnet/dotnet"))
+ add(File("/usr/lib/dotnet/dotnet"))
+ add(File("/usr/local/share/dotnet/dotnet"))
+ }
+ add(File(System.getProperty("user.home"), ".dotnet${File.separator}$executableName"))
+ }
+ return candidates.firstOrNull { it.isFile }?.absolutePath
+ }
+}
diff --git a/src/main/kotlin/com/incredibuild/ibplugin/settings/IncredibuildConfigurable.kt b/src/main/kotlin/com/incredibuild/ibplugin/settings/IncredibuildConfigurable.kt
index 117af75..13d12c6 100644
--- a/src/main/kotlin/com/incredibuild/ibplugin/settings/IncredibuildConfigurable.kt
+++ b/src/main/kotlin/com/incredibuild/ibplugin/settings/IncredibuildConfigurable.kt
@@ -16,16 +16,25 @@
package com.incredibuild.ibplugin.settings
import com.intellij.ide.plugins.PluginManagerCore
+import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.extensions.PluginId
+import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
import com.intellij.openapi.options.BoundConfigurable
+import com.intellij.ui.dsl.builder.AlignX
import com.intellij.ui.dsl.builder.bindIntText
import com.intellij.ui.dsl.builder.bindSelected
+import com.intellij.ui.dsl.builder.bindText
import com.intellij.ui.dsl.builder.panel
private val RUST_PLUGIN_ID = PluginId.getId("com.jetbrains.rust")
-/** Settings > Tools > Incredibuild - the "-j"/"--parallel" job count and (RustRover only)
- * IDE build acceleration. */
+// Registered by rider-support.xml, so present exactly when the .NET half is loaded. Checked instead
+// of the com.intellij.modules.rider alias it's gated on: PluginManagerCore.isLoaded() only matches
+// real plugin ids, not module aliases, and PlatformUtils.isRider() is internal API.
+private const val RIDER_ACTION_ID = "Incredibuild.DotNet.BuildSolution"
+
+/** Settings > Tools > Incredibuild - the "-j"/"--parallel" job count, (RustRover only) IDE build
+ * acceleration, and (Rider only) the MSBuild to use. */
class IncredibuildConfigurable : BoundConfigurable("Incredibuild") {
override fun createPanel() = panel {
@@ -46,5 +55,18 @@ class IncredibuildConfigurable : BoundConfigurable("Incredibuild") {
"The actions in the Incredibuild menu always use Incredibuild, either way."
)
}.visible(PluginManagerCore.isLoaded(RUST_PLUGIN_ID))
+ // Only meaningful for the .NET actions (see rider-support.xml), so only shown in Rider.
+ row("MSBuild executable:") {
+ textFieldWithBrowseButton(
+ FileChooserDescriptorFactory.createSingleFileNoJarsDescriptor().withTitle("Select MSBuild or dotnet")
+ )
+ .align(AlignX.FILL)
+ .bindText(IncredibuildSettings.getInstance()::msBuildPath)
+ .comment(
+ "Used by the ... with Incredibuild .NET build actions. Leave empty to detect it " +
+ "automatically: Visual Studio's MSBuild.exe on Windows if it has the .NET SDK component, otherwise " +
+ "dotnet msbuild. Either MSBuild.exe or the dotnet executable can be given."
+ )
+ }.visible(ActionManager.getInstance().getAction(RIDER_ACTION_ID) != null)
}
}
diff --git a/src/main/kotlin/com/incredibuild/ibplugin/settings/IncredibuildSettings.kt b/src/main/kotlin/com/incredibuild/ibplugin/settings/IncredibuildSettings.kt
index ba1c9e1..f17f02e 100644
--- a/src/main/kotlin/com/incredibuild/ibplugin/settings/IncredibuildSettings.kt
+++ b/src/main/kotlin/com/incredibuild/ibplugin/settings/IncredibuildSettings.kt
@@ -15,6 +15,7 @@
*/
package com.incredibuild.ibplugin.settings
+import com.incredibuild.ibplugin.actions.dotnet.MsBuildLocator
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.PersistentStateComponent
import com.intellij.openapi.components.Service
@@ -35,6 +36,12 @@ class IncredibuildSettings : PersistentStateComponentIncredibuild
Incredibuild Build Acceleration builds your project through Incredibuild directly from the IDE
- — a Rust/Cargo project from RustRover (or IntelliJ IDEA with the Rust plugin), or a CMake project from
- CLion.
+ — a Rust/Cargo project from RustRover (or IntelliJ IDEA with the Rust plugin), a CMake project from
+ CLion, or a .NET solution from Rider.
Incredibuild is a build acceleration tool that distributes compilation work across the free cores of multiple machines on your network (or in the cloud), cutting build times without requiring any changes to your code or project structure.
A dedicated Incredibuild menu accelerates a build on demand. In RustRover:
@@ -19,7 +19,14 @@
- Rebuild — cleans and builds that same target in one accelerated step.
- Build All — builds every target in the active CMake profile.
- Stop Build — cancels the build in progress — is available either way.
+ In Rider, alongside the standard actions in the Build menu and the Solution Explorer's context menu:
+
+ - Build Solution with Incredibuild — builds the solution in its active configuration and platform.
+ - Rebuild Solution with Incredibuild — cleans and rebuilds it.
+ - Build Selected Projects with Incredibuild — builds the projects selected in the Solution Explorer, along with the projects they reference.
+
+ These run MSBuild on the solution exactly as it is configured — nothing about the solution or its projects changes. Each can be bound to a keyboard shortcut in Settings | Keymap. While an accelerated build runs, the status bar shows it is building with Incredibuild, and a notification reports the result.
+ Stop Build — cancels the build in progress — is available in every IDE.
In RustRover, the IDE's own build actions can be accelerated as well. Turn on Accelerate the IDE's own build actions in Settings | Tools | Incredibuild, and two things change: Build Project goes through Incredibuild, and so does the build that runs before Run and Test. The commands and keyboard shortcuts stay exactly where they are — only what happens underneath changes. The setting is off by default. While it is off — or if Incredibuild is not installed on the machine — those actions behave exactly as they do without this plugin. The actions in the Incredibuild menu always use Incredibuild, whichever way the setting is set.
Either way the accelerated build runs the same command the IDE itself would have run: the selected run configuration, build profile and target selection are respected for Cargo, and in CLion the accelerated build runs cmake's own generator-agnostic cmake --build against whichever build directory, toolchain and compiler the active CMake profile is already configured with, so it works the same way whether that profile uses Ninja, Unix Makefiles or NMake Makefiles. The same settings page configures how many parallel jobs accelerated builds request.
Works with Incredibuild on both Windows and Linux.
@@ -38,6 +45,9 @@
instance="com.incredibuild.ibplugin.settings.IncredibuildConfigurable"
id="com.incredibuild.ibplugin.settings.IncredibuildConfigurable"
displayName="Incredibuild"/>
+
+
@@ -59,10 +69,11 @@
-
+
com.jetbrains.rust
com.intellij.cmake
+ com.intellij.modules.rider
diff --git a/src/main/resources/META-INF/rider-support.xml b/src/main/resources/META-INF/rider-support.xml
new file mode 100644
index 0000000..85ed321
--- /dev/null
+++ b/src/main/resources/META-INF/rider-support.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/test/kotlin/com/incredibuild/ibplugin/actions/IncredibuildRunnerTest.kt b/src/test/kotlin/com/incredibuild/ibplugin/actions/IncredibuildRunnerTest.kt
index 12728ef..24e4de2 100644
--- a/src/test/kotlin/com/incredibuild/ibplugin/actions/IncredibuildRunnerTest.kt
+++ b/src/test/kotlin/com/incredibuild/ibplugin/actions/IncredibuildRunnerTest.kt
@@ -24,6 +24,18 @@ import org.junit.Test
* wrapper's `%` escaping) and [VALID_SHELL_IDENTIFIER] (the Linux environment export filter). */
class IncredibuildRunnerTest {
+ @Test
+ fun `pathWithFirst puts the cargo directory ahead of the existing PATH`() {
+ val sep = java.io.File.pathSeparator
+ assertEquals("C:\\cargo\\bin${sep}C:\\Windows", IncredibuildRunner.pathWithFirst("C:\\cargo\\bin", "C:\\Windows"))
+ }
+
+ @Test
+ fun `pathWithFirst is just the directory when there is no PATH`() {
+ assertEquals("/home/u/.cargo/bin", IncredibuildRunner.pathWithFirst("/home/u/.cargo/bin", null))
+ assertEquals("/home/u/.cargo/bin", IncredibuildRunner.pathWithFirst("/home/u/.cargo/bin", ""))
+ }
+
@Test
fun `batchEscaped doubles a lone percent`() {
assertEquals("%%TEMP%%", IncredibuildRunner.batchEscaped("%TEMP%"))
@@ -55,6 +67,24 @@ class IncredibuildRunnerTest {
assertEquals("\"%%TEMP%% dir\"", IncredibuildRunner.cmdQuoted("%TEMP% dir"))
}
+ @Test
+ fun `cmdQuoted doubles trailing backslashes so they don't escape the closing quote`() {
+ assertEquals(
+ "\"-p:SolutionDir=C:\\My Projects\\App\\\\\"",
+ IncredibuildRunner.cmdQuoted("-p:SolutionDir=C:\\My Projects\\App\\")
+ )
+ // Unquoted, a trailing backslash is just a literal one and needs nothing.
+ assertEquals("-p:SolutionDir=C:\\App\\", IncredibuildRunner.cmdQuoted("-p:SolutionDir=C:\\App\\"))
+ }
+
+ @Test
+ fun `cmdQuoted quotes cmd operator characters and empty values`() {
+ assertEquals("\"C:\\R&D\\App.sln\"", IncredibuildRunner.cmdQuoted("C:\\R&D\\App.sln"))
+ assertEquals("\"C:\\Apps (old)\\App.sln\"", IncredibuildRunner.cmdQuoted("C:\\Apps (old)\\App.sln"))
+ assertEquals("\"a^b\"", IncredibuildRunner.cmdQuoted("a^b"))
+ assertEquals("\"\"", IncredibuildRunner.cmdQuoted(""))
+ }
+
@Test
fun `VALID_SHELL_IDENTIFIER accepts ordinary environment variable names`() {
assertTrue(VALID_SHELL_IDENTIFIER.matches("PATH"))
diff --git a/src/test/kotlin/com/incredibuild/ibplugin/actions/dotnet/MsBuildCommandTest.kt b/src/test/kotlin/com/incredibuild/ibplugin/actions/dotnet/MsBuildCommandTest.kt
new file mode 100644
index 0000000..cbcafd9
--- /dev/null
+++ b/src/test/kotlin/com/incredibuild/ibplugin/actions/dotnet/MsBuildCommandTest.kt
@@ -0,0 +1,372 @@
+/*
+ * Copyright 2026 Incredibuild
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.incredibuild.ibplugin.actions.dotnet
+
+import com.incredibuild.ibplugin.actions.IncredibuildRunner
+import com.incredibuild.ibplugin.actions.IncredibuildRunner.NativeBuild
+import com.incredibuild.ibplugin.actions.IncredibuildRunner.NativeCommand
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import java.io.File
+import java.nio.file.Files
+
+/** Covers how the .NET actions turn Rider's state into MSBuild invocations, and the wrapper
+ * scripts those invocations are run through. */
+class MsBuildCommandTest {
+
+ private val solutionDir = File("work", "Sln").absoluteFile
+ private val solutionFile = File(solutionDir, "App.sln").path
+ private val appProject = File(solutionDir, "src${File.separator}App${File.separator}App.csproj").path
+ private val libProject = File(solutionDir, "Lib${File.separator}Lib.csproj").path
+
+ private val msBuild = MsBuildTool("C:\\VS\\MSBuild.exe", emptyList(), "MSBuild")
+
+ private val sln = """
+ Microsoft Visual Studio Solution File, Format Version 12.00
+ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{33333333-3333-3333-3333-333333333333}"
+ EndProject
+ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "App", "src\App\App.csproj", "{11111111-1111-1111-1111-111111111111}"
+ ProjectSection(ProjectDependencies) = postProject
+ {22222222-2222-2222-2222-222222222222} = {22222222-2222-2222-2222-222222222222}
+ EndProjectSection
+ EndProject
+ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "My.Lib", "Lib\Lib.csproj", "{22222222-2222-2222-2222-222222222222}"
+ EndProject
+ Global
+ GlobalSection(NestedProjects) = preSolution
+ {11111111-1111-1111-1111-111111111111} = {33333333-3333-3333-3333-333333333333}
+ EndGlobalSection
+ EndGlobal
+ """.trimIndent()
+
+ private val targets = mapOf(normalizedPath(appProject) to "src\\App", normalizedPath(libProject) to "My_Lib")
+
+ @Test
+ fun `MsBuildTool fromPath runs dotnet through its msbuild verb`() {
+ val tool = MsBuildTool.fromPath("C:\\Program Files\\dotnet\\dotnet.exe")
+ assertEquals(listOf("msbuild"), tool.leadingArguments)
+ }
+
+ @Test
+ fun `MsBuildTool fromPath runs MSBuild exe directly`() {
+ assertEquals(emptyList(), MsBuildTool.fromPath("C:\\VS\\MSBuild.exe").leadingArguments)
+ }
+
+ @Test
+ fun `solution build passes the target and active configuration unchanged`() {
+ val build = solutionBuild(msBuild, solutionFile, DotNetBuildKind.REBUILD_SOLUTION, "Debug", "Any CPU")
+ val command = build.commands.single()
+ assertEquals(msBuild.executable, command.executable)
+ assertEquals(
+ listOf(solutionFile, "-t:Rebuild", "-p:Configuration=Debug", "-p:Platform=Any CPU"),
+ command.arguments.take(4)
+ )
+ assertTrue(command.arguments.containsAll(COMMON_MSBUILD_ARGUMENTS))
+ assertEquals(solutionDir.path, build.workingDirectory)
+ }
+
+ @Test
+ fun `solution build through dotnet puts msbuild first`() {
+ val dotnet = MsBuildTool.fromPath("/usr/bin/dotnet")
+ val command = solutionBuild(dotnet, solutionFile, DotNetBuildKind.BUILD_SOLUTION, "Debug", "Any CPU").commands.single()
+ assertEquals(listOf("msbuild", solutionFile, "-t:Build"), command.arguments.take(3))
+ }
+
+ @Test
+ fun `sln project targets follow solution folders and MSBuild's name cleansing`() {
+ // App's ProjectDependencies line has the same {guid} = {guid} shape as a nesting one - it
+ // must not make Lib App's parent.
+ assertEquals(targets, solutionProjectTargets(sln, solutionDir, isSlnx = false))
+ }
+
+ @Test
+ fun `sln project targets nest folders inside folders`() {
+ val nested = """
+ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{33333333-3333-3333-3333-333333333333}"
+ EndProject
+ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "my.dir (x)", "my.dir (x)", "{44444444-4444-4444-4444-444444444444}"
+ EndProject
+ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lib", "Lib\Lib.csproj", "{22222222-2222-2222-2222-222222222222}"
+ EndProject
+ Global
+ GlobalSection(NestedProjects) = preSolution
+ {44444444-4444-4444-4444-444444444444} = {33333333-3333-3333-3333-333333333333}
+ {22222222-2222-2222-2222-222222222222} = {44444444-4444-4444-4444-444444444444}
+ EndGlobalSection
+ EndGlobal
+ """.trimIndent()
+ assertEquals(
+ mapOf(normalizedPath(libProject) to "src\\my_dir _x_\\Lib"),
+ solutionProjectTargets(nested, solutionDir, isSlnx = false)
+ )
+ }
+
+ @Test
+ fun `slnx project targets use folder paths and project file names`() {
+ val slnx = """
+
+
+
+
+
+
+ """.trimIndent()
+ assertEquals(
+ mapOf(normalizedPath(libProject) to "src\\my_dir _x_\\Lib", normalizedPath(appProject) to "App"),
+ solutionProjectTargets(slnx, solutionDir, isSlnx = true)
+ )
+ }
+
+ @Test
+ fun `projects build goes through the solution with one target per project`() {
+ // Through the solution, so MSBuild maps every project - and every project it references -
+ // to the solution's own configuration for it, instead of passing the selected one on.
+ val build = projectsBuild(msBuild, solutionFile, listOf(appProject, libProject), "Release", "x64", targets)
+ val command = build.commands.single()
+ assertEquals(
+ listOf(solutionFile, "-t:src\\App;My_Lib", "-p:Configuration=Release", "-p:Platform=x64"),
+ command.arguments.take(4)
+ )
+ assertTrue(command.arguments.containsAll(COMMON_MSBUILD_ARGUMENTS))
+ assertEquals(solutionDir.path, build.workingDirectory)
+ }
+
+ @Test
+ fun `builds leave restore to Rider by default`() {
+ val solution = solutionBuild(msBuild, solutionFile, DotNetBuildKind.BUILD_SOLUTION, "Debug", "Any CPU")
+ val projects = projectsBuild(msBuild, solutionFile, listOf(appProject), "Debug", "Any CPU", targets)
+ assertFalse(solution.commands.single().arguments.contains(RESTORE_ARGUMENT))
+ assertFalse(projects.commands.single().arguments.contains(RESTORE_ARGUMENT))
+ }
+
+ @Test
+ fun `builds restore when asked to`() {
+ val solution = solutionBuild(msBuild, solutionFile, DotNetBuildKind.BUILD_SOLUTION, "Debug", "Any CPU", restore = true)
+ val projects =
+ projectsBuild(msBuild, solutionFile, listOf(appProject), "Debug", "Any CPU", targets, restore = true)
+ assertTrue(solution.commands.single().arguments.contains(RESTORE_ARGUMENT))
+ assertTrue(projects.commands.single().arguments.contains(RESTORE_ARGUMENT))
+ }
+
+ private fun withTempDir(block: (File) -> Unit) {
+ val root = Files.createTempDirectory("msbuild-restore-test").toFile()
+ try {
+ block(root)
+ } finally {
+ root.deleteRecursively()
+ }
+ }
+
+ private fun File.project(relativePath: String, text: String) =
+ File(this, relativePath).apply { parentFile.mkdirs(); writeText(text) }
+
+ private fun File.assetsFile(relativeDir: String) =
+ File(this, "$relativeDir/project.assets.json").apply { parentFile.mkdirs(); writeText("{}") }
+
+ private val sdkProject = """"""
+
+ @Test
+ fun `needsRestore is true only when an SDK-style project has no assets file`() = withTempDir { root ->
+ val restored = root.project("Restored/Restored.csproj", sdkProject)
+ root.assetsFile("Restored/obj")
+ val fresh = root.project("Fresh/Fresh.csproj", sdkProject)
+
+ assertFalse(needsRestore(listOf(restored.path)))
+ assertTrue(needsRestore(listOf(restored.path, fresh.path)))
+ assertFalse(needsRestore(emptyList()))
+ }
+
+ @Test
+ fun `needsRestore ignores projects that never get an assets file`() = withTempDir { root ->
+ val classic = root.project("Classic/Classic.csproj", """""")
+ val shared = root.project("Shared/Shared.shproj", """""")
+ val cpp = root.project("Native/Native.vcxproj", """""")
+ val missing = File(root, "Gone/Gone.csproj").path
+
+ assertFalse(needsRestore(listOf(classic.path, shared.path, cpp.path, missing)))
+ }
+
+ @Test
+ fun `needsRestore counts classic projects using PackageReference`() = withTempDir { root ->
+ val classic = root.project(
+ "Classic/Classic.csproj",
+ """"""
+ )
+ assertTrue(needsRestore(listOf(classic.path)))
+ root.assetsFile("Classic/obj")
+ assertFalse(needsRestore(listOf(classic.path)))
+ }
+
+ @Test
+ fun `needsRestore finds the assets file in the artifacts output layout`() = withTempDir { root ->
+ val app = root.project("src/App/App.csproj", sdkProject)
+ assertTrue(needsRestore(listOf(app.path)))
+ root.assetsFile("artifacts/obj/App")
+ assertFalse(needsRestore(listOf(app.path)))
+ }
+
+ @Test
+ fun `solutionProjectFiles lists the projects of an sln`() {
+ assertEquals(
+ listOf(appProject, libProject),
+ solutionProjectFiles(sln, solutionDir)
+ )
+ }
+
+ @Test
+ fun `solutionProjectFiles lists the projects of an slnx and skips non-project entries`() {
+ val slnx = """
+
+
+
+
+
+
+
+ """.trimIndent()
+ assertEquals(listOf(appProject, libProject), solutionProjectFiles(slnx, solutionDir))
+ }
+
+ @Test
+ fun `solutionFilterProjectFiles lists only the filter's projects, relative to the solution`() {
+ // The filter in a subfolder of the solution, as Rider allows: its solution path is relative
+ // to the filter, its projects to the solution - which is how MSBuild resolves them.
+ val filterDir = File(solutionDir, "filters")
+ val slnf = """
+ {
+ "solution": {
+ "path": "..\\Sln.sln",
+ "projects": [
+ "src\\App\\App.csproj"
+ ]
+ }
+ }
+ """.trimIndent()
+ assertEquals(listOf(appProject), solutionFilterProjectFiles(slnf, filterDir))
+ }
+
+ @Test
+ fun `solutionFilterProjectFiles accepts forward slashes`() {
+ val slnf = """{"solution": {"path": "Sln.sln", "projects": ["src/App/App.csproj", "Lib/Lib.csproj"]}}"""
+ assertEquals(listOf(appProject, libProject), solutionFilterProjectFiles(slnf, solutionDir))
+ }
+
+ @Test
+ fun `a filter excluding the only unrestored project doesn't need restore`() = withTempDir { root ->
+ // App is restored, Unrestored never was; a filter with only App mustn't ask for -restore,
+ // though the whole solution would.
+ val sdkProject = """"""
+ val app = File(root, "App/App.csproj").apply { parentFile.mkdirs(); writeText(sdkProject) }
+ File(root, "App/obj/project.assets.json").apply { parentFile.mkdirs(); writeText("{}") }
+ val unrestored = File(root, "Unrestored/Unrestored.csproj").apply { parentFile.mkdirs(); writeText(sdkProject) }
+ val slnf = """{"solution": {"path": "Sln.sln", "projects": ["App\\App.csproj"]}}"""
+
+ assertFalse(needsRestore(solutionFilterProjectFiles(slnf, root)))
+ assertTrue(needsRestore(listOf(app.path, unrestored.path)))
+ }
+
+ @Test
+ fun `windows wrapper stops at the first failing command`() {
+ val build = NativeBuild(
+ listOf(NativeCommand("msbuild", listOf("a b.csproj")), NativeCommand("msbuild", listOf("c.csproj"))),
+ "."
+ )
+ assertEquals(
+ "@echo off\r\n" +
+ "msbuild \"a b.csproj\"\r\n" +
+ "if not \"%ERRORLEVEL%\"==\"0\" exit /b %ERRORLEVEL%\r\n" +
+ "msbuild c.csproj\r\n" +
+ "exit /b %ERRORLEVEL%\r\n",
+ IncredibuildRunner.windowsWrapperScript(build)
+ )
+ }
+
+ @Test
+ fun `windows wrapper for a single command is unchanged from the CMake one`() {
+ val build = NativeBuild(listOf(NativeCommand("cmake", listOf("--build", "out"))), ".", mapOf("A" to "1"))
+ assertEquals(
+ "@echo off\r\nset \"A=1\"\r\ncmake --build out\r\nexit /b %ERRORLEVEL%\r\n",
+ IncredibuildRunner.windowsWrapperScript(build)
+ )
+ }
+
+ @Test
+ fun `linux wrapper chains several commands inside one ib_console`() {
+ val build = NativeBuild(
+ listOf(NativeCommand("dotnet", listOf("msbuild", "a.csproj")), NativeCommand("dotnet", listOf("msbuild", "b.csproj"))),
+ "."
+ )
+ val script = IncredibuildRunner.linuxWrapperScript("Sln", "/opt/incredibuild/bin/ib_console", build)
+ assertTrue(
+ script,
+ script.endsWith(
+ "-- /bin/sh -c ''\\''dotnet'\\'' '\\''msbuild'\\'' '\\''a.csproj'\\'' && " +
+ "'\\''dotnet'\\'' '\\''msbuild'\\'' '\\''b.csproj'\\''' 2>&1\n"
+ )
+ )
+ }
+
+ @Test
+ fun `banner project list is capped for large selections`() {
+ assertEquals("App", DotNetBuildRunner.cappedProjectList(listOf("App")))
+ // Three still fit: "+1 more" would be no shorter than the name it hides.
+ assertEquals("App, Lib, Tests", DotNetBuildRunner.cappedProjectList(listOf("App", "Lib", "Tests")))
+ assertEquals(
+ "P0, P1 +12 more",
+ DotNetBuildRunner.cappedProjectList((0 until 14).map { "P$it" })
+ )
+ }
+
+ @Test
+ fun `msbuild builds leave Build Cache off`() {
+ val tool = MsBuildTool("msbuild", emptyList(), "MSBuild")
+ assertFalse(solutionBuild(tool, "Sln.sln", DotNetBuildKind.BUILD_SOLUTION, "Debug", "Any CPU").buildCache)
+ assertFalse(
+ projectsBuild(tool, "Sln.sln", listOf(appProject), "Debug", "Any CPU", mapOf(normalizedPath(appProject) to "src\\App"))
+ .buildCache
+ )
+ }
+
+ @Test
+ fun `linux wrapper passes the build cache options only when the build wants them`() {
+ val command = listOf(NativeCommand("cmake", listOf("--build", "out")))
+ val cached = IncredibuildRunner.linuxWrapperScript("P", "/ib", NativeBuild(command, "."))
+ val uncached = IncredibuildRunner.linuxWrapperScript("P", "/ib", NativeBuild(command, ".", buildCache = false))
+ assertTrue(cached, cached.contains(" --ib-quiet --build-cache-local-shared --build-cache-basedir=\"\$PWD\" -- "))
+ assertTrue(uncached, uncached.contains(" --ib-quiet -- "))
+ assertFalse(uncached, uncached.contains("build-cache"))
+ }
+
+ @Test
+ fun `windows launcher passes BUILDCACHELOCAL only when the build wants it`() {
+ val cached = IncredibuildRunner.windowsLauncherScript("BuildConsole.exe", "w.cmd", "P", buildCache = true)
+ val uncached = IncredibuildRunner.windowsLauncherScript("BuildConsole.exe", "w.cmd", "P", buildCache = false)
+ assertEquals(
+ "& \"BuildConsole.exe\" '/command=\"w.cmd\"' '/title=\"P\"' /BUILDCACHELOCAL=ON\nexit \$LASTEXITCODE\n",
+ cached
+ )
+ assertEquals("& \"BuildConsole.exe\" '/command=\"w.cmd\"' '/title=\"P\"'\nexit \$LASTEXITCODE\n", uncached)
+ }
+
+ @Test
+ fun `linux wrapper runs a single command directly`() {
+ val build = NativeBuild(listOf(NativeCommand("cmake", listOf("--build", "out"))), ".")
+ val script = IncredibuildRunner.linuxWrapperScript("P", "/ib", build)
+ assertTrue(script, script.endsWith("-- 'cmake' '--build' 'out' 2>&1\n"))
+ }
+}