diff --git a/src/main/kotlin/com/beust/kobalt/internal/build/BuildFile.kt b/src/main/kotlin/com/beust/kobalt/internal/build/BuildFile.kt new file mode 100644 index 00000000..17954ed0 --- /dev/null +++ b/src/main/kotlin/com/beust/kobalt/internal/build/BuildFile.kt @@ -0,0 +1,17 @@ +package com.beust.kobalt.internal.build + +import java.io.File +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.attribute.BasicFileAttributes +/** + * Sometimes, build files are moved to temporary files, so we give them a specific name for clarity. + */ +class BuildFile(val path: Path, val name: String) { + public fun exists() : Boolean = Files.exists(path) + + public val lastModified : Long + get() = Files.readAttributes(path, BasicFileAttributes::class.java).lastModifiedTime().toMillis() + + public val directory : File get() = path.toFile().directory +} diff --git a/src/main/kotlin/com/beust/kobalt/internal/build/BuildFileCompiler.kt b/src/main/kotlin/com/beust/kobalt/internal/build/BuildFileCompiler.kt new file mode 100644 index 00000000..41ce498d --- /dev/null +++ b/src/main/kotlin/com/beust/kobalt/internal/build/BuildFileCompiler.kt @@ -0,0 +1,126 @@ +package com.beust.kobalt.internal.build + +import com.beust.kobalt.Args +import com.beust.kobalt.KobaltException +import com.beust.kobalt.Plugins +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.internal.build.VersionFile +import com.beust.kobalt.maven.DependencyManager +import com.beust.kobalt.misc.KFiles +import com.beust.kobalt.misc.KobaltExecutors +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.net.URL +import java.nio.file.Paths +import javax.inject.Inject + +/** + * Manage the compilation of Build.kt. There are two passes for this processing: + * 1) Extract the repos() and plugins() statements in a separate .kt and compile it into preBuildScript.jar. + * 2) Actually build the whole Build.kt file after adding to the classpath whatever phase 1 found (plugins, repos) + */ +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 buildScriptUtil: BuildScriptUtil) { + + interface IFactory { + fun create(@Assisted("buildFiles") buildFiles: List, pluginInfo: PluginInfo) : BuildFileCompiler + } + + val observable = PublishSubject.create>() + + private val SCRIPT_JAR = "buildScript.jar" + + fun compileBuildFiles(args: Args): List { + // + // Create the KobaltContext + // + val context = KobaltContext(args) + context.pluginInfo = pluginInfo + context.pluginProperties = pluginProperties + context.dependencyManager = dependencyManager + context.executors = executors + Kobalt.context = context + + // + // Find all the projects in the build file, possibly compiling them + // + val allProjects = findProjects(context) + plugins.applyPlugins(context, allProjects) + + return allProjects + } + + private fun findProjects(context: KobaltContext): List { + val result = arrayListOf() + buildFiles.forEach { buildFile -> + 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 (! VersionFile.isSameVersionFile(dir)) { + log(1, "Detected new installation, wiping $dir") + dir.listFiles().map { it.delete() } + } + } + + // 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, processBuildFile.buildScriptCode) + maybeCompileBuildFile(context, BuildFile(Paths.get(modifiedBuildFile.path), "Modified Build.kt"), + buildScriptJarFile, pluginUrls) + val projects = buildScriptUtil.runBuildScriptJarFile(buildScriptJarFile, pluginUrls, context) + result.addAll(projects) + } + return result + } + + private fun maybeCompileBuildFile(context: KobaltContext, buildFile: BuildFile, buildScriptJarFile: File, + pluginUrls: List) { + log(2, "Running build file ${buildFile.name} jar: $buildScriptJarFile") + + if (buildScriptUtil.isUpToDate(buildFile, buildScriptJarFile)) { + log(2, "Build file is up to date") + } else { + log(2, "Need to recompile ${buildFile.name}") + + buildScriptJarFile.delete() + kotlinCompilePrivate { + classpath(files.kobaltJar) + classpath(pluginUrls.map { it.file }) + sourceFiles(listOf(buildFile.path.toFile().absolutePath)) + output = buildScriptJarFile + }.compile(context = context) + + if (! buildScriptJarFile.exists()) { + throw KobaltException("Could not compile ${buildFile.name}") + } + } + } + + /** + * Generate the script file with only the plugins()/repos() directives and run it. Then return + * - 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 parseBuildFile(context: KobaltContext, buildFile: BuildFile) : ParsedBuildFile { + // Parse the build file so we can generate preBuildScript and buildScript from it. + with(ParsedBuildFile(buildFile, context, buildScriptUtil, dependencyManager, files)) { + // Notify possible listeners (e.g. KobaltServer) we now have all the projects + observable.onNext(projects) + return this + } + } +} diff --git a/src/main/kotlin/com/beust/kobalt/internal/build/BuildScriptUtil.kt b/src/main/kotlin/com/beust/kobalt/internal/build/BuildScriptUtil.kt new file mode 100644 index 00000000..7fceb1a9 --- /dev/null +++ b/src/main/kotlin/com/beust/kobalt/internal/build/BuildScriptUtil.kt @@ -0,0 +1,135 @@ +package com.beust.kobalt.internal.build + +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.internal.build.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/internal/build/ParsedBuildFile.kt b/src/main/kotlin/com/beust/kobalt/internal/build/ParsedBuildFile.kt new file mode 100644 index 00000000..7e6df00d --- /dev/null +++ b/src/main/kotlin/com/beust/kobalt/internal/build/ParsedBuildFile.kt @@ -0,0 +1,133 @@ +package com.beust.kobalt.internal.build + +import com.beust.kobalt.Plugins +import com.beust.kobalt.api.KobaltContext +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.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.* +import kotlin.text.Regex + +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.*", + "import com.beust.kobalt.api.*") + val preBuildScriptCode : String get() = preBuildScript.joinToString("\n") + + private val buildScript = arrayListOf() + val buildScriptCode : String get() = buildScript.joinToString("\n") + + init { + parseBuildFile() + initPluginUrls() + } + + private fun parseBuildFile() { + var parenCount = 0 + buildFile.path.toFile().forEachLine(Charset.defaultCharset()) { line -> + var current: ArrayList? = null + var index = line.indexOf("plugins(") + if (index >= 0) { + current = pluginList + } else { + index = line.indexOf("repos(") + if (index >= 0) { + current = repos + } + } + if (parenCount > 0 || current != null) { + if (index == -1) index = 0 + with(line.substring(index)) { + parenCount += line countChar '(' + if (parenCount > 0) { + current!!.add(line) + } + parenCount -= line countChar ')' + } + } + + /** + * If the current line matches one of the profile, turns the declaration into + * val profile = true, otherwise return the same line + */ + fun correctProfileLine(line: String) : String { + context.profiles.forEach { + if (line.matches(Regex("[ \\t]*val[ \\t]+$it[ \\t]+=.*"))) { + with("val $it = true") { + profileLines.add(this) + return this + } + } + } + return line + } + + buildScript.add(correctProfileLine(line)) + } + + repos.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/internal/build/VersionFile.kt b/src/main/kotlin/com/beust/kobalt/internal/build/VersionFile.kt new file mode 100644 index 00000000..4343ed3f --- /dev/null +++ b/src/main/kotlin/com/beust/kobalt/internal/build/VersionFile.kt @@ -0,0 +1,20 @@ +package com.beust.kobalt.internal.build + +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