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

Refactoring of BuildScript.

This commit is contained in:
Cedric Beust 2015-11-27 09:12:40 -08:00
parent 947c620e56
commit 43ccceb71e
5 changed files with 246 additions and 198 deletions

View file

@ -1,6 +1,7 @@
package com.beust.kobalt.internal.remote
import com.beust.kobalt.Args
import com.beust.kobalt.api.Project
import com.beust.kobalt.internal.PluginInfo
import com.beust.kobalt.kotlin.BuildFile
import com.beust.kobalt.kotlin.BuildFileCompiler
@ -28,16 +29,16 @@ class GetDependenciesCommand @Inject constructor(val executors: KobaltExecutors,
val buildFile = BuildFile(Paths.get(received.get("buildFile").asString), "GetDependenciesCommand")
val scriptCompiler = buildFileCompilerFactory.create(listOf(buildFile), pluginInfo)
scriptCompiler.observable.subscribe {
buildScriptInfo -> if (buildScriptInfo.projects.size > 0) {
sender.sendData(toData(buildScriptInfo))
projects -> if (projects.size > 0) {
sender.sendData(toData(projects))
}
}
scriptCompiler.compileBuildFiles(args)
}
private fun toData(info: BuildFileCompiler.BuildScriptInfo) : CommandData {
private fun toData(projects: List<Project>) : CommandData {
val projectDatas = arrayListOf<ProjectData>()
val executor = executors.miscExecutor
val projects = arrayListOf<ProjectData>()
fun toDependencyData(d: IClasspathDependency, scope: String) : DependencyData {
val dep = MavenDependency.create(d.id, executor)
@ -46,7 +47,7 @@ class GetDependenciesCommand @Inject constructor(val executors: KobaltExecutors,
fun allDeps(l: List<IClasspathDependency>) = dependencyManager.transitiveClosure(l)
info.projects.forEach { project ->
projects.forEach { project ->
val allDependencies =
allDeps(project.compileDependencies).map { toDependencyData(it, "compile") } +
allDeps(project.compileProvidedDependencies).map { toDependencyData(it, "provided") } +
@ -54,10 +55,10 @@ class GetDependenciesCommand @Inject constructor(val executors: KobaltExecutors,
allDeps(project.testDependencies).map { toDependencyData(it, "testCompile") } +
allDeps(project.testProvidedDependencies).map { toDependencyData(it, "testProvided") }
projects.add(ProjectData(project.name, allDependencies))
projectDatas.add(ProjectData(project.name, allDependencies))
}
log(1, "Returning BuildScriptInfo")
val result = toCommandData(Gson().toJson(GetDependenciesData(projects)))
val result = toCommandData(Gson().toJson(GetDependenciesData(projectDatas)))
log(2, " $result")
return result
}

View file

@ -3,28 +3,24 @@ package com.beust.kobalt.kotlin
import com.beust.kobalt.Args
import com.beust.kobalt.KobaltException
import com.beust.kobalt.Plugins
import com.beust.kobalt.api.*
import com.beust.kobalt.api.annotation.Task
import com.beust.kobalt.api.Kobalt
import com.beust.kobalt.api.KobaltContext
import com.beust.kobalt.api.PluginProperties
import com.beust.kobalt.api.Project
import com.beust.kobalt.internal.PluginInfo
import com.beust.kobalt.kotlin.internal.BuildScriptUtil
import com.beust.kobalt.kotlin.internal.ParsedBuildFile
import com.beust.kobalt.kotlin.internal.VersionFile
import com.beust.kobalt.maven.DependencyManager
import com.beust.kobalt.misc.KFiles
import com.beust.kobalt.misc.KobaltExecutors
import com.beust.kobalt.misc.Topological
import com.beust.kobalt.misc.log
import com.beust.kobalt.plugin.kotlin.kotlinCompilePrivate
import com.google.inject.assistedinject.Assisted
import rx.subjects.PublishSubject
import java.io.File
import java.io.FileInputStream
import java.io.InputStream
import java.lang.reflect.Modifier
import java.net.URL
import java.net.URLClassLoader
import java.nio.charset.Charset
import java.nio.file.Paths
import java.util.*
import java.util.jar.JarInputStream
import javax.inject.Inject
/**
@ -35,13 +31,13 @@ import javax.inject.Inject
public class BuildFileCompiler @Inject constructor(@Assisted("buildFiles") val buildFiles: List<BuildFile>,
@Assisted val pluginInfo: PluginInfo, val files: KFiles, val plugins: Plugins,
val dependencyManager: DependencyManager, val pluginProperties: PluginProperties,
val executors: KobaltExecutors) {
val executors: KobaltExecutors, val buildScriptUtil: BuildScriptUtil) {
interface IFactory {
fun create(@Assisted("buildFiles") buildFiles: List<BuildFile>, pluginInfo: PluginInfo) : BuildFileCompiler
}
val observable = PublishSubject.create<BuildScriptInfo>()
val observable = PublishSubject.create<List<Project>>()
private val SCRIPT_JAR = "buildScript.jar"
@ -65,31 +61,17 @@ public class BuildFileCompiler @Inject constructor(@Assisted("buildFiles") val b
return allProjects
}
/**
* Make sure all the projects have a unique name.
*/
private fun validateProjects(projects: List<Project>) {
val seen = hashSetOf<String>()
projects.forEach {
if (seen.contains(it.name)) {
throw KobaltException("Duplicate project name: $it")
} else {
seen.add(it.name)
}
}
}
private fun findProjects(context: KobaltContext): List<Project> {
val result = arrayListOf<Project>()
buildFiles.forEach { buildFile ->
val pair = processBuildFile(context, buildFile)
val pluginUrls = pair.second
val processBuildFile = parseBuildFile(context, buildFile)
val pluginUrls = processBuildFile.pluginUrls
val buildScriptJarFile = File(KFiles.findBuildScriptLocation(buildFile, SCRIPT_JAR))
// If the script jar files were generated by a different version, wipe them in case the API
// changed in-between
buildScriptJarFile.parentFile.let { dir ->
if (! isSameVersionFile(dir)) {
if (! VersionFile.isSameVersionFile(dir)) {
log(1, "Detected new installation, wiping $dir")
dir.listFiles().map { it.delete() }
}
@ -98,24 +80,20 @@ public class BuildFileCompiler @Inject constructor(@Assisted("buildFiles") val b
// Write the modified Build.kt (e.g. maybe profiles were applied) to a temporary file,
// compile it, jar it in buildScript.jar and run it
val modifiedBuildFile = KFiles.createTempFile(".kt")
KFiles.saveFile(modifiedBuildFile, pair.first.buildScriptCode)
KFiles.saveFile(modifiedBuildFile, processBuildFile.buildScriptCode)
maybeCompileBuildFile(context, BuildFile(Paths.get(modifiedBuildFile.path), "Modified Build.kt"),
buildScriptJarFile, pluginUrls)
val buildScriptInfo = runBuildScriptJarFile(buildScriptJarFile, pluginUrls)
result.addAll(buildScriptInfo.projects)
val projects = buildScriptUtil.runBuildScriptJarFile(buildScriptJarFile, pluginUrls, context)
result.addAll(projects)
}
return result
}
private fun isUpToDate(buildFile: BuildFile, jarFile: File) =
buildFile.exists() && jarFile.exists()
&& buildFile.lastModified < jarFile.lastModified()
private fun maybeCompileBuildFile(context: KobaltContext, buildFile: BuildFile, buildScriptJarFile: File,
pluginUrls: List<URL>) {
log(2, "Running build file ${buildFile.name} jar: $buildScriptJarFile")
if (isUpToDate(buildFile, buildScriptJarFile)) {
if (buildScriptUtil.isUpToDate(buildFile, buildScriptJarFile)) {
log(2, "Build file is up to date")
} else {
log(2, "Need to recompile ${buildFile.name}")
@ -139,156 +117,12 @@ public class BuildFileCompiler @Inject constructor(@Assisted("buildFiles") val b
* - the source code for the modified Build.kt (after profiles are applied)
* - the URL's of all the plug-ins that were found.
*/
private fun processBuildFile(context: KobaltContext, buildFile: BuildFile): Pair<ParsedBuildFile, List<URL>> {
val result = arrayListOf<URL>()
private fun parseBuildFile(context: KobaltContext, buildFile: BuildFile) : ParsedBuildFile {
// Parse the build file so we can generate preBuildScript and buildScript from it.
val parsedBuildFile = ParsedBuildFile(buildFile.path.toFile(), context)
//
// Compile and run preBuildScriptCode, which contains all the plugins() calls extracted. This
// will add all the dynamic plugins found in this code to Plugins.dynamicPlugins
//
val pluginSourceFile = KFiles.createTempFile(".kt")
pluginSourceFile.writeText(parsedBuildFile.preBuildScriptCode, Charset.defaultCharset())
log(2, "Saved ${pluginSourceFile.absolutePath}")
//
// Compile to preBuildScript.jar
//
val buildScriptJar = KFiles.findBuildScriptLocation(buildFile, "preBuildScript.jar")
val buildScriptJarFile = File(buildScriptJar)
if (! isUpToDate(buildFile, File(buildScriptJar))) {
buildScriptJarFile.parentFile.mkdirs()
generateJarFile(context, BuildFile(Paths.get(pluginSourceFile.path), "Plugins"), buildScriptJarFile)
generateVersionFile(buildScriptJarFile.parentFile)
}
//
// Run preBuildScript.jar to initialize plugins and repos
//
runBuildScriptJarFile(buildScriptJarFile, arrayListOf<URL>())
//
// All the plug-ins are now in Plugins.dynamicPlugins, download them if they're not already
//
Plugins.dynamicPlugins.forEach {
result.add(it.jarFile.get().toURI().toURL())
}
return Pair(parsedBuildFile, result)
}
private val VERSION_FILE = "version.txt"
private fun generateVersionFile(directory: File) {
KFiles.saveFile(File(directory, VERSION_FILE), Kobalt.version)
}
private fun isSameVersionFile(directory: File) =
with(File(directory, VERSION_FILE)) {
! exists() || (exists() && readText() == Kobalt.version)
}
private fun generateJarFile(context: KobaltContext, buildFile: BuildFile, buildScriptJarFile: File) {
val kotlintDeps = dependencyManager.calculateDependencies(null, context)
val deps: List<String> = kotlintDeps.map { it.jarFile.get().absolutePath }
kotlinCompilePrivate {
classpath(files.kobaltJar)
classpath(deps)
sourceFiles(buildFile.path.toFile().absolutePath)
output = File(buildScriptJarFile.absolutePath)
}.compile(context = context)
}
class BuildScriptInfo(val projects: List<Project>)
/**
* Run the given preBuildScript (or buildScript) jar file, using a classloader made of the passed URL's.
*/
private fun runBuildScriptJarFile(buildScriptJarFile: File, urls: List<URL>) : BuildScriptInfo {
val projects = arrayListOf<Project>()
var stream : InputStream? = null
val allUrls = (urls + arrayOf(
buildScriptJarFile.toURI().toURL()) + File(files.kobaltJar).toURI().toURL())
.toTypedArray()
val classLoader = URLClassLoader(allUrls)
//
// Install all the plugins
//
plugins.installPlugins(Plugins.dynamicPlugins, classLoader)
try {
stream = JarInputStream(FileInputStream(buildScriptJarFile))
var entry = stream.nextJarEntry
val classes = hashSetOf<Class<*>>()
while (entry != null) {
val name = entry.name;
if (name.endsWith(".class")) {
val className = name.substring(0, name.length - 6).replace("/", ".")
var cl : Class<*>? = classLoader.loadClass(className)
if (cl != null) {
classes.add(cl)
} else {
throw KobaltException("Couldn't instantiate $className")
}
}
entry = stream.nextJarEntry;
}
// Invoke all the "val" found on the _DefaultPackage class (the Build.kt file)
classes.filter { cls ->
cls.name != "_DefaultPackage"
}.forEach { cls ->
cls.methods.forEach { method ->
// Invoke vals and see if they return a Project
if (method.name.startsWith("get") && Modifier.isStatic(method.modifiers)) {
try {
val r = method.invoke(null)
if (r is Project) {
log(2, "Found project $r in class $cls")
projects.add(r)
}
} catch(ex: Throwable) {
throw ex.cause ?: KobaltException(ex)
}
} else {
val taskAnnotation = method.getAnnotation(Task::class.java)
if (taskAnnotation != null) {
Plugins.defaultPlugin.methodTasks.add(IPlugin.MethodTask(method, taskAnnotation))
}
}}
}
} finally {
stream?.close()
}
validateProjects(projects)
//
// Now that the build file has run, fetch all the project contributors, grab the projects from them and sort
// them topologically
//
Topological<Project>().let { topologicalProjects ->
val all = hashSetOf<Project>()
pluginInfo.projectContributors.forEach { contributor ->
val descriptions = contributor.projects()
descriptions.forEach { pd ->
all.add(pd.project)
pd.dependsOn.forEach { dependsOn ->
topologicalProjects.addEdge(pd.project, dependsOn)
all.add(dependsOn)
}
}
}
val result = BuildScriptInfo(topologicalProjects.sort(ArrayList(all)))
with(ParsedBuildFile(buildFile, context, buildScriptUtil, dependencyManager, files)) {
// Notify possible listeners (e.g. KobaltServer) we now have all the projects
observable.onNext(result)
return result
observable.onNext(projects)
return this
}
}
}

View file

@ -0,0 +1,135 @@
package com.beust.kobalt.kotlin.internal
import com.beust.kobalt.KobaltException
import com.beust.kobalt.Plugins
import com.beust.kobalt.api.IPlugin
import com.beust.kobalt.api.KobaltContext
import com.beust.kobalt.api.Project
import com.beust.kobalt.api.annotation.Task
import com.beust.kobalt.kotlin.BuildFile
import com.beust.kobalt.misc.KFiles
import com.beust.kobalt.misc.Topological
import com.beust.kobalt.misc.log
import com.google.inject.Inject
import java.io.File
import java.io.FileInputStream
import java.io.InputStream
import java.lang.reflect.Modifier
import java.net.URL
import java.net.URLClassLoader
import java.util.*
import java.util.jar.JarInputStream
class BuildScriptUtil @Inject constructor(val plugins: Plugins, val files: KFiles){
val projects = arrayListOf<Project>()
/**
* Run the given preBuildScript (or buildScript) jar file, using a classloader made of the passed URL's.
* This list is empty when we run preBuildScript.jar but for buildScript.jar, it contains the list of
* URL's found from running preBuildScript.jar.
*/
fun runBuildScriptJarFile(buildScriptJarFile: File, urls: List<URL>,
context: KobaltContext) : List<Project> {
var stream : InputStream? = null
val allUrls = (urls + arrayOf(
buildScriptJarFile.toURI().toURL()) + File(files.kobaltJar).toURI().toURL())
.toTypedArray()
val classLoader = URLClassLoader(allUrls)
//
// Install all the plugins
//
plugins.installPlugins(Plugins.dynamicPlugins, classLoader)
//
// Classload all the jar files and invoke their methods
//
try {
stream = JarInputStream(FileInputStream(buildScriptJarFile))
var entry = stream.nextJarEntry
val classes = hashSetOf<Class<*>>()
while (entry != null) {
val name = entry.name;
if (name.endsWith(".class")) {
val className = name.substring(0, name.length - 6).replace("/", ".")
var cl : Class<*>? = classLoader.loadClass(className)
if (cl != null) {
classes.add(cl)
} else {
throw KobaltException("Couldn't instantiate $className")
}
}
entry = stream.nextJarEntry;
}
// Invoke all the "val" found on the _DefaultPackage class (the Build.kt file)
classes.filter { cls ->
cls.name != "_DefaultPackage"
}.forEach { cls ->
cls.methods.forEach { method ->
// Invoke vals and see if they return a Project
if (method.name.startsWith("get") && Modifier.isStatic(method.modifiers)) {
try {
val r = method.invoke(null)
if (r is Project) {
log(2, "Found project $r in class $cls")
projects.add(r)
}
} catch(ex: Throwable) {
throw ex.cause ?: KobaltException(ex)
}
} else {
val taskAnnotation = method.getAnnotation(Task::class.java)
if (taskAnnotation != null) {
Plugins.defaultPlugin.methodTasks.add(IPlugin.MethodTask(method, taskAnnotation))
}
}}
}
} finally {
stream?.close()
}
validateProjects(projects)
//
// Now that the build file has run, fetch all the project contributors, grab the projects from them and sort
// them topologically
//
Topological<Project>().let { topologicalProjects ->
val all = hashSetOf<Project>()
context.pluginInfo.projectContributors.forEach { contributor ->
val descriptions = contributor.projects()
descriptions.forEach { pd ->
all.add(pd.project)
pd.dependsOn.forEach { dependsOn ->
topologicalProjects.addEdge(pd.project, dependsOn)
all.add(dependsOn)
}
}
}
val result = topologicalProjects.sort(ArrayList(all))
return result
}
}
fun isUpToDate(buildFile: BuildFile, jarFile: File) =
buildFile.exists() && jarFile.exists()
&& buildFile.lastModified < jarFile.lastModified()
/**
* Make sure all the projects have a unique name.
*/
private fun validateProjects(projects: List<Project>) {
val seen = hashSetOf<String>()
projects.forEach {
if (seen.contains(it.name)) {
throw KobaltException("Duplicate project name: $it")
} else {
seen.add(it.name)
}
}
}
}

View file

@ -1,17 +1,30 @@
package com.beust.kobalt.kotlin.internal
import com.beust.kobalt.Plugins
import com.beust.kobalt.api.KobaltContext
import com.beust.kobalt.api.Project
import com.beust.kobalt.kotlin.BuildFile
import com.beust.kobalt.maven.DependencyManager
import com.beust.kobalt.misc.KFiles
import com.beust.kobalt.misc.countChar
import com.beust.kobalt.misc.log
import com.beust.kobalt.plugin.kotlin.kotlinCompilePrivate
import java.io.File
import java.net.URL
import java.nio.charset.Charset
import java.nio.file.Paths
import java.util.*
class ParsedBuildFile(val file: File, val context: KobaltContext) {
val plugins = arrayListOf<String>()
class ParsedBuildFile(val buildFile: BuildFile, val context: KobaltContext, val buildScriptUtil: BuildScriptUtil,
val dependencyManager: DependencyManager, val files: KFiles) {
val pluginList = arrayListOf<String>()
val repos = arrayListOf<String>()
val profileLines = arrayListOf<String>()
val pluginUrls = arrayListOf<URL>()
val projects = arrayListOf<Project>()
private val preBuildScript = arrayListOf("import com.beust.kobalt.*",
private val preBuildScript = arrayListOf(
"import com.beust.kobalt.*",
"import com.beust.kobalt.api.*")
val preBuildScriptCode : String get() = preBuildScript.joinToString("\n")
@ -20,15 +33,16 @@ class ParsedBuildFile(val file: File, val context: KobaltContext) {
init {
parseBuildFile()
initPluginUrls()
}
private fun parseBuildFile() {
var parenCount = 0
file.forEachLine(Charset.defaultCharset()) { line ->
buildFile.path.toFile().forEachLine(Charset.defaultCharset()) { line ->
var current: ArrayList<String>? = null
var index = line.indexOf("plugins(")
if (index >= 0) {
current = plugins
current = pluginList
} else {
index = line.indexOf("repos(")
if (index >= 0) {
@ -66,7 +80,51 @@ class ParsedBuildFile(val file: File, val context: KobaltContext) {
}
repos.forEach { preBuildScript.add(it) }
plugins.forEach { preBuildScript.add(it) }
pluginList.forEach { preBuildScript.add(it) }
}
private fun initPluginUrls() {
//
// Compile and run preBuildScriptCode, which contains all the plugins() calls extracted. This
// will add all the dynamic plugins found in this code to Plugins.dynamicPlugins
//
val pluginSourceFile = KFiles.createTempFile(".kt")
pluginSourceFile.writeText(preBuildScriptCode, Charset.defaultCharset())
log(2, "Saved ${pluginSourceFile.absolutePath}")
//
// Compile to preBuildScript.jar
//
val buildScriptJar = KFiles.findBuildScriptLocation(buildFile, "preBuildScript.jar")
val buildScriptJarFile = File(buildScriptJar)
if (! buildScriptUtil.isUpToDate(buildFile, File(buildScriptJar))) {
buildScriptJarFile.parentFile.mkdirs()
generateJarFile(context, BuildFile(Paths.get(pluginSourceFile.path), "Plugins"), buildScriptJarFile)
VersionFile.generateVersionFile(buildScriptJarFile.parentFile)
}
//
// Run preBuildScript.jar to initialize plugins and repos
//
projects.addAll(buildScriptUtil.runBuildScriptJarFile(buildScriptJarFile, arrayListOf<URL>(), context))
//
// All the plug-ins are now in Plugins.dynamicPlugins, download them if they're not already
//
Plugins.dynamicPlugins.forEach {
pluginUrls.add(it.jarFile.get().toURI().toURL())
}
}
private fun generateJarFile(context: KobaltContext, buildFile: BuildFile, buildScriptJarFile: File) {
val kotlintDeps = dependencyManager.calculateDependencies(null, context)
val deps: List<String> = kotlintDeps.map { it.jarFile.get().absolutePath }
kotlinCompilePrivate {
classpath(files.kobaltJar)
classpath(deps)
sourceFiles(buildFile.path.toFile().absolutePath)
output = File(buildScriptJarFile.absolutePath)
}.compile(context = context)
}
}

View file

@ -0,0 +1,20 @@
package com.beust.kobalt.kotlin.internal
import com.beust.kobalt.api.Kobalt
import com.beust.kobalt.misc.KFiles
import java.io.File
class VersionFile {
companion object {
private val VERSION_FILE = "version.txt"
fun generateVersionFile(directory: File) {
KFiles.saveFile(File(directory, VERSION_FILE), Kobalt.version)
}
fun isSameVersionFile(directory: File) =
with(File(directory, VERSION_FILE)) {
! exists() || (exists() && readText() == Kobalt.version)
}
}
}