1
0
Fork 0
mirror of https://github.com/ethauvin/kobalt.git synced 2025-04-26 16:28:12 -07:00

Introducing HostInfo.

First step toward allowing authenticated repos.
This commit is contained in:
Cedric Beust 2015-12-02 02:33:25 -08:00
parent 0323eae868
commit c10acfbaf7
11 changed files with 128 additions and 66 deletions

View file

@ -29,10 +29,30 @@ fun plugins(vararg dependencies : String) {
} }
} }
data class HostInfo(val url: String, var keyUsername: String? = null, var keyPassword: String? = null) {
fun hasAuth() : Boolean {
return (! keyUsername.isNullOrBlank()) && (! keyPassword.isNullOrBlank())
}
}
@Directive @Directive
fun repos(vararg repos : String) { fun repos(vararg repos : String) {
repos.forEach { Kobalt.addRepo(HostInfo(it)) }
}
@Directive
fun repos(vararg repos : HostInfo) {
repos.forEach { Kobalt.addRepo(it) } repos.forEach { Kobalt.addRepo(it) }
} }
class HostConfig(var keyUsername: String? = null, var keyPassword: String? = null)
@Directive
fun authRepo(url: String, init: HostConfig.() -> Unit) : HostInfo {
val r = HostConfig()
r.init()
return HostInfo(url, r.keyUsername, r.keyPassword)
}
@Directive @Directive
fun glob(g: String) : IFileSpec.Glob = IFileSpec.Glob(g) fun glob(g: String) : IFileSpec.Glob = IFileSpec.Glob(g)

View file

@ -67,7 +67,7 @@ private class Main @Inject constructor(
private fun addReposFromContributors(project: Project?) = private fun addReposFromContributors(project: Project?) =
pluginInfo.repoContributors.forEach { pluginInfo.repoContributors.forEach {
it.reposFor(project).forEach { it.reposFor(project).forEach {
Kobalt.addRepo(it.toString()) Kobalt.addRepo(it)
} }
} }

View file

@ -1,7 +1,9 @@
package com.beust.kobalt package com.beust.kobalt
import com.beust.kobalt.maven.*
import com.beust.kobalt.api.IClasspathDependency import com.beust.kobalt.api.IClasspathDependency
import com.beust.kobalt.maven.MavenId
import com.beust.kobalt.maven.RepoFinder
import com.beust.kobalt.maven.SimpleDep
import com.beust.kobalt.maven.dependency.MavenDependency import com.beust.kobalt.maven.dependency.MavenDependency
import com.beust.kobalt.misc.Node import com.beust.kobalt.misc.Node
import com.beust.kobalt.misc.log import com.beust.kobalt.misc.log
@ -29,7 +31,7 @@ class ResolveDependency @Inject constructor(val repoFinder: RepoFinder) {
val repoResult = repoFinder.findCorrectRepo(id) val repoResult = repoFinder.findCorrectRepo(id)
val simpleDep = SimpleDep(MavenId(id)) val simpleDep = SimpleDep(MavenId(id))
val url = repoResult.repoUrl + simpleDep.toJarFile(repoResult) val url = repoResult.repoHostInfo.url + simpleDep.toJarFile(repoResult)
AsciiArt.logBox(listOf(id, url).map { " $it" }, {s -> println(s) }) AsciiArt.logBox(listOf(id, url).map { " $it" }, {s -> println(s) })
display(root.children) display(root.children)

View file

@ -1,6 +1,6 @@
package com.beust.kobalt.api package com.beust.kobalt.api
import java.net.URI import com.beust.kobalt.HostInfo
/** /**
* Plugins that add their own repos. * Plugins that add their own repos.
@ -11,6 +11,6 @@ interface IRepoContributor : IContributor {
* before the build file gets parsed (so we don't have any projects yet) and after the * before the build file gets parsed (so we don't have any projects yet) and after the
* build file has been parsed (then it gets called once for each project discovered). * build file has been parsed (then it gets called once for each project discovered).
*/ */
fun reposFor(project: Project?) : List<URI> fun reposFor(project: Project?) : List<HostInfo>
} }

View file

