Skip to content

Commit 02a970b

Browse files
committed
Add build artifact collection and archive() method for easy artifact access
- Automatically collect build artifacts (DLL, PDB, XML) after MSBuild completes - Scan output directories even when project parsing is skipped - Add archive(projectName, type) method for simple artifact access - Works with old .NET Framework projects where parsing is disabled - Provides clean API: msbuild.archive('Project', 'dll') in artifacts block
1 parent f23a0e3 commit 02a970b

File tree

1 file changed

+183
-0
lines changed

1 file changed

+183
-0
lines changed

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

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ class Msbuild extends ConventionTask {
6868
IExecutableResolver resolver
6969
@Internal
7070
Boolean parseProject = true
71+
@Internal
72+
Map<String, Map<String, File>> buildArtifacts = [:]
7173
@Inject
7274
ExecOperations getExecOps() {}
7375

@@ -398,6 +400,187 @@ class Msbuild extends ConventionTask {
398400
exec.commandLine(commandLineArgs)
399401
exec.workingDir(project.projectDir)
400402
}
403+
404+
// Collect build artifacts after build completes
405+
collectBuildArtifacts()
406+
}
407+
408+
/**
409+
* Collects build artifacts (DLL, PDB, XML files) from the build output directories.
410+
* This works even when project parsing is skipped for old .NET Framework projects.
411+
*
412+
* @return Map of project names to their artifacts (dll, pdb, xml files)
413+
*/
414+
@Internal
415+
Map<String, Map<String, File>> getArtifacts() {
416+
if (buildArtifacts.isEmpty()) {
417+
collectBuildArtifacts()
418+
}
419+
return buildArtifacts
420+
}
421+
422+
/**
423+
* Collects build artifacts by scanning output directories.
424+
* Artifacts are organized by project name (derived from DLL name or directory structure).
425+
*/
426+
private void collectBuildArtifacts() {
427+
buildArtifacts.clear()
428+
429+
// Determine output directories to search
430+
def outputDirs = []
431+
432+
// Add destinationDir if specified
433+
if (destinationDir != null) {
434+
def destDir = project.file(destinationDir)
435+
if (destDir.exists()) {
436+
outputDirs.add(destDir)
437+
}
438+
}
439+
440+
// Add standard MSBuild output locations
441+
def config = configuration ?: 'Release'
442+
if (isSolutionBuild()) {
443+
// For solutions, check each project directory
444+
def solutionFile = getRootedSolutionFile()
445+
if (solutionFile?.exists()) {
446+
// Try to find project directories from solution file
447+
try {
448+
def solutionContent = solutionFile.text
449+
def projectPattern = ~/Project\([^)]+\)\s*=\s*"[^"]+",\s*"([^"]+\.csproj)"/
450+
def matcher = projectPattern.matcher(solutionContent)
451+
while (matcher.find()) {
452+
def projectRelativePath = matcher.group(1).replace('\\', File.separator)
453+
def projectFile = new File(solutionFile.parentFile, projectRelativePath)
454+
if (projectFile.exists()) {
455+
def projectDir = projectFile.parentFile
456+
// Check standard output paths
457+
outputDirs.add(new File(projectDir, "bin/${config}"))
458+
outputDirs.add(new File(projectDir, "bin/${config}/signed"))
459+
}
460+
}
461+
} catch (Exception e) {
462+
logger.debug("Could not parse solution file for project directories: ${e.message}")
463+
}
464+
}
465+
} else if (isProjectBuild()) {
466+
// For single project builds
467+
def projectFile = getRootedProjectFile()
468+
if (projectFile?.exists()) {
469+
def projectDir = projectFile.parentFile
470+
outputDirs.add(new File(projectDir, "bin/${config}"))
471+
outputDirs.add(new File(projectDir, "bin/${config}/signed"))
472+
}
473+
}
474+
475+
// Also check root build directories
476+
outputDirs.add(new File(project.projectDir, "bin/${config}"))
477+
outputDirs.add(new File(project.projectDir, "build/${config}"))
478+
479+
// Remove duplicates and non-existent directories
480+
outputDirs = outputDirs.findAll { it.exists() }.unique()
481+
482+
logger.debug("Scanning for build artifacts in: ${outputDirs.collect { it.absolutePath }}")
483+
484+
// Scan for DLL files and collect associated PDB and XML files
485+
outputDirs.each { dir ->
486+
def dllFiles = []
487+
dir.eachFileRecurse { file ->
488+
if (file.name.endsWith('.dll') && !file.path.contains('obj')) {
489+
dllFiles.add(file)
490+
}
491+
}
492+
493+
dllFiles.each { dllFile ->
494+
// Derive project name from DLL name (remove .dll, handle signed variants)
495+
def projectName = dllFile.name.replaceAll(/\.(Signed)?\.dll$/, '').replaceAll(/^inetsoftware\./, '')
496+
497+
// If we have parsed projects, try to match by assembly name
498+
if (allProjects && !allProjects.isEmpty()) {
499+
def matched = allProjects.find { name, proj ->
500+
def assemblyName = proj.properties?.AssemblyName ?: name
501+
dllFile.name.contains(assemblyName) || assemblyName.contains(projectName)
502+
}
503+
if (matched) {
504+
projectName = matched.key
505+
}
506+
}
507+
508+
def artifacts = [:]
509+
artifacts['dll'] = dllFile
510+
511+
// Find associated PDB file
512+
def pdbFile = new File(dllFile.parentFile, dllFile.name.replace('.dll', '.pdb'))
513+
if (pdbFile.exists()) {
514+
artifacts['pdb'] = pdbFile
515+
}
516+
517+
// Find associated XML documentation file
518+
def xmlFile = new File(dllFile.parentFile, dllFile.name.replace('.dll', '.xml'))
519+
if (xmlFile.exists()) {
520+
artifacts['xml'] = xmlFile
521+
}
522+
523+
// Store artifacts (allow multiple projects with same name by using full path as key if needed)
524+
if (buildArtifacts.containsKey(projectName)) {
525+
// If project name already exists, use a more specific key
526+
projectName = "${projectName}_${dllFile.parentFile.name}"
527+
}
528+
buildArtifacts[projectName] = artifacts
529+
530+
logger.debug("Found artifacts for project '${projectName}': ${artifacts.keySet()}")
531+
}
532+
}
533+
534+
logger.info("Collected build artifacts for ${buildArtifacts.size()} project(s): ${buildArtifacts.keySet()}")
535+
}
536+
537+
/**
538+
* Gets a build artifact file for use in the artifacts block.
539+
*
540+
* Usage:
541+
* <pre>
542+
* artifacts {
543+
* archives msbuild.archive('Reporting', 'dll')
544+
* archives msbuild.archive('Reporting', 'pdb')
545+
* archives msbuild.archive('Reporting', 'xml')
546+
* }
547+
* </pre>
548+
*
549+
* @param projectName The name of the project (e.g., 'Reporting', 'Tests')
550+
* @param type The artifact type: 'dll', 'pdb', or 'xml' (default: 'dll')
551+
* @return The artifact File
552+
* @throws GradleException if the artifact is not found
553+
*/
554+
File archive(String projectName, String type = 'dll') {
555+
def artifacts = getArtifacts()
556+
557+
// Try exact match first
558+
def projectArtifacts = artifacts[projectName]
559+
560+
// Try case-insensitive match
561+
if (!projectArtifacts) {
562+
def match = artifacts.find { key, value ->
563+
key.equalsIgnoreCase(projectName) ||
564+
key.toLowerCase().contains(projectName.toLowerCase()) ||
565+
projectName.toLowerCase().contains(key.toLowerCase())
566+
}
567+
if (match) {
568+
projectArtifacts = match.value
569+
}
570+
}
571+
572+
if (!projectArtifacts) {
573+
throw new GradleException("Project '${projectName}' not found in build artifacts. " +
574+
"Available projects: ${artifacts.keySet()}")
575+
}
576+
577+
def file = projectArtifacts[type]
578+
if (!file) {
579+
throw new GradleException("Artifact type '${type}' not found for project '${projectName}'. " +
580+
"Available types: ${projectArtifacts.keySet()}")
581+
}
582+
583+
return file
401584
}
402585

403586
@Internal

0 commit comments

Comments
 (0)