@@ -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