diff --git a/src/main/kotlin/com/beust/kobalt/internal/remote/GetDependenciesCommand.kt b/src/main/kotlin/com/beust/kobalt/internal/remote/GetDependenciesCommand.kt index 81ab63b6..9c6aaef4 100644 --- a/src/main/kotlin/com/beust/kobalt/internal/remote/GetDependenciesCommand.kt +++ b/src/main/kotlin/com/beust/kobalt/internal/remote/GetDependenciesCommand.kt @@ -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) : CommandData { + val projectDatas = arrayListOf() val executor = executors.miscExecutor - val projects = arrayListOf() 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) = 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 } diff --git a/src/main/kotlin/com/beust/kobalt/kotlin/BuildFileCompiler.kt b/src/main/kotlin/com/beust/kobalt/kotlin/BuildFileCompiler.kt index 5f2b5ad2..be5b2698 100644 --- a/src/main/kotlin/com/beust/kobalt/kotlin/BuildFileCompiler.kt +++ b/src/main/kotlin/com/beust/kobalt/kotlin/BuildFileCompiler.kt @@ -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, @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, pluginInfo: PluginInfo) : BuildFileCompiler } - val observable = PublishSubject.create() + val observable = PublishSubject.create>() 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) { - val seen = hashSetOf() - projects.forEach { - if (seen.contains(it.name)) { - throw KobaltException("Duplicate project name: $it") - } else { - seen.add(it.name) - } - } - } - private fun findProjects(context: KobaltContext): List { val result = arrayListOf() 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) { 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> { - val result = arrayListOf() - + 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()) - - // - // 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 = 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) - - /** - * Run the given preBuildScript (or buildScript) jar file, using a classloader made of the passed URL's. - */ - private fun runBuildScriptJarFile(buildScriptJarFile: File, urls: List) : BuildScriptInfo { - val projects = arrayListOf() - 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>() - 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().let { topologicalProjects -> - val all = hashSetOf() - 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 } } } diff --git a/src/main/kotlin/com/beust/kobalt/kotlin/internal/BuildScriptUtil.kt b/src/main/kotlin/com/beust/kobalt/kotlin/internal/BuildScriptUtil.kt new file mode 100644 index 00000000..f3b373a5 --- /dev/null +++ b/src/main/kotlin/com/beust/kobalt/kotlin/internal/BuildScriptUtil.kt @@ -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() + + /** + * 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, + context: KobaltContext) : List { + 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>() + 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().let { topologicalProjects -> + val all = hashSetOf() + 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) { + val seen = hashSetOf() + projects.forEach { + if (seen.contains(it.name)) { + throw KobaltException("Duplicate project name: $it") + } else { + seen.add(it.name) + } + } + } +} diff --git a/src/main/kotlin/com/beust/kobalt/kotlin/internal/ParsedBuildFile.kt b/src/main/kotlin/com/beust/kobalt/kotlin/internal/ParsedBuildFile.kt index bc233e96..6eea86f7 100644 --- a/src/main/kotlin/com/beust/kobalt/kotlin/internal/ParsedBuildFile.kt +++ b/src/main/kotlin/com/beust/kobalt/kotlin/internal/ParsedBuildFile.kt @@ -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() +class ParsedBuildFile(val buildFile: BuildFile, val context: KobaltContext, val buildScriptUtil: BuildScriptUtil, + val dependencyManager: DependencyManager, val files: KFiles) { + val pluginList = arrayListOf() val repos = arrayListOf() val profileLines = arrayListOf() + val pluginUrls = arrayListOf() + val projects = arrayListOf() - 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? = 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(), 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 = kotlintDeps.map { it.jarFile.get().absolutePath } + kotlinCompilePrivate { + classpath(files.kobaltJar) + classpath(deps) + sourceFiles(buildFile.path.toFile().absolutePath) + output = File(buildScriptJarFile.absolutePath) + }.compile(context = context) } } diff --git a/src/main/kotlin/com/beust/kobalt/kotlin/internal/VersionFile.kt b/src/main/kotlin/com/beust/kobalt/kotlin/internal/VersionFile.kt new file mode 100644 index 00000000..c9d4aa4f --- /dev/null +++ b/src/main/kotlin/com/beust/kobalt/kotlin/internal/VersionFile.kt @@ -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) + } + } +} \ No newline at end of file