Skip to content

Commit 750fe61

Browse files
committed
Fix assemblyInfoPatcher dependency resolution when dotnet is unavailable
- Add defensive error handling in AssemblyInfoVersionPatcher provider evaluation - Catch process execution failures when dotnet command cannot be started - Handle Windows and Linux-specific error messages gracefully - Make provider evaluation return empty lists instead of throwing exceptions - Add OS-aware warning messages (Windows MSBuild vs Mono's MSBuild) - Update version to 4.9-SNAPSHOT - Update CHANGELOG with all fixes This fixes the issue where builds fail during task dependency resolution on Windows servers without dotnet SDK installed, allowing builds to proceed using Windows MSBuild instead.
1 parent fe3ae0f commit 750fe61

File tree

4 files changed

+99
-27
lines changed

4 files changed

+99
-27
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@
1212
* ProjectFileParser Linux compatibility
1313
* MSBuild task execution for Gradle 9
1414
* Snapshot publishing optimizations
15+
* Fixed `assemblyInfoPatcher` task dependency resolution failure when `dotnet` command is not available
16+
* Improved error handling for process execution failures when `dotnet` cannot be started (Windows and Linux)
17+
* Made `assemblyInfoPatcher` provider evaluation defensive to gracefully handle exceptions during configuration phase
18+
* OS-aware warning messages (Windows MSBuild vs Mono's MSBuild)
1519

1620
### Changed
1721
* Updated Gradle wrapper from 6.9 to 9.2.1

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
version = 4.8
1+
version = 4.9-SNAPSHOT

src/main/groovy/com/ullink/AssemblyInfoVersionPatcher.groovy

Lines changed: 53 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,26 +18,64 @@ class AssemblyInfoVersionPatcher extends DefaultTask {
1818
AssemblyInfoVersionPatcher() {
1919
projects = project.getObjects().listProperty(String)
2020
projects.set(project.provider({
21-
project.tasks.msbuild.projects.collect { it.key }
21+
try {
22+
def msbuildTask = project.tasks.findByName('msbuild')
23+
if (msbuildTask == null) {
24+
project.logger.debug("AssemblyInfoPatcher: msbuild task not found, using empty list")
25+
return []
26+
}
27+
msbuildTask.projects.collect { it.key }
28+
} catch (Exception e) {
29+
// If accessing msbuild.projects fails (e.g., dotnet not available), return empty list
30+
// This allows the build to continue without project parsing
31+
project.logger.debug("AssemblyInfoPatcher: Could not access msbuild.projects, using empty list. Error: ${e.message}")
32+
[]
33+
}
2234
}))
2335

2436
files = project.getObjects().listProperty(File)
2537
files.set(project.provider({
26-
projects.get()
27-
.collect { project.tasks.msbuild.projects[it] }
28-
.collect {
29-
if (it.properties.UsingMicrosoftNETSdk == 'true') {
30-
it.properties.MSBuildProjectFullPath
31-
} else {
32-
it?.getItems('Compile')?.find { Files.getNameWithoutExtension(it.name) == 'AssemblyInfo' }
33-
}
34-
}
35-
.findAll { it != null }
36-
.unique()
37-
.collect {
38-
project.logger.info("AssemblyInfoPatcher: found file ${it} (${it?.class})")
39-
project.file(it)
38+
try {
39+
def msbuildTask = project.tasks.findByName('msbuild')
40+
if (msbuildTask == null) {
41+
project.logger.debug("AssemblyInfoPatcher: msbuild task not found, using empty file list")
42+
return []
4043
}
44+
projects.get()
45+
.collect {
46+
try {
47+
msbuildTask.projects[it]
48+
} catch (Exception e) {
49+
project.logger.debug("AssemblyInfoPatcher: Could not access project '${it}', skipping. Error: ${e.message}")
50+
null
51+
}
52+
}
53+
.findAll { it != null }
54+
.collect {
55+
try {
56+
def result
57+
if (it.properties.UsingMicrosoftNETSdk == 'true') {
58+
result = it.properties.MSBuildProjectFullPath
59+
} else {
60+
result = it?.getItems('Compile')?.find { Files.getNameWithoutExtension(it.name) == 'AssemblyInfo' }
61+
}
62+
result
63+
} catch (Exception e) {
64+
project.logger.debug("AssemblyInfoPatcher: Error processing project file, skipping. Error: ${e.message}")
65+
null
66+
}
67+
}
68+
.findAll { it != null }
69+
.unique()
70+
.collect {
71+
project.logger.info("AssemblyInfoPatcher: found file ${it} (${it?.class})")
72+
project.file(it)
73+
}
74+
} catch (Exception e) {
75+
// If file resolution fails, return empty list
76+
project.logger.debug("AssemblyInfoPatcher: Could not resolve files, using empty list. Error: ${e.message}")
77+
[]
78+
}
4179
}))
4280

4381
fileVersion = project.getObjects().property(String)

src/main/groovy/com/ullink/Msbuild.groovy

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -115,9 +115,10 @@ class Msbuild extends ConventionTask {
115115
version.startsWith('4.') || version == '14.0' || version == '12.0')
116116

117117
if (useOldMsbuild) {
118+
def msbuildType = OperatingSystem.current().windows ? "Windows MSBuild" : "Mono's MSBuild"
118119
logger.warn("Skipping project file parsing for old MSBuild version (${version}). " +
119120
"ProjectFileParser uses .NET SDK MSBuild which cannot parse old-style projects. " +
120-
"The build will proceed using Mono's MSBuild.")
121+
"The build will proceed using ${msbuildType}.")
121122
parseProject = false
122123
// Initialize allProjects as empty map to prevent NPE
123124
if (allProjects == null) {
@@ -132,8 +133,11 @@ class Msbuild extends ConventionTask {
132133
resolveProject()
133134
}
134135
} catch (OldProjectFormatException e) {
135-
// Old project format detected - skip parsing
136-
logger.warn("Old-style project format detected. Build will proceed using Mono's MSBuild.")
136+
// Old project format detected or dotnet not available - skip parsing
137+
def msbuildType = OperatingSystem.current().windows ? "Windows MSBuild" : "Mono's MSBuild"
138+
def reason = e.message?.contains("dotnet command not available") ?
139+
"dotnet command not available" : "old-style project format detected"
140+
logger.warn("${reason}. Project file parsing will be skipped. Build will proceed using ${msbuildType}.")
137141
parseProject = false
138142
if (allProjects == null) {
139143
allProjects = [:]
@@ -153,8 +157,9 @@ class Msbuild extends ConventionTask {
153157
combinedError.contains('invalidprojectfileexception') ||
154158
combinedError.contains('microsoft.build.exceptions') ||
155159
(combinedError.contains('failed to parse project') && combinedError.contains('exit code: 255'))) {
160+
def msbuildType = OperatingSystem.current().windows ? "Windows MSBuild" : "Mono's MSBuild"
156161
logger.warn("Failed to parse project file (old-style project detected, .NET SDK MSBuild cannot parse it). " +
157-
"Build will proceed using Mono's MSBuild.")
162+
"Build will proceed using ${msbuildType}.")
158163
parseProject = false
159164
if (allProjects == null) {
160165
allProjects = [:]
@@ -224,13 +229,38 @@ class Msbuild extends ConventionTask {
224229
def parserDll = new File(tempDir, 'ProjectFileParser.dll')
225230
def parseOutputStream = new ByteArrayOutputStream()
226231
def errorOutputStream = new ByteArrayOutputStream()
227-
def parser = execOps.exec { exec ->
228-
exec.commandLine('dotnet', '--roll-forward', 'Major', parserDll)
229-
exec.args(file.toString(), JsonOutput.toJson(getInitProperties()).replace('"', '\''))
230-
exec.standardOutput = parseOutputStream
231-
exec.errorOutput = errorOutputStream
232-
// We want to be able to print the details of what actually failed, otherwise we won't have this info
233-
exec.ignoreExitValue = true
232+
def parser
233+
try {
234+
parser = execOps.exec { exec ->
235+
exec.commandLine('dotnet', '--roll-forward', 'Major', parserDll)
236+
exec.args(file.toString(), JsonOutput.toJson(getInitProperties()).replace('"', '\''))
237+
exec.standardOutput = parseOutputStream
238+
exec.errorOutput = errorOutputStream
239+
// We want to be able to print the details of what actually failed, otherwise we won't have this info
240+
exec.ignoreExitValue = true
241+
}
242+
} catch (Exception e) {
243+
// Handle process start failures (e.g., dotnet command not found, permission denied, etc.)
244+
def errorMsg = e.message?.toLowerCase() ?: ''
245+
def isCommandNotFound = errorMsg.contains('command') && errorMsg.contains('not found') ||
246+
errorMsg.contains('cannot run program') ||
247+
errorMsg.contains('no such file') ||
248+
errorMsg.contains('executable not found') ||
249+
errorMsg.contains('not recognized') || // Windows: "is not recognized as an internal or external command"
250+
errorMsg.contains('cannot find the file') || // Windows alternative
251+
(errorMsg.contains('system cannot find') && errorMsg.contains('specified')) // Windows error
252+
253+
if (isCommandNotFound) {
254+
logger.warn("dotnet command not found or cannot be executed. " +
255+
"Project file parsing will be skipped. " +
256+
"Make sure dotnet SDK is installed and available in PATH. " +
257+
"Error: ${e.message}")
258+
throw new OldProjectFormatException("dotnet command not available - skipping project parsing")
259+
} else {
260+
// Re-throw other exceptions (IO errors, etc.)
261+
logger.error("Failed to execute ProjectFileParser: ${e.message}")
262+
throw new GradleException("Failed to execute ProjectFileParser: ${e.message}", e)
263+
}
234264
}
235265
if (parser.exitValue != 0) {
236266
def errorOutput = errorOutputStream.toString()

0 commit comments

Comments
 (0)