From 80f2b9d1d81bf3c57e777251af6d6a80460e48bd Mon Sep 17 00:00:00 2001 From: Marcus Lagergren <1062473+lagergren@users.noreply.github.com> Date: Wed, 20 Dec 2023 17:30:37 +0100 Subject: [PATCH] Finished porting the Platform to use the XTC plugin. Rewrite some of the documentation --- build.gradle.kts | 75 ++++++++++++++++++++++++++++--------- common/build.gradle.kts | 35 ----------------- gradle/libs.versions.toml | 14 ++++++- host/build.gradle.kts | 30 --------------- kernel/build.gradle.kts | 29 -------------- platformDB/build.gradle.kts | 8 ++-- platformUI/build.gradle.kts | 47 +++++++++++++++++++---- settings.gradle.kts | 14 ++++--- 8 files changed, 123 insertions(+), 129 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 67670aa..5e37a54 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,54 +1,93 @@ -/* +/** * Main build file for the "platform" project. */ +/** + * Plugins required: + * + * Enable the XTC plugin, so that we can parse this build file. In the interest to avoid + * hardcoded artifact descriptors, and copy-and-paste for versions, we refer to the + * plugin aliases declared in "gradle/libs.versions.toml" + */ plugins { alias(libs.plugins.xtc) + alias(libs.plugins.tasktree) // for debugging purposes, for example: } +/** + * Dependencies to other projects, configurations and artifacts. + * + * These are the dependencies to other projects, and to the XDK proper (versioned). We follow + * the Gradle Version Catalog standard for this project, and normally, when changing the version + * of any requested artifact or plugin, there should only be the need to change + * "gradle/libs.versions.toml" + */ dependencies { xdkZip(libs.xdk) - xtcModule(project(":kernel")) + xtcModule(project(":kernel")) // main module to run. xtcModule(project(":common")) // runtime path - xtcModule(project(":platformDB")) // runtime path xtcModule(project(":host")) // runtime path + xtcModule(project(":platformDB")) // runtime path xtcModule(project(":platformUI")) // runtime path } -//x/ec -L lib/ kernel.xtc [password]") +internal val mainModuleName = "kernel" -/* - * xtcRun is a configuration for anything launching from the main source set. - * xtcRunTest +/** + * This is the run configuration, which configures all xtcRun taks for the main source set. (runXtc, runAllXtc) + * The DSL for modules to run is a list of "module { }" elements or a list of moduleName("...") statements. + * To look at the DSL for all parts of the XTC build, you can use your IDE and browse the implementing + * classes. For example, there should be a hint in IntelliJ with the type for the xtcRun element and + * the modules element (DefaultXtcRuntimeExtension and XtcRuntimeExtension.XtcRunModule, respectively). + * It is a good way to understand how the build DSL works, so you can add your own powerful XTC build + * syntax constructs and nice syntactic sugar/shorthand for things you feel should be simpler to write. */ xtcRun { verbose = true module { - moduleName = "kernel" - // TOOD: Check that grabbing properties through -P or -D or System.getenv works and maps to XTC properties. - // TODO: Implement arg() and args() syntactic sugar - args = listOf(readPassword()) + moduleName = mainModuleName + args(readPassword().get()) // TODO: Implement a version of args that takes a provider (Object...) so we can evaluate the password just as we are about to use it. + // methodName = "run" // This is default, and we don't need to specify it. } } val runXtc by tasks.existing { + // Add a dependency on the build to ensure that any subcomponents that have "build" tasks, but may not have them + // due to being proper Gradle lifecycle projects, are built. I am 99% sure we can just remove this. dependsOn(tasks.build) } val run by tasks.registering { group = "application" description = "Build (if necessary) and run the platform (equivalent to 'xec [-L ]+ kernel.xtc )" - dependsOn("runXtc") + dependsOn(tasks.runXtc) doFirst { logger.lifecycle("Starting the XTC platform (kernel).") } } -internal fun readPassword(): String { - val password = findProperty("org.xtclang.platform.password")?.toString() ?: "" +/** + * Read the password. Typically, the password is either placed as a Gradle property + * with the key "org.xtclang.kernel.password" in an external gradle.properties or init + * file outside of the project. The most common choice is $GRADLE_USER_HOME/.gradle.properties, + * which generatally contains secrets. + * + * You can also send values as project properties for the root project by using the + * "-P" switch on the Gradle command line, like so: + * "./gradlew run -Porg.xtclang.kernel.password=Uhlers0th" + * + * Here, we have also added a final way of passing the password property through a Java + * System property, which is equivalent to the above, but would use the "-D" switch instead + * of the "-P" switch, and would make the property value available to any project under + * the same "gradlew" run, just like for a Java program. For example: + * "./gradlew run -Dorg.xtclang.kernel.password=Uhlers0th" + */ +internal fun readPassword(): Provider = provider { + val key = "org.xtclang.${project.name}.password" + val password = findProperty(key)?.toString() ?: System.getProperty(key) ?: "" if (password.isEmpty()) { - throw GradleException("Error. No password was given.") + throw GradleException("Error. No password was found for key: '$key'.") } - logger.lifecycle("Resolved password: [REDACTED]") - return password -} \ No newline at end of file + logger.lifecycle("Successfully resolved password: '$key' -> [REDACTED]") + password +} diff --git a/common/build.gradle.kts b/common/build.gradle.kts index 790fe02..a161289 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -9,38 +9,3 @@ plugins { dependencies { xdkZip(libs.xdk) } - -xtcCompile { - verbose = true -} - -// Rather than forcing the XTC module to show up somewhere, it should be an outgoing configuration or -// an artifact. -/* --val libDir = "${rootProject.projectDir}/lib" -+plugins { - + alias(libs.plugins.xtc) - +} - --tasks.register("build") { - - group = "Build" - - description = "Build this module" - +dependencies { - + xdkZip(libs.xdk) - +} - - - val src = fileTree("${projectDir}/src").files.stream(). - - mapToLong{f -> f.lastModified()}.max().orElse(0) - - val dst = file("$libDir/common.xtc").lastModified() - - - - if (src > dst) { - - val srcModule = "${projectDir}/src/main/x/common.x" - - - - project.exec { - - commandLine("xcc", "-verbose", - - "-o", libDir, - - srcModule) - - } - - } - -} -*/ diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 959ad80..319171d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,3 +1,15 @@ +# +# This is the only file in the project where we keep version information and artifact +# names for our plugins and dependencies. +# +# TODO: Note that there are still a few hardcoded artifact names nad versions in some places in the +# project, mostly on the settings level, due to boostrapping issues. These can be handled better, but +# we haven't had enough time to spend on it + it will also be significantly simpler, once we are up +# and running with artifact repositories for our Gradle artifacts + regularly publish both releases +# (and snapshot releases internally) to gradlePluginPortal, mavenCentral and the XTC Organization +# GitHub artifact repository) +# + [versions] node = "7.0.1" npm = "10.2.0" @@ -10,7 +22,7 @@ node = { id = "com.github.node-gradle.node", version.ref = "node" } xtc = { id = "org.xtclang.xtc-plugin", version.ref = "xdk" } # taskTree is a helper that we can use to view task dependencies -# for example: ./gradlew run taskTree +# for example: ./gradlew run taskTree, or ./gradle run taskTree --with-inputs --with-outputs tasktree = { id = "com.dorongold.task-tree", version.ref = "tasktree" } [libraries] diff --git a/host/build.gradle.kts b/host/build.gradle.kts index 5052fae..80678ce 100644 --- a/host/build.gradle.kts +++ b/host/build.gradle.kts @@ -2,8 +2,6 @@ * Build the host module. */ -// Depends on common project being built, and put its xtc modules on the path. - plugins { alias(libs.plugins.xtc) } @@ -12,31 +10,3 @@ dependencies { xdkZip(libs.xdk) xtcModule(project(":common")) } - -/* -val libDir = "${rootProject.projectDir}/lib" - -tasks.register("build") { - group = "Build" - description = "Build this module" - - dependsOn(project(":common").tasks["build"]) - - doLast { - val src = fileTree("${projectDir}/src").files.stream(). - mapToLong{f -> f.lastModified()}.max().orElse(0) - val dst = file("$libDir/host.xtc").lastModified() - - if (src > dst) { - val srcModule = "${projectDir}/src/main/x/host.x" - - project.exec { - commandLine("xcc", "-verbose", - "-o", libDir, - "-L", libDir, - srcModule) - } - } - } -} -*/ \ No newline at end of file diff --git a/kernel/build.gradle.kts b/kernel/build.gradle.kts index a21960a..4cc8508 100644 --- a/kernel/build.gradle.kts +++ b/kernel/build.gradle.kts @@ -15,32 +15,3 @@ dependencies { xtcCompile { verbose = true } - -/* - -val libDir = "${rootProject.projectDir}/lib" - -tasks.register("build") { - group = "Build" - description = "Build this module" - - dependsOn(project(":common") .tasks["build"]) - dependsOn(project(":platformDB") .tasks["build"]) - - doLast { - val src = fileTree("${projectDir}/src").files.stream(). - mapToLong{f -> f.lastModified()}.max().orElse(0) - val dst = file("$libDir/kernel.xtc").lastModified() - - if (src > dst) { - val srcModule = "${projectDir}/src/main/x/kernel.x" - - project.exec { - commandLine("xcc", "-verbose", - "-o", libDir, - "-L", libDir, - srcModule) - } - } - } -}*/ \ No newline at end of file diff --git a/platformDB/build.gradle.kts b/platformDB/build.gradle.kts index 0dd8cf4..1038d66 100644 --- a/platformDB/build.gradle.kts +++ b/platformDB/build.gradle.kts @@ -1,3 +1,7 @@ +/** + * The platform database subproject. + */ + plugins { alias(libs.plugins.xtc) } @@ -6,7 +10,3 @@ dependencies { xdkZip(libs.xdk) xtcModule(project(":common")) } - -xtcCompile { - verbose = true -} diff --git a/platformUI/build.gradle.kts b/platformUI/build.gradle.kts index 5dc5523..b07f820 100644 --- a/platformUI/build.gradle.kts +++ b/platformUI/build.gradle.kts @@ -1,9 +1,26 @@ +/** + * The platform UI subproject. This relies on npm and yarn for building the web + * UI. We plug the node builder and the quasar runner into the Gradle lifecycle, + * so that we don't need to rebuild the non-XTC parts of the webapp unless something + * has explicitly changed. + * + * We also use the version catalog to resolve the name and version of the popular + * third party Node plugin for Gradle. + * + * This project used to be buildable both with Npm and Yarn, but due to time + * constraints, reimplementing the Npm functionality is in the backlog. The user + * should not need to care anymore, however, because the build system takes care + * of setting up the web app frameworks, and make sure they interact correctly + * with the rest of the Gradle build lifecycle. + */ + import com.github.gradle.node.yarn.task.YarnTask node { version = libs.versions.node.get() npmVersion = libs.versions.npm.get() yarnVersion = libs.versions.yarn.get() + // TODO: Typically you would set download=true here to ensure that we download and use // exactly the versions of the tools above. These would go into the .gradle cache // directory of this subproject, and not anywhere else in the system. However, @@ -28,8 +45,7 @@ dependencies { xtcModule(project(":common")) } -// TODO: Run a build scan and figure out why yarn wants to invalidate dependencies. -// TODO: Implement a parallel NPM / package-lock based approach. Yarn does not like having a package lock in the same build. +// TODO: Future webapp improvement; implement a parallel NPM / package-lock based approach. Yarn does not like having a package lock in the same build. internal val gui = project.file("gui") internal val buildDirs = arrayOf("gui/node_modules", "gui_dist", "gui/.quasar") @@ -37,6 +53,13 @@ val compileXtc by tasks.existing { dependsOn(yarnQuasarBuild) } +/** + * By adding the gui/dist folder as a resource directory, the build will also treat + * it like an input to the build result. This means that any changes of its contents + * or timestamps will require that we rebuild it and its dependencies. This also means + * that as long as it stays unchanged, a finished build task for this project remains + * a no-op. + */ sourceSets.main { xtc { resources { @@ -49,11 +72,16 @@ val clean by tasks.existing { delete(layout.files(*buildDirs)) } +// The yarnSetup task is just referenced here for type safety. We can also refer to it by name below, +// instead of to the variable "yarnSetup", but that is more brittle, and if we rename the task, any references +// to it might accidentally be forgotten. val yarnSetup by tasks.existing val yarnAddQuasar by tasks.registering(YarnTask::class) { dependsOn(yarnSetup) workingDir = gui + // Tag this task as a producer of the "node_modules" directory, implicitly ensuring that any changes + // to the resolved node_modules will make its dependents rebuild properly. outputs.file("node_modules") val isQuasarGlobal = (findProperty("org.xtclang.platform.quasarGlobal")?.toString() ?: "false").toBoolean() args = buildList { @@ -67,7 +95,7 @@ val yarnAddQuasar by tasks.registering(YarnTask::class) { doFirst { logger.lifecycle("Task '$name' installing Quasar (${if (isQuasarGlobal) "globally" else "locally, only for ${rootProject.name}"}.") - printTaskOutputs() + printTaskOutputs(LogLevel.INFO) } } @@ -77,11 +105,16 @@ val yarnQuasarBuild by tasks.registering(YarnTask::class) { outputs.files() args = listOf("--ignore-engines", "quasar", "build") doLast { - printTaskOutputs() + printTaskOutputs(LogLevel.INFO) } } -internal fun Task.printTaskOutputs() { - logger.lifecycle("${project.name} Task '$name' finished.") - outputs.files.asFileTree.forEachIndexed { i, it -> logger.lifecycle("$name: ** '$name' task output $i: $it") } +internal fun Task.printTaskOutputs(level: LogLevel = LogLevel.LIFECYCLE) { + val outputFiles = outputs.files.asFileTree + val n = outputFiles.count() + logger.log(level, "${project.name} Task '$name' finished.") + logger.log(level, "${project.name} Outputs (count: $n):") + outputs.files.asFileTree.forEachIndexed { + i, it -> logger.log(level, "${project.name} '$name' output $i (of $n): $it") + } } diff --git a/settings.gradle.kts b/settings.gradle.kts index ced14a9..b68823a 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,16 +1,20 @@ rootProject.name = "platform" -// Point out the XTC plugin artifact. +/** + * Declare the XTC plugin artifact bootstrap. This has to be resolved before any build script + * using the XTC plugin can be further evaluated. + */ pluginManagement { repositories { + // This "val" declaration will go away, as soon as we have a functional artifact workflow running. val orgXtcLangRepoPlugins: () -> MavenArtifactRepository by settings orgXtcLangRepoPlugins() gradlePluginPortal() } plugins { val xtcVersion: String by settings - id("org.xtclang.xtc-plugin").version(xtcVersion) - id("com.github.node-gradle.node").version("7.0.1") // TODO version from version catalog instead? + id("org.xtclang.xtc-plugin") //version(xtcVersion) + id("com.github.node-gradle.node") } } @@ -19,14 +23,14 @@ pluginManagement { dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { + // This "val" declaration will go away, as soon as we have a functional artifact workflow running. val orgXtcLangRepo: () -> MavenArtifactRepository by settings orgXtcLangRepo() } } +include(":kernel") include(":common") include(":host") include(":platformDB") -include(":kernel") include(":platformUI") -