@ -2,6 +2,7 @@ package com.beust.kobalt.api
import com.beust.kobalt.Args import com.beust.kobalt.Args
import com.beust.kobalt.Plugins import com.beust.kobalt.Plugins
import com.beust.kobalt.HostInfo
import com.beust.kobalt.misc.MainModule import com.beust.kobalt.misc.MainModule
import com.google.inject.Guice import com.google.inject.Guice
import com.google.inject.Injector import com.google.inject.Injector
@ -23,9 +24,11 @@ public class Kobalt {
"https://jcenter.bintray.com/" "https://jcenter.bintray.com/"
) )
val repos = HashSet<String>(DEFAULT_REPOS) val repos = HashSet<HostInfo>(DEFAULT_REPOS.map { HostInfo(it) })
fun addRepo(repo: String) = repos.add(if (repo.endsWith("/")) repo else repo + "/") fun addRepo(repo: HostInfo) = repos.add(
if (repo.url.endsWith("/")) repo
else repo.copy(url = (repo.url + "/")))
private val PROPERTY_KOBALT_VERSION = "kobalt.version" private val PROPERTY_KOBALT_VERSION = "kobalt.version"
private val KOBALT_PROPERTIES = "kobalt.properties" private val KOBALT_PROPERTIES = "kobalt.properties"

View file

@ -3,9 +3,6 @@ package com.beust.kobalt.internal.build
import com.beust.kobalt.Plugins import com.beust.kobalt.Plugins
import com.beust.kobalt.api.KobaltContext import com.beust.kobalt.api.KobaltContext
import com.beust.kobalt.api.Project import com.beust.kobalt.api.Project
import com.beust.kobalt.internal.build.VersionFile
import com.beust.kobalt.internal.build.BuildFile
import com.beust.kobalt.internal.build.BuildScriptUtil
import com.beust.kobalt.maven.DependencyManager import com.beust.kobalt.maven.DependencyManager
import com.beust.kobalt.misc.KFiles import com.beust.kobalt.misc.KFiles
import com.beust.kobalt.misc.countChar import com.beust.kobalt.misc.countChar
@ -41,17 +38,20 @@ class ParsedBuildFile(val buildFile: BuildFile, val context: KobaltContext, val
private fun parseBuildFile() { private fun parseBuildFile() {
var parenCount = 0 var parenCount = 0
var current: ArrayList<String>? = null
buildFile.path.toFile().forEachLine(Charset.defaultCharset()) { line -> buildFile.path.toFile().forEachLine(Charset.defaultCharset()) { line ->
var current: ArrayList<String>? = null
var index = line.indexOf("plugins(") var index = line.indexOf("plugins(")
if (index >= 0) { if (current == null) {
current = pluginList
} else {
index = line.indexOf("repos(")
if (index >= 0) { if (index >= 0) {
current = repos current = pluginList
} else {
index = line.indexOf("repos(")
if (index >= 0) {
current = repos
}
} }
} }
if (parenCount > 0 || current != null) { if (parenCount > 0 || current != null) {
if (index == -1) index = 0 if (index == -1) index = 0
with(line.substring(index)) { with(line.substring(index)) {
@ -63,6 +63,10 @@ class ParsedBuildFile(val buildFile: BuildFile, val context: KobaltContext, val
} }
} }
if (parenCount == 0) {
current = null
}
/** /**
* If the current line matches one of the profile, turns the declaration into * If the current line matches one of the profile, turns the declaration into
* val profile = true, otherwise return the same line * val profile = true, otherwise return the same line

View file

@ -1,5 +1,6 @@
package com.beust.kobalt.maven package com.beust.kobalt.maven
import com.beust.kobalt.HostInfo
import com.beust.kobalt.misc.KFiles import com.beust.kobalt.misc.KFiles
import com.beust.kobalt.misc.log import com.beust.kobalt.misc.log
import com.beust.kobalt.misc.warn import com.beust.kobalt.misc.warn
@ -19,49 +20,50 @@ import javax.inject.Singleton
@Singleton @Singleton
class DownloadManager @Inject constructor(val factory: ArtifactFetcher.IFactory) { class DownloadManager @Inject constructor(val factory: ArtifactFetcher.IFactory) {
class Key(val url: String, val fileName: String, val executor: ExecutorService) { class Key(val hostInfo: HostInfo, val fileName: String, val executor: ExecutorService) {
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
return (other as Key).url == url return (other as Key).hostInfo.url == hostInfo.url
} }
override fun hashCode(): Int { override fun hashCode(): Int {
return url.hashCode() return hostInfo.url.hashCode()
} }
} }
private val CACHE : LoadingCache<Key, Future<File>> = CacheBuilder.newBuilder() private val CACHE : LoadingCache<Key, Future<File>> = CacheBuilder.newBuilder()
.build(object : CacheLoader<Key, Future<File>>() { .build(object : CacheLoader<Key, Future<File>>() {
override fun load(key: Key): Future<File> { override fun load(key: Key): Future<File> {
return key.executor.submit(factory.create(key.url, key.fileName)) return key.executor.submit(factory.create(key.hostInfo, key.fileName))
} }
}) })
fun download(url: String, fileName: String, executor: ExecutorService) fun download(hostInfo: HostInfo, fileName: String, executor: ExecutorService)
: Future<File> = CACHE.get(Key(url, fileName, executor)) : Future<File> = CACHE.get(Key(hostInfo, fileName, executor))
} }
/** /**
* Fetches an artifact (a file in a Maven repo, .jar, -javadoc.jar, ...) to the given local file. * Fetches an artifact (a file in a Maven repo, .jar, -javadoc.jar, ...) to the given local file.
*/ */
class ArtifactFetcher @Inject constructor(@Assisted("url") val url: String, class ArtifactFetcher @Inject constructor(@Assisted("hostInfo") val hostInfo: HostInfo,
@Assisted("fileName") val fileName: String, @Assisted("fileName") val fileName: String,
val files: KFiles, val urlFactory: Kurl.IFactory) : Callable<File> { val files: KFiles) : Callable<File> {
interface IFactory { interface IFactory {
fun create(@Assisted("url") url: String, @Assisted("fileName") fileName: String) : ArtifactFetcher fun create(@Assisted("hostInfo") hostInfo: HostInfo, @Assisted("fileName") fileName: String) : ArtifactFetcher
} }
override fun call() : File { override fun call() : File {
val k = urlFactory.create(url + ".md5") val k = Kurl(hostInfo.copy(url = hostInfo.url + ".md5"))
val remoteMd5 = val remoteMd5 =
if (k.exists) k.string.trim(' ', '\t', '\n').substring(0, 32) if (k.exists) k.string.trim(' ', '\t', '\n').substring(0, 32)
else null else null
val tmpFile = Paths.get(fileName + ".tmp") val tmpFile = Paths.get(fileName + ".tmp")
val file = Paths.get(fileName) val file = Paths.get(fileName)
val url = hostInfo.url
if (! Files.exists(file)) { if (! Files.exists(file)) {
with(tmpFile.toFile()) { with(tmpFile.toFile()) {
parentFile.mkdirs() parentFile.mkdirs()
urlFactory.create(url).toFile(this) Kurl(hostInfo).toFile(this)
} }
log(2, "Done downloading, renaming $tmpFile to $file") log(2, "Done downloading, renaming $tmpFile to $file")
Files.move(tmpFile, file, StandardCopyOption.REPLACE_EXISTING) Files.move(tmpFile, file, StandardCopyOption.REPLACE_EXISTING)

View file

@ -1,30 +1,45 @@
package com.beust.kobalt.maven package com.beust.kobalt.maven
import com.beust.kobalt.HostInfo
import com.beust.kobalt.maven.dependency.FileDependency import com.beust.kobalt.maven.dependency.FileDependency
import com.google.inject.assistedinject.Assisted
import java.io.* import java.io.*
import java.net.HttpURLConnection import java.net.HttpURLConnection
import java.net.URL import java.net.URL
import java.net.URLConnection import java.net.URLConnection
import javax.inject.Inject import java.util.*
/** /**
* Abstracts a URL so that it works transparently on either http:// or file:// * Abstracts a URL so that it works transparently on either http:// or file://
*/ */
class Kurl @Inject constructor(@Assisted val url: String) { class Kurl(val hostInfo: HostInfo) {
val connection : URLConnection by lazy { // constructor(url: String) : this(HostInfo(url))
URL(url).openConnection()
}
interface IFactory { val connection : URLConnection
fun create(url: String) : Kurl get() {
val result = URL(hostInfo.url).openConnection()
if (hostInfo.hasAuth()) {
val userPass = hostInfo.keyUsername + ":" + hostInfo.keyPassword
val basicAuth = "Basic " + String(Base64.getEncoder().encode(userPass.toByteArray()))
result.setRequestProperty("Authorization", basicAuth)
}
return result
}
val inputStream : InputStream by lazy {
connection.inputStream
} }
val exists : Boolean val exists : Boolean
get() { get() {
if (hostInfo.url.contains("localhost")) {
println("DONOTCOMMIT")
}
val url = hostInfo.url
val result = val result =
if (connection is HttpURLConnection) { if (connection is HttpURLConnection) {
(connection as HttpURLConnection).responseCode == 200 val responseCode = (connection as HttpURLConnection).responseCode
checkResponseCode(responseCode)
responseCode == 200
} else if (url.startsWith(FileDependency.PREFIX_FILE)) { } else if (url.startsWith(FileDependency.PREFIX_FILE)) {
val fileName = url.substring(FileDependency.PREFIX_FILE.length) val fileName = url.substring(FileDependency.PREFIX_FILE.length)
File(fileName).exists() File(fileName).exists()
@ -34,11 +49,22 @@ class Kurl @Inject constructor(@Assisted val url: String) {
return result return result
} }
private fun checkResponseCode(responseCode: Int) {
if (responseCode == 401) {
if (hostInfo.hasAuth()) {
error("Bad credentials supplied for ${hostInfo.url}")
} else {
error("This repo requires authentication: ${hostInfo.url}")
}
}
}
/** The Kotlin compiler is about 17M and downloading it with the default buffer size takes forever */ /** The Kotlin compiler is about 17M and downloading it with the default buffer size takes forever */
private val estimatedSize: Int private val estimatedSize: Int
get() = if (url.contains("kotlin-compiler")) 18000000 else 1000000 get() = if (hostInfo.url.contains("kotlin-compiler")) 18000000 else 1000000
fun toOutputStream(os: OutputStream, progress: (Long) -> Unit) = copy(connection.inputStream, os, progress) fun toOutputStream(os: OutputStream, progress: (Long) -> Unit) = copy(inputStream, os, progress)
fun toFile(file: File, progress: (Long) -> Unit = {}) = FileOutputStream(file).use { fun toFile(file: File, progress: (Long) -> Unit = {}) = FileOutputStream(file).use {
toOutputStream(it, progress) toOutputStream(it, progress)
@ -71,14 +97,12 @@ class Kurl @Inject constructor(@Assisted val url: String) {
val string: String val string: String
get() { get() {
val sb = StringBuilder() val sb = StringBuilder()
connection.inputStream.use { inputStream -> val reader = BufferedReader(InputStreamReader(inputStream))
val reader = BufferedReader(InputStreamReader(inputStream))
var line: String? = reader.readLine() var line: String? = reader.readLine()
while (line != null) { while (line != null) {
sb.append(line).append('\n') sb.append(line).append('\n')
line = reader.readLine() line = reader.readLine()
}
} }
return sb.toString() return sb.toString()

View file

@ -1,5 +1,6 @@
package com.beust.kobalt.maven package com.beust.kobalt.maven
import com.beust.kobalt.HostInfo
import com.beust.kobalt.api.Kobalt import com.beust.kobalt.api.Kobalt
import com.beust.kobalt.misc.KobaltExecutors import com.beust.kobalt.misc.KobaltExecutors
import com.beust.kobalt.misc.Strings import com.beust.kobalt.misc.Strings
@ -8,6 +9,7 @@ import com.beust.kobalt.misc.warn
import com.google.common.cache.CacheBuilder import com.google.common.cache.CacheBuilder
import com.google.common.cache.CacheLoader import com.google.common.cache.CacheLoader
import com.google.common.cache.LoadingCache import com.google.common.cache.LoadingCache
import kotlinx.dom.parseXml
import java.io.File import java.io.File
import java.util.concurrent.Callable import java.util.concurrent.Callable
import java.util.concurrent.ExecutorCompletionService import java.util.concurrent.ExecutorCompletionService
@ -15,19 +17,18 @@ import java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject
import javax.xml.xpath.XPathConstants import javax.xml.xpath.XPathConstants
import javax.xml.xpath.XPathFactory import javax.xml.xpath.XPathFactory
import kotlinx.dom.parseXml
/** /**
* Find the repo that contains the given dependency among a list of repos. Searches are performed in parallel and * Find the repo that contains the given dependency among a list of repos. Searches are performed in parallel and
* cached so we never make a network call for the same dependency more than once. * cached so we never make a network call for the same dependency more than once.
*/ */
public class RepoFinder @Inject constructor(val urlFactory: Kurl.IFactory, val executors: KobaltExecutors) { public class RepoFinder @Inject constructor(val executors: KobaltExecutors) {
public fun findCorrectRepo(id: String): RepoResult { public fun findCorrectRepo(id: String): RepoResult {
return FOUND_REPOS.get(id) return FOUND_REPOS.get(id)
} }
data class RepoResult(val repoUrl: String, val found: Boolean, val version: String, val hasJar: Boolean = true, data class RepoResult(val repoHostInfo: HostInfo, val found: Boolean, val version: String,
val snapshotVersion: String = "") val hasJar: Boolean = true, val snapshotVersion: String = "")
private val FOUND_REPOS: LoadingCache<String, RepoResult> = CacheBuilder.newBuilder() private val FOUND_REPOS: LoadingCache<String, RepoResult> = CacheBuilder.newBuilder()
.build(object : CacheLoader<String, RepoResult>() { .build(object : CacheLoader<String, RepoResult>() {
@ -50,14 +51,14 @@ public class RepoFinder @Inject constructor(val urlFactory: Kurl.IFactory, val e
val result = cs.take().get(2000, TimeUnit.MILLISECONDS) val result = cs.take().get(2000, TimeUnit.MILLISECONDS)
log(2, "Result for repo #$i: $result") log(2, "Result for repo #$i: $result")
if (result.found) { if (result.found) {
log(2, "Located $id in ${result.repoUrl}") log(2, "Located $id in ${result.repoHostInfo.url}")
return result return result
} }
} catch(ex: Exception) { } catch(ex: Exception) {
warn("Error: $ex") warn("Error: $ex")
} }
} }
return RepoResult("", false, id) return RepoResult(HostInfo(""), false, id)
} finally { } finally {
executor.shutdownNow() executor.shutdownNow()
} }
@ -66,8 +67,9 @@ public class RepoFinder @Inject constructor(val urlFactory: Kurl.IFactory, val e
/** /**
* Execute a single HTTP request to one repo. * Execute a single HTTP request to one repo.
*/ */
inner class RepoFinderCallable(val id: String, val repoUrl: String) : Callable<RepoResult> { inner class RepoFinderCallable(val id: String, val repo: HostInfo) : Callable<RepoResult> {
override fun call(): RepoResult { override fun call(): RepoResult {
val repoUrl = repo.url
log(2, "Checking $repoUrl for $id") log(2, "Checking $repoUrl for $id")
val mavenId = MavenId(id) val mavenId = MavenId(id)
@ -78,9 +80,9 @@ public class RepoFinder @Inject constructor(val urlFactory: Kurl.IFactory, val e
val ud = UnversionedDep(groupId, artifactId) val ud = UnversionedDep(groupId, artifactId)
val foundVersion = findCorrectVersionRelease(ud.toMetadataXmlPath(false), repoUrl) val foundVersion = findCorrectVersionRelease(ud.toMetadataXmlPath(false), repoUrl)
if (foundVersion != null) { if (foundVersion != null) {
return RepoResult(repoUrl, true, foundVersion) return RepoResult(repo, true, foundVersion)
} else { } else {
return RepoResult(repoUrl, false, "") return RepoResult(repo, false, "")
} }
} else { } else {
val version = mavenId.version val version = mavenId.version
@ -88,26 +90,27 @@ public class RepoFinder @Inject constructor(val urlFactory: Kurl.IFactory, val e
val dep = SimpleDep(mavenId) val dep = SimpleDep(mavenId)
val snapshotVersion = findSnapshotVersion(dep.toMetadataXmlPath(false), repoUrl) val snapshotVersion = findSnapshotVersion(dep.toMetadataXmlPath(false), repoUrl)
if (snapshotVersion != null) { if (snapshotVersion != null) {
return RepoResult(repoUrl, true, version, true /* hasJar, potential bug here */, return RepoResult(repo, true, version, true /* hasJar, potential bug here */,
snapshotVersion) snapshotVersion)
} else { } else {
return RepoResult(repoUrl, false, "") return RepoResult(repo, false, "")
} }
} else { } else {
val dep = SimpleDep(mavenId) val dep = SimpleDep(mavenId)
// Try to find the jar file // Try to find the jar file
val urlJar = repoUrl + dep.toJarFile(dep.version) val urlJar = repo.copy(url = repo.url + dep.toJarFile(dep.version))
val hasJar = urlFactory.create(urlJar).exists val hasJar = Kurl(urlJar).exists
val found = val found =
if (! hasJar) { if (! hasJar) {
// No jar, try to find the directory // No jar, try to find the directory
val url = repoUrl + File(dep.toJarFile(dep.version)).parentFile.path.replace("\\", "/") val url = repo.copy(url = repoUrl
urlFactory.create(url).exists + File(dep.toJarFile(dep.version)).parentFile.path.replace("\\", "/"))
Kurl(url).exists
} else { } else {
true true
} }
log(2, "Result for $repoUrl for $id: $found") log(2, "Result for $repoUrl for $id: $found")
return RepoResult(repoUrl, found, dep.version, hasJar) return RepoResult(repo, found, dep.version, hasJar)
} }
} }
} }

View file

@ -1,5 +1,6 @@
package com.beust.kobalt.maven.dependency package com.beust.kobalt.maven.dependency
import com.beust.kobalt.HostInfo
import com.beust.kobalt.KobaltException import com.beust.kobalt.KobaltException
import com.beust.kobalt.api.IClasspathDependency import com.beust.kobalt.api.IClasspathDependency
import com.beust.kobalt.api.Kobalt import com.beust.kobalt.api.Kobalt
@ -36,12 +37,13 @@ public class MavenDependency @Inject constructor(mavenId: MavenId,
if (repoResult.found) { if (repoResult.found) {
jarFile = jarFile =
if (repoResult.hasJar) { if (repoResult.hasJar) {
downloadManager.download(repoResult.repoUrl + toJarFile(repoResult), jar.absolutePath, executor) downloadManager.download(HostInfo(url = repoResult.repoHostInfo.url + toJarFile(repoResult)),
jar.absolutePath, executor)
} else { } else {
CompletedFuture(File("nonexistentFile")) // will be filtered out CompletedFuture(File("nonexistentFile")) // will be filtered out
} }
pomFile = downloadManager.download(repoResult.repoUrl + toPomFile(repoResult), pom.absolutePath, pomFile = downloadManager.download(HostInfo(url = repoResult.repoHostInfo.url + toPomFile(repoResult)),
executor) pom.absolutePath, executor)
} else { } else {
throw KobaltException("Couldn't resolve ${mavenId.toId}") throw KobaltException("Couldn't resolve ${mavenId.toId}")
} }

View file

@ -3,7 +3,10 @@ package com.beust.kobalt.misc
import com.beust.kobalt.Args import com.beust.kobalt.Args
import com.beust.kobalt.internal.PluginInfo import com.beust.kobalt.internal.PluginInfo
import com.beust.kobalt.internal.build.BuildFileCompiler import com.beust.kobalt.internal.build.BuildFileCompiler
import com.beust.kobalt.maven.* import com.beust.kobalt.maven.ArtifactFetcher
import com.beust.kobalt.maven.LocalRepo
import com.beust.kobalt.maven.Pom
import com.beust.kobalt.maven.PomGenerator
import com.beust.kobalt.plugin.publish.JCenterApi import com.beust.kobalt.plugin.publish.JCenterApi
import com.google.inject.* import com.google.inject.*
import com.google.inject.assistedinject.FactoryModuleBuilder import com.google.inject.assistedinject.FactoryModuleBuilder
@ -35,7 +38,6 @@ public open class MainModule(val args: Args) : AbstractModule() {
JCenterApi.IFactory::class.java, JCenterApi.IFactory::class.java,
Pom.IFactory::class.java, Pom.IFactory::class.java,
BuildFileCompiler.IFactory::class.java, BuildFileCompiler.IFactory::class.java,
Kurl.IFactory::class.java,
ArtifactFetcher.IFactory::class.java) ArtifactFetcher.IFactory::class.java)
.forEach { .forEach {
install(builder.build(it)) install(builder.build(it))