Skip to content

Commit

Permalink
Add auto discovery build steps for projects in sln file
Browse files Browse the repository at this point in the history
  • Loading branch information
dtretyakov committed Dec 13, 2016
1 parent 6f36853 commit eaaaa70
Show file tree
Hide file tree
Showing 5 changed files with 122 additions and 38 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class DotnetConstants {
val CONFIG_PATH = CONFIG_NAME + "_Path"
val PROJECT_JSON = "project.json"
val PROJECT_CSPROJ = ".csproj"
val PROJECT_SLN = ".sln"

val COMMAND_BUILD = "build"
val COMMAND_PACK = "pack"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class DotnetModelParser {

private val myGson: Gson
private val myXmlMapper: ObjectMapper
private val ProjectPathPattern: Regex = Regex(""""([^\"]+\${DotnetConstants.PROJECT_CSPROJ})"""")

init {
val builder = GsonBuilder()
Expand Down Expand Up @@ -64,7 +65,9 @@ class DotnetModelParser {
try {
val inputStream = getInputStreamReader(element.inputStream)
BufferedReader(inputStream).use {
return myXmlMapper.readValue(it, CsProject::class.java)
val project = myXmlMapper.readValue(it, CsProject::class.java)
project.path = element.fullName
return project
}
} catch (e: Exception) {
val message = "Failed to retrieve file for given path ${element.fullName}: $e"
Expand All @@ -74,6 +77,38 @@ class DotnetModelParser {
return null
}

fun getCsProjectModels(element: Element): List<CsProject>? {
if (!element.isContentAvailable) {
return null
}

val projectPaths = arrayListOf<String>()
try {
val inputStream = getInputStreamReader(element.inputStream)
BufferedReader(inputStream).use {
inputStream.readLines().forEach {
ProjectPathPattern.find(it)?.let {
projectPaths.add(it.groupValues[1].replace('\\', '/'))
}
}
}
} catch (e: Exception) {
val message = "Failed to retrieve file for given path ${element.fullName}: $e"
Loggers.SERVER.infoAndDebugDetails(message, e)
}

return projectPaths.map { getElement(element, it) }
.filterNotNull()
.map { getCsProjectModel(it) }
.filterNotNull()
}

private fun getElement(element: Element, it: String): Element? {
val fullName = element.fullName
val parent = fullName.substring(0, fullName.length - element.name.length)
return element.browser.getElement("$parent$it")
}

private fun getInputStreamReader(inputStream: InputStream): Reader {
inputStream.mark(3)
val byte1 = inputStream.read()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,34 +18,43 @@ import java.util.*
*/
class DotnetRunnerCsprojDiscoveryExtension(private val myModelParser: DotnetModelParser) : BreadthFirstRunnerDiscoveryExtension(3) {

private val MsBuildVersion: List<String> = listOf("15.0")
private val TestPackages: Regex = Regex("xunit|nunit|test")

override fun discoverRunnersInDirectory(dir: Element,
filesAndDirs: List<Element>): List<DiscoveredObject> {
val result = ArrayList<DiscoveredObject>()
for (item in filesAndDirs) {
if (item.isLeaf && item.name.endsWith(DotnetConstants.PROJECT_CSPROJ) && item.isContentAvailable) {
val project = myModelParser.getCsProjectModel(item) ?: continue

var fullName = item.fullName
if (fullName.contains(" ")) {
fullName = "\"$fullName\""
}
if (!item.isLeaf || !item.isContentAvailable) continue

if (item.name.endsWith(DotnetConstants.PROJECT_CSPROJ)) {
val project = myModelParser.getCsProjectModel(item) ?: continue
val fullName = getEscapedPath(item.fullName)
val steps = discover(project, fullName)
result.addAll(steps.map { DiscoveredObject(DotnetConstants.RUNNER_TYPE, it) })
}

if (item.name.endsWith(DotnetConstants.PROJECT_SLN)) {
val projects = myModelParser.getCsProjectModels(item) ?: continue
val fullName = getEscapedPath(item.fullName)
val steps = discover(projects, fullName)
result.addAll(steps.map { DiscoveredObject(DotnetConstants.RUNNER_TYPE, it) })
}
}

return result
}

private fun discover(project: CsProject, fullName: String): ArrayList<Map<String, String>> {
if (project.toolsVersion.isNullOrEmpty() || !MsBuildVersion.contains(project.toolsVersion)) {
return arrayListOf()
private fun getEscapedPath(name: String): String {
if (name.contains(" ")) {
return "\"$name\""
}
return name
}

/**
* Discover steps for standalone project
*/
private fun discover(project: CsProject, fullName: String): List<Map<String, String>> {
val steps = arrayListOf<Map<String, String>>()
project.itemGroups?.let {
val packages = it.fold(hashSetOf<String>(), {
Expand All @@ -56,34 +65,71 @@ class DotnetRunnerCsprojDiscoveryExtension(private val myModelParser: DotnetMode
all
})

// Restore nuget packages
if (packages.size > 0) {
steps.add(mapOf(
Pair(DotnetConstants.PARAM_COMMAND, DotnetConstants.COMMAND_RESTORE),
Pair(DotnetConstants.PARAM_PATHS, fullName)))
}
steps.addAll(restorePackages(fullName, packages))
steps.addAll(getBuildSteps(fullName, packages))
}

// Check whether project contains test framework packages
if (packages.any { TestPackages.matches(it) }) {
steps.add(mapOf(
Pair(DotnetConstants.PARAM_COMMAND, DotnetConstants.COMMAND_TEST),
Pair(DotnetConstants.PARAM_PATHS, fullName)))
}
return steps
}

/**
* Discover steps for projects in solution.
*/
private fun discover(projects: List<CsProject>, fullName: String): List<Map<String, String>> {
val steps = arrayListOf<Map<String, String>>()
val packages = hashSetOf<String>()

// Check whether project contains web application packages
if (packages.any { it.startsWith("Microsoft.AspNet") }) {
steps.add(mapOf(
Pair(DotnetConstants.PARAM_COMMAND, DotnetConstants.COMMAND_PUBLISH),
Pair(DotnetConstants.PARAM_PATHS, fullName)))
for (project in projects) {
project.itemGroups?.let {
val projectPackages = it.filterNotNull()
.map { it.packageReferences }
.filterNotNull()
.flatMap {
it.map { it.include }.filterNotNull()
}.toSet()

val projectPath = getEscapedPath(project.path!!)
steps.addAll(getBuildSteps(projectPath, projectPackages))
packages.addAll(projectPackages)
}
}

if (steps.size == 0) {
Collections.sort(steps, { s1, s2 ->
val command1 = s1[DotnetConstants.PARAM_COMMAND]!!
val command2 = s2[DotnetConstants.PARAM_COMMAND]!!
command2.compareTo(command1)
})

steps.addAll(0, restorePackages(fullName, packages))

return steps
}

private fun getBuildSteps(fullName: String, packages: Set<String>): List<Map<String, String>> {
val steps = arrayListOf<Map<String, String>>()

// Check whether project contains test framework packages
if (packages.any { TestPackages.matches(it) }) {
steps.add(mapOf(
Pair(DotnetConstants.PARAM_COMMAND, DotnetConstants.COMMAND_BUILD),
Pair(DotnetConstants.PARAM_COMMAND, DotnetConstants.COMMAND_TEST),
Pair(DotnetConstants.PARAM_PATHS, fullName)))
}

// Check whether project contains web application packages
if (packages.any { it.startsWith("Microsoft.AspNet") }) {
steps.add(mapOf(
Pair(DotnetConstants.PARAM_COMMAND, DotnetConstants.COMMAND_PUBLISH),
Pair(DotnetConstants.PARAM_PATHS, fullName)))
}

return steps
}

private fun restorePackages(fullName: String, packages: Set<String>): List<Map<String, String>> {
if (packages.isEmpty()) return emptyList()

return listOf(mapOf(
Pair(DotnetConstants.PARAM_COMMAND, DotnetConstants.COMMAND_RESTORE),
Pair(DotnetConstants.PARAM_PATHS, fullName)))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,13 @@ import com.fasterxml.jackson.dataformat.xml.annotation.*
*/
@JacksonXmlRootElement(localName = "Project", namespace = "http://schemas.microsoft.com/developer/msbuild/2003")
data class CsProject(
@get:JacksonXmlProperty(localName = "ToolsVersion", isAttribute = true)
var toolsVersion: String? = null,

@get:[JacksonXmlProperty(localName = "PropertyGroup") JacksonXmlElementWrapper(useWrapping = false)]
var propertyGroups: List<CsPropertyGroup>? = null,

@get:[JacksonXmlProperty(localName = "ItemGroup") JacksonXmlElementWrapper(useWrapping = false)]
var itemGroups: List<CsItemGroup>? = null
var itemGroups: List<CsItemGroup>? = null,

var path: String? = null
)

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ class DotnetModelParserTest {
val m = Mockery()
val element = m.mock(Element::class.java)
val parser = DotnetModelParser()
val csproj = File("src/test/resources/project.csproj")
val fullName = "src/test/resources/project.csproj"
val csproj = File(fullName)

m.checking(object : Expectations() {
init {
Expand All @@ -38,13 +39,15 @@ class DotnetModelParserTest {

one(element).inputStream
will(returnValue(BufferedInputStream(FileInputStream(csproj))))

one(element).fullName
will(returnValue(fullName))
}
})

val project = parser.getCsProjectModel(element)

Assert.assertNotNull(project)
Assert.assertEquals(project!!.toolsVersion, "15.0")
Assert.assertEquals(project!!.path, fullName)

Assert.assertNotNull(project.propertyGroups)
project.propertyGroups?.let {
Expand Down

0 comments on commit eaaaa70

Please sign in to comment.