Skip to content

Commit f653e49

Browse files
committed
Add modern .NET SDK MSBuild auto-detection and improve macOS/Linux support
- Auto-detect dotnet SDK MSBuild on non-Windows platforms - Add msBuildPath property to NuGetRestore for explicit MSBuild configuration - Add ignoreFailuresOnNonWindows property to gracefully handle Mono xbuild issues - Improve ignoreExitValue handling for Gradle 8 compatibility - Use Exec task's built-in execution when ignoring failures to respect ignoreExitValue
1 parent 0f4b4d5 commit f653e49

File tree

3 files changed

+159
-33
lines changed

3 files changed

+159
-33
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,17 @@
22

33
## 2.24
44

5+
### Added
6+
* Auto-detection of modern .NET SDK MSBuild on macOS/Linux platforms
7+
* Added `msBuildPath` property to `NuGetRestore` task for explicit MSBuild path configuration
8+
* Added `ignoreFailuresOnNonWindows` property to `NuGetRestore` task to gracefully handle Mono xbuild limitations
9+
510
### Fixed
611
* Fixed stack overflow in Gradle 8 when calling `super.exec()` by using `@TaskAction` instead of overriding `exec()`
712
* Fixed constructor injection compatibility for both Gradle 8 (no injection) and Gradle 9+ (with injection)
813
* Fixed `nugetExePath` resolution to use project directory instead of Gradle daemon working directory
914
* Changed private helper methods to `protected @Internal` for Gradle 9 task validation
15+
* Improved MSBuild handling on non-Windows platforms by auto-detecting dotnet SDK and using `ignoreExitValue` when configured
1016

1117
## 2.22
1218
### Fixed

src/main/groovy/com/ullink/BaseNuGet.groovy

Lines changed: 37 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -165,46 +165,53 @@ class BaseNuGet extends Exec {
165165
args '-NonInteractive'
166166
args '-Verbosity', (verbosity ?: getNugetVerbosity())
167167

168-
// Execute using the ExecActionFactory to avoid method interception recursion
169-
def execActionFactory = getExecActionFactory()
170-
if (execActionFactory != null) {
171-
def execAction = execActionFactory.newExecAction()
172-
execAction.setExecutable(executable)
173-
execAction.setArgs(getArgs())
174-
execAction.setWorkingDir(project.projectDir)
175-
176-
// Check if we should ignore failures on non-Windows (for Mono xbuild issues)
177-
def shouldIgnoreFailures = false
168+
// Check if we should ignore failures on non-Windows (for Mono xbuild issues)
169+
// Set ignoreExitValue on the Exec task itself before execution
170+
def shouldIgnoreFailures = false
171+
try {
178172
if (this.hasProperty('ignoreFailuresOnNonWindows') && this.ignoreFailuresOnNonWindows) {
179173
shouldIgnoreFailures = !isFamily(FAMILY_WINDOWS)
180-
}
181-
182-
try {
183-
execAction.execute()
184-
} catch (Exception e) {
185174
if (shouldIgnoreFailures) {
186-
project.logger.warn("NuGet restore failed on non-Windows platform (likely Mono xbuild issue), ignoring: ${e.message}")
187-
return
175+
project.logger.debug("Will ignore failures on non-Windows platform")
176+
// Use the Exec task's built-in ignoreExitValue property
177+
this.ignoreExitValue = true
188178
}
189-
throw e
190179
}
191-
} else {
192-
// Fallback: try to call Exec.exec() via reflection
180+
} catch (Exception e) {
181+
project.logger.debug("Could not check ignoreFailuresOnNonWindows: ${e.message}")
182+
}
183+
184+
// Execute using the ExecActionFactory to avoid method interception recursion
185+
// However, if we need to ignore exit values, we must use the Exec task's built-in execution
186+
// because ExecAction doesn't respect the Exec task's ignoreExitValue property
187+
if (shouldIgnoreFailures) {
188+
// Use Exec task's built-in execution when ignoring failures (respects ignoreExitValue)
193189
try {
194190
def execMethod = Exec.class.getDeclaredMethod("exec")
195191
execMethod.setAccessible(true)
196192
execMethod.invoke(this)
197-
} catch (Exception e) {
198-
// Check if we should ignore failures on non-Windows
199-
def shouldIgnoreFailures = false
200-
if (this.hasProperty('ignoreFailuresOnNonWindows') && this.ignoreFailuresOnNonWindows) {
201-
shouldIgnoreFailures = !isFamily(FAMILY_WINDOWS)
202-
}
203-
if (shouldIgnoreFailures) {
204-
project.logger.warn("NuGet restore failed on non-Windows platform (likely Mono xbuild issue), ignoring: ${e.message}")
205-
return
193+
} catch (Throwable e) {
194+
project.logger.warn("NuGet restore failed on non-Windows platform (likely Mono xbuild issue), ignoring: ${e.class.simpleName}: ${e.message}")
195+
return
196+
}
197+
} else {
198+
// Use ExecActionFactory for normal execution (avoids stack overflow in Gradle 8)
199+
def execActionFactory = getExecActionFactory()
200+
if (execActionFactory != null) {
201+
def execAction = execActionFactory.newExecAction()
202+
execAction.setExecutable(executable)
203+
execAction.setArgs(getArgs())
204+
execAction.setWorkingDir(project.projectDir)
205+
execAction.execute()
206+
} else {
207+
// Fallback: try to call Exec.exec() via reflection
208+
try {
209+
def execMethod = Exec.class.getDeclaredMethod("exec")
210+
execMethod.setAccessible(true)
211+
execMethod.invoke(this)
212+
} catch (Exception e) {
213+
throw new RuntimeException("Cannot execute NuGet command - no ExecActionFactory available", e)
206214
}
207-
throw new RuntimeException("Cannot execute NuGet command - no ExecActionFactory available", e)
208215
}
209216
}
210217
}

src/main/groovy/com/ullink/NuGetRestore.groovy

Lines changed: 116 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import com.ullink.util.GradleHelper
44
import org.gradle.api.tasks.Input
55
import org.gradle.api.tasks.InputDirectory
66
import org.gradle.api.tasks.InputFile
7+
import org.gradle.api.tasks.Internal
78
import org.gradle.api.tasks.Optional
89
import org.gradle.api.tasks.OutputDirectory
910

@@ -37,6 +38,9 @@ class NuGetRestore extends BaseNuGet {
3738
def msBuildVersion
3839
@Optional
3940
@Input
41+
def msBuildPath
42+
@Optional
43+
@Input
4044
def packagesDirectory
4145
@Input
4246
def ignoreFailuresOnNonWindows = false
@@ -86,14 +90,39 @@ class NuGetRestore extends BaseNuGet {
8690
if (solutionDirectory) args '-SolutionDirectory', solutionDirectory
8791
if (disableParallelProcessing) args '-DisableParallelProcessing'
8892

89-
// Skip MSBuildVersion on non-Windows platforms (macOS/Linux) because Mono's xbuild/MSBuild
90-
// doesn't work properly with NuGet restore and causes "Too many project files specified" errors
93+
// On non-Windows platforms, try to use modern .NET SDK's MSBuild if available
9194
if (!isFamily(FAMILY_WINDOWS)) {
92-
project.logger.debug("Skipping MSBuildVersion on non-Windows platform to avoid Mono xbuild issues")
95+
// If msBuildPath is not explicitly set, try to auto-detect dotnet SDK MSBuild
96+
if (!msBuildPath) {
97+
def dotnetPath = findDotnetPath()
98+
if (dotnetPath) {
99+
def dotnetMsbuildPath = findDotnetMsbuildPath(dotnetPath)
100+
if (dotnetMsbuildPath) {
101+
msBuildPath = dotnetMsbuildPath
102+
project.logger.debug("Auto-detected dotnet MSBuild at: ${msBuildPath}")
103+
}
104+
}
105+
}
106+
107+
// Use MSBuildPath if available (takes precedence over MSBuildVersion)
108+
if (msBuildPath) {
109+
args '-MSBuildPath', msBuildPath
110+
project.logger.debug("Using MSBuildPath: ${msBuildPath}")
111+
} else {
112+
// Skip MSBuildVersion on non-Windows if no MSBuildPath found
113+
// because Mono's xbuild/MSBuild doesn't work properly with NuGet restore
114+
project.logger.debug("Skipping MSBuildVersion on non-Windows platform (no dotnet MSBuild found)")
115+
}
93116
} else {
117+
// On Windows, use MSBuildVersion as before
94118
if (!msBuildVersion) msBuildVersion = GradleHelper.getPropertyFromTask(project, 'version', 'msbuild')
95119
if (msBuildVersion) args '-MsBuildVersion', msBuildVersion
96120
}
121+
122+
// MSBuildPath can also be explicitly set on Windows
123+
if (msBuildPath) {
124+
args '-MSBuildPath', msBuildPath
125+
}
97126

98127
project.logger.info "Restoring NuGet packages " +
99128
(sources ? "from $sources" : '') +
@@ -116,4 +145,88 @@ class NuGetRestore extends BaseNuGet {
116145
def solutionDir = solutionFile ? project.file(solutionFile.getParent()) : solutionDirectory
117146
return new File(solutionDir ? solutionDir.toString() : '.', 'packages')
118147
}
148+
149+
/**
150+
* Find the dotnet executable path
151+
*/
152+
private String findDotnetPath() {
153+
try {
154+
def process = ['which', 'dotnet'].execute()
155+
process.waitFor()
156+
if (process.exitValue() == 0) {
157+
return process.text.trim()
158+
}
159+
} catch (Exception e) {
160+
// dotnet not found
161+
}
162+
return null
163+
}
164+
165+
/**
166+
* Find the MSBuild.dll path in the dotnet SDK
167+
*/
168+
private String findDotnetMsbuildPath(String dotnetPath) {
169+
try {
170+
// Get dotnet SDK path
171+
def process = [dotnetPath, '--info'].execute()
172+
process.waitFor()
173+
def output = process.text
174+
175+
// Look for SDK base path
176+
def sdkBasePath = null
177+
output.eachLine { line ->
178+
if (line.contains('Base Path:') || line.contains('SDK Base Path:')) {
179+
def path = line.split(':')[1]?.trim()
180+
if (path) {
181+
sdkBasePath = path
182+
}
183+
}
184+
}
185+
186+
if (sdkBasePath) {
187+
// Try to find MSBuild.dll in the SDK
188+
def msbuildDll = new File(sdkBasePath, 'MSBuild.dll')
189+
if (msbuildDll.exists()) {
190+
return msbuildDll.parentFile.absolutePath
191+
}
192+
193+
// Alternative: look in Current/MSBuild directory
194+
def currentMsbuild = new File(sdkBasePath, 'Current/MSBuild.dll')
195+
if (currentMsbuild.exists()) {
196+
return currentMsbuild.parentFile.absolutePath
197+
}
198+
}
199+
200+
// Fallback: try common SDK locations
201+
def commonPaths = [
202+
'/usr/local/share/dotnet/sdk',
203+
'/usr/share/dotnet/sdk',
204+
System.getProperty('user.home') + '/.dotnet/sdk'
205+
]
206+
207+
for (def basePath : commonPaths) {
208+
def sdkDir = new File(basePath)
209+
if (sdkDir.exists() && sdkDir.isDirectory()) {
210+
// Find the highest version SDK
211+
def sdkVersions = sdkDir.listFiles().findAll { it.isDirectory() && it.name.matches(/^\d+\.\d+\.\d+.*/) }
212+
if (sdkVersions) {
213+
sdkVersions.sort { a, b ->
214+
// Simple version comparison
215+
def aVer = a.name.split(/[.-]/).collect { it.toInteger() }
216+
def bVer = b.name.split(/[.-]/).collect { it.toInteger() }
217+
return bVer <=> aVer
218+
}
219+
def latestSdk = sdkVersions[0]
220+
def msbuildDll = new File(latestSdk, 'MSBuild.dll')
221+
if (msbuildDll.exists()) {
222+
return msbuildDll.parentFile.absolutePath
223+
}
224+
}
225+
}
226+
}
227+
} catch (Exception e) {
228+
project.logger.debug("Could not find dotnet MSBuild: ${e.message}")
229+
}
230+
return null
231+
}
119232
}

0 commit comments

Comments
 (0)