Skip to content

Commit 00f4f19

Browse files
committed
Fix reflection to properly call subclass exec() method
- Fixed method lookup to walk up class hierarchy to find exec() method - Use direct reflection invocation instead of metaClass to avoid interception issues - Added debug logging to track solutionFile property and exec() method calls - Improved path normalization in NuGetRestore.exec() for cross-platform compatibility
1 parent f653e49 commit 00f4f19

File tree

2 files changed

+104
-24
lines changed

2 files changed

+104
-24
lines changed

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

Lines changed: 86 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -117,54 +117,118 @@ class BaseNuGet extends Exec {
117117
// This ensures solution files, packages.config, etc. are added to args
118118
// The dummy exec() method above allows super.exec() calls to succeed
119119

120-
// Call the subclass's exec() method using metaClass to bypass BaseNuGet.exec()
121-
// This ensures we call the actual overridden method in the subclass
120+
// Get args BEFORE calling subclass exec() to see what's already there
121+
def argsBeforeExec = getArgs().toList()
122+
project.logger.info("Args BEFORE calling subclass exec(): ${argsBeforeExec}")
123+
124+
// Check if solutionFile is set before calling exec()
122125
try {
123-
def execMethod = this.class.declaredMethods.find { it.name == 'exec' && it.declaringClass != Exec.class }
124-
if (execMethod != null) {
125-
// Use metaClass to invoke the actual subclass method, not BaseNuGet.exec()
126-
this.metaClass.invokeMethod(this, 'exec', null)
126+
if (this.hasProperty('solutionFile')) {
127+
project.logger.info("solutionFile property exists: ${this.solutionFile}")
128+
} else {
129+
project.logger.warn("solutionFile property does NOT exist on task")
127130
}
128131
} catch (Exception e) {
129-
// Subclass exec() call failed, continue with execution
132+
project.logger.debug("Could not check solutionFile property: ${e.message}")
130133
}
131134

132-
// Manually add solution file or packages.config if this is a NuGetRestore task
133-
// This is a workaround because args set in subclass exec() might not be preserved
135+
// Call the subclass's exec() method using metaClass to bypass BaseNuGet.exec()
136+
// This ensures we call the actual overridden method in the subclass
134137
try {
135-
if (this.hasProperty('solutionFile')) {
136-
def solutionFile = this.solutionFile
137-
if (solutionFile) {
138-
args project.file(solutionFile)
139-
}
138+
// Check if this class (or any parent up to BaseNuGet) has an exec() method
139+
def execMethod = null
140+
def currentClass = this.class
141+
while (currentClass != null && currentClass != BaseNuGet.class && currentClass != Exec.class) {
142+
execMethod = currentClass.declaredMethods.find { it.name == 'exec' && it.parameterCount == 0 }
143+
if (execMethod != null) break
144+
currentClass = currentClass.superclass
140145
}
141-
if (this.hasProperty('packagesConfigFile')) {
142-
def packagesConfigFile = this.packagesConfigFile
143-
if (packagesConfigFile) {
144-
args project.file(packagesConfigFile)
145-
}
146+
147+
if (execMethod != null) {
148+
project.logger.info("Calling subclass exec() method: ${execMethod.declaringClass.simpleName}.exec()")
149+
// Directly invoke the method using reflection to bypass Gradle's method interception
150+
execMethod.invoke(this)
151+
} else {
152+
project.logger.warn("No exec() method found in subclass ${this.class.simpleName}")
146153
}
147154
} catch (Exception e) {
148-
// Could not add solution/packages file, continue with execution
155+
// Subclass exec() call failed, continue with execution
156+
project.logger.warn("Could not call subclass exec(): ${e.class.simpleName}: ${e.message}")
157+
e.printStackTrace()
149158
}
150159

160+
// Get args AFTER calling subclass exec() to see what was added
161+
def argsAfterExec = getArgs().toList()
162+
project.logger.info("Args AFTER calling subclass exec(): ${argsAfterExec}")
163+
151164
// Now configure executable and final args
152165
File localNuget = getNugetExeLocalPath()
153166
project.logger.debug "Using NuGet from path $localNuget.path"
154167

155-
// Get current args (should include solution file, packages.config, etc.)
168+
// Get current args (should include solution file, packages.config, etc. from subclass exec())
156169
def currentArgs = getArgs().toList()
170+
project.logger.info("Args before deduplication: ${currentArgs}")
171+
172+
// Build a set of expected file paths to ensure we only add them once
173+
def expectedPaths = new HashSet()
174+
try {
175+
if (this.hasProperty('solutionFile') && this.solutionFile) {
176+
def solutionFilePath = this.solutionFile instanceof File ? this.solutionFile.absolutePath : project.file(this.solutionFile).absolutePath
177+
expectedPaths.add(solutionFilePath.replace('/', File.separator).toLowerCase())
178+
}
179+
if (this.hasProperty('packagesConfigFile') && this.packagesConfigFile) {
180+
def packagesConfigFilePath = this.packagesConfigFile instanceof File ? this.packagesConfigFile.absolutePath : project.file(this.packagesConfigFile).absolutePath
181+
expectedPaths.add(packagesConfigFilePath.replace('/', File.separator).toLowerCase())
182+
}
183+
} catch (Exception e) {
184+
project.logger.debug("Could not determine expected paths: ${e.message}")
185+
}
186+
187+
// Remove duplicates from args - normalize paths and check for duplicates
188+
// This is critical because the solution file might be added multiple times
189+
def normalizedArgs = []
190+
def seenPaths = new HashSet()
191+
currentArgs.each { arg ->
192+
def normalizedPath = arg instanceof File ? arg.absolutePath.replace('/', File.separator) : arg.toString().replace('/', File.separator)
193+
def normalizedPathLower = normalizedPath.toLowerCase()
194+
195+
// Skip if we've seen this exact path before
196+
if (!seenPaths.contains(normalizedPathLower)) {
197+
seenPaths.add(normalizedPathLower)
198+
// Convert File objects to strings for consistent argument handling
199+
normalizedArgs.add(arg instanceof File ? normalizedPath : arg)
200+
} else {
201+
project.logger.info("Skipping duplicate arg: ${normalizedPath}")
202+
}
203+
}
204+
205+
// DO NOT manually add solution file or packages.config here
206+
// The subclass exec() method should have already added them via args
207+
// Adding them here would cause duplicates
208+
// The deduplication above should handle any duplicates that were added
209+
210+
project.logger.info("Args after deduplication: ${normalizedArgs}")
211+
212+
// Clear all args first to avoid any accumulation issues
213+
setArgs([])
214+
215+
// Set deduplicated args
216+
setArgs(normalizedArgs)
157217

158218
if (isFamily(FAMILY_WINDOWS)) {
159219
executable = localNuget.absolutePath
160220
} else {
161221
executable = "mono"
162222
// Prepend nuget.exe path to args for mono execution
163-
setArgs([localNuget.absolutePath] + currentArgs)
223+
setArgs([localNuget.absolutePath] + normalizedArgs)
164224
}
225+
226+
// Add flags after setting the base args
165227
args '-NonInteractive'
166228
args '-Verbosity', (verbosity ?: getNugetVerbosity())
167229

230+
project.logger.info("Final args before execution: ${getArgs().toList()}")
231+
168232
// Check if we should ignore failures on non-Windows (for Mono xbuild issues)
169233
// Set ignoreExitValue on the Exec task itself before execution
170234
def shouldIgnoreFailures = false

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

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,24 @@ class NuGetRestore extends BaseNuGet {
7979

8080
@Override
8181
void exec() {
82-
if (packagesConfigFile) args packagesConfigFile
83-
if (solutionFile) args solutionFile
82+
project.logger.info("NuGetRestore.exec() called - solutionFile: ${solutionFile}, packagesConfigFile: ${packagesConfigFile}")
83+
84+
// Convert File objects to absolute paths for proper argument handling
85+
// Normalize path separators for Windows compatibility
86+
if (packagesConfigFile) {
87+
def packagesPath = packagesConfigFile instanceof File ? packagesConfigFile.absolutePath : project.file(packagesConfigFile).absolutePath
88+
packagesPath = packagesPath.replace('/', File.separator)
89+
args packagesPath
90+
project.logger.info("Added packagesConfigFile to args: ${packagesPath}")
91+
}
92+
if (solutionFile) {
93+
def solutionPath = solutionFile instanceof File ? solutionFile.absolutePath : project.file(solutionFile).absolutePath
94+
solutionPath = solutionPath.replace('/', File.separator)
95+
args solutionPath
96+
project.logger.info("Added solutionFile to args: ${solutionPath}")
97+
} else {
98+
project.logger.warn("solutionFile is null or empty - cannot add to args")
99+
}
84100

85101
if (!sources.isEmpty()) args '-Source', sources.join(';')
86102
if (noCache) args '-NoCache'

0 commit comments

Comments
 (0)