diff --git a/.env b/.env new file mode 100644 index 0000000..35750e4 --- /dev/null +++ b/.env @@ -0,0 +1,13 @@ +# +# This env file is not the same as the one in docker compose env_file sections. +# This sets a default enviromnent from the build, e.g. with build args. +# The others set environment variables to be used in the container. +# +COMPOSE_PROJECT_NAME=platform + +GITHUB_BRANCH_PLATFORM=${GITHUB_BRANCH:-platform-xtcplugin} + +PLATFORM_HOSTNAME=${PLATFORM_HOSTNAME:-xtc-platform.container.xqiz.it} +PLATFORM_HOSTNAME_DEV=${PLATFORM_HOSTNAME_DEV:-xtc-platform.container-dev.xqiz.it} + +XTC_VERSION=0.4.3 diff --git a/.gitignore b/.gitignore index e1c5f79..fcdeab2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ -# Logs and databases # -###################### +# +# Logs and databases +# *.log *.out *.tmp @@ -7,8 +8,9 @@ *.sql *.sqlite +# # OS generated files # -###################### +# .DS_Store .DS_Store? ._* @@ -17,20 +19,41 @@ ehthumbs.db Thumbs.db -# build directory # -################### +# +# Build directories +# build/ + +# +# Module aggregation directory: +# +# While we can still collect everyting into a common lib folder, as a final build step, +# if we want, the 'standard' Maven style way would be to just build and run the project. +# Every component declaratively states its dependencies to build and/or run, and any +# task using these dependencies gets them lazily resolved, and exactly what it needs, +# nothing more. We will likely implement a distribution config for XTC applications +# like this, of course, so that the old "lib" folder is some kind of versioned +# publishable artifact) +# lib/ -# xdk directory # -################### +# +# XDK directory +# xdk/ -# user-specific project files # -############################### +# +# User-specific project files +# prj/ .idea/ -# Gradle-specific project files # -################################# -.gradle \ No newline at end of file +# +# Gradle caches +# +.gradle + +# +# Local environment fike for docker/docker compose that may contain secrets +# +.env.local diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index ca75cbc..0000000 --- a/Dockerfile +++ /dev/null @@ -1,52 +0,0 @@ -FROM homebrew/brew:4.1.4 - -# install essential tools and libraries -USER root -RUN apt-get update \ - && apt-get upgrade -y \ - && apt-get install -y build-essential git - -# install xvm -RUN brew tap xtclang/xvm \ - && brew install xdk-latest - -# build the latest xvm from source and update the default -RUN git clone https://github.com/xtclang/xvm.git ~/xvm \ - && cd ~/xvm \ - && ./gradlew dist-local - -# install JS tools -ARG NODE_VERSION=18.17.1 -ARG NODE_PACKAGE=node-v$NODE_VERSION-linux-x64 -ARG NODE_HOME=/opt/$NODE_PACKAGE - -ENV NODE_PATH $NODE_HOME/lib/node_modules -ENV PATH $NODE_HOME/bin:$PATH - -RUN curl https://nodejs.org/dist/v$NODE_VERSION/$NODE_PACKAGE.tar.gz | tar -xzC /opt/ \ - && npm install --global yarn - -# build the platform -RUN mkdir -p ~/xqiz.it/platform \ - && keytool -genkeypair \ - -alias platform \ - -keyalg RSA \ - -keysize 2048 \ - -validity 365 \ - -dname "OU=Platform, O=[some.org], C=US" \ - -keystore ~/xqiz.it/platform/certs.p12 \ - -storetype PKCS12 -storepass qwerty \ - && keytool -genseckey \ - -alias cookies \ - -keyalg AES \ - -keysize 256 \ - -keystore ~/xqiz.it/platform/certs.p12 \ - -storetype PKCS12 \ - -storepass qwerty \ - && git clone https://github.com/azzazzel/xtc_platform.git ~/xtc_platform \ - && cd ~/xtc_platform && git checkout quasar_gui \ - && cd ~/xtc_platform/platformUI/gui && npm install \ - && cd ~/xtc_platform && ~/xvm/gradlew build - -WORKDIR /root/xtc_platform -CMD ["xec", "-L", "lib/", "lib/kernel.xtc", "qwerty"] \ No newline at end of file diff --git a/README.md b/README.md index 13ab2d3..79bb62e 100644 --- a/README.md +++ b/README.md @@ -1,101 +1,205 @@ -# Platform as a Service # +FROM gradle-8.6 -This is the public repository for the open source Ecstasy PaaS project, sponsored by [xqiz.it](http://xqiz.it). - -## Status: +# Platform as a Service -This project is being actively developed, but is not yet considered a production-ready release. +This is the public repository for the open source Ecstasy PaaS project, sponsored by [xqiz.it](http://xqiz.it). ## Layout The project is organized as a number of sub-projects, with the important ones to know about being: -* The *common* library (`./common`), contains common interfaces shared across platform modules. - -* The *kernel* library (`./kernel`), contains the boot-strapping functionality. It's responsible for starting system services and introducing them to each other. - -* The *host* library (`./host`), contains the manager for hosted applications. +* The *common* library ([platform/common](./common)), contains common interfaces shared across platform modules. + +* The *kernel* library ([platform/kernel](./kernel)), contains the boot-strapping functionality. It's responsible for + starting system services and introducing them to each other. -* The *platformDB* library (`./platformDB`), contains the platform database. +* The *host* library ([platform/host](./host)), contains the manager for hosted applications. + +* The *platformDB* library ([platform/platformDB](./platformDB)), contains the platform database. + +* The *platformUI* library ([platform/platformUI](./platformUI)), contains the end-points for the platform + web-application. -* The *platformUI* library (`./platformUI`), contains the end-points for the platform web-application. - ## Installation 1. Please follow steps 1-3 of the [XDK Installation](https://github.com/xtclang/xvm#installation). -2. Clone [the platform repository](https://github.com/xtclang/platform) to your local machine. For purposes of this document, we will assume that the project directory is `~/Development/platform/`, but you may use whatever location makes sense for your environment. +2. Clone [the platform repository](https://github.com/xtclang/platform) to your local machine. ## Steps to test the PAAS functionality: -Note that steps 2 and 3 are temporary, and step 3 needs to be re-executed every time after an OS reboot. +### Provide properties named `gitHubUser` and `gitHubPassword` + +The platform expects to be able to read artifacts from the GitHub Maven Package Repository, +and requires credentials to do so. Later we will have this repo retrieve its artifacts from +gradlePluginPortal and mavenCentral. To understand how to set up these credentials, follow +the instructions in the [xtc-template-app](https://github.com/xtclang/xtc-app-template/blob/master/README.md) +and if you want to get more detail on this, take a look at the comment in the +[xtc-template-app settings](https://github.com/xtclang/xtc-app-template/blob/master/settings.gradle.kts) + +Hence, ensure that you have properties named "gitHubUser" and "gitHubToken" set up in your +environments, and that the token has read:package privileges for the GitHub Maven Repo. -1. Create "xqiz.it" subdirectory under the user home directory for the platform persistent data. The subdirectory "platform" will be used to keep the platform operational information and subdirectory "users" for hosted applications. +If you can build and run the [xtc-template-app](https://github.com/xtclang/xtc-app-template/tree/master), and +have that correctly configured, you can skip this step. -2. Create a file "~/xqiz.it/platform/port-forwarding.conf" with the following content: +### Create a local (or containerized) platform environment - rdr pass on lo0 inet proto tcp from any to self port 80 -> 127.0.0.1 port 8080 - rdr pass on lo0 inet proto tcp from any to self port 443 -> 127.0.0.1 port 8090 +Note that steps 1 and 2 are temporary, and step 2 needs to be re-executed every time after an OS reboot. Steps 3-8 need +to be done just once. Or Dockerize and never have to think about this again. The platform should also be distributed +as a container/Dockerfile/docker in the near future, so that you won't have to do any of these manual steps. + +1. Create `xqiz.it` subdirectory under the user home directory for the platform persistent data. The subdirectory " + platform" will be used to keep the platform operational information and subdirectory "users" for hosted applications. + +2. Create a file `~/xqiz.it/platform/port-forwarding.conf` with the following content: + +``` + rdr pass on lo0 inet proto tcp from any to self port 80 -> 127.0.0.1 port 8080 + rdr pass on lo0 inet proto tcp from any to self port 443 -> 127.0.0.1 port 8090 +``` 3. Run the following command to redirect http and https traffic to unprivileged ports: - - sudo pfctl -evf ~/xqiz.it/platform/port-forwarding.conf + +``` + sudo pfctl -evf ~/xqiz.it/platform/port-forwarding.conf +``` 4. Make sure you can ping the local platform address: - - ping xtc-platform.localhost.xqiz.it - - The domain name `xtc-platform.localhost.xqiz.it` should resolve to `127.0.0.1`. This allows the same xqiz.it cloud-hosted platform to be self-hosted on the `localhost` loop-back address, enabling local and disconnected development. - If that address fails to resolve you may need to change the rules on you DNS server. For example, for Verizon routers you would need add an exception entry for "127.0.0.1" to your DNS Server settings: "Exceptions to DNS Rebind Protection" (Advanced - Network Settings - DNS Server) +``` + ping xtc-platform.localhost.xqiz.it +``` + + The domain name `xtc-platform.localhost.xqiz.it` should resolve to `127.0.0.1`. This allows the same xqiz.it + cloud-hosted platform to be self-hosted on the `localhost` loop-back address, enabling local and disconnected + development. + + If that address fails to resolve you may need to change the rules on you DNS server. For example, for Verizon routers + you would need add an exception entry for `127.0.0.1` to your DNS Server settings: "Exceptions to DNS Rebind + Protection" (Advanced - Network Settings - DNS Server) + + TODO: Why not just add an /etc/host entry, or run a dns server in a co-deployed container? + +5. Create a self-signed certificate for the platform web server. For example: + +``` + keytool -genkeypair -alias platform -keyalg RSA -keysize 2048 -validity 365 -dname "OU=Platform, O=[your name], C=US" -keystore ~/xqiz.it/platform/certs.p12 -storetype PKCS12 -storepass [password] +``` + +6. Add a symmetric key to encode the cookies: + +``` + keytool -genseckey -alias cookies -keyalg AES -keysize 256 -keystore ~/xqiz.it/platform/certs.p12 -storetype PKCS12 -storepass [password] +``` + +7. If you want to run with an XDK installation and not just let the plugin sort it out, make sure you + have [xdk-latest](https://github.com/xtclang/xvm#readme) installed. + +8. Make sure you have a Java runtime installed for bootstrapping. It should really be enough with any + old Java, just so that you can run the Gradle wrapper. The Java toolchains support should download the + latest compatible JDK environment for you, to build and run the XTC Platform. + +9. Make sure that when you issue Gradle commands, you do it either through the Gradle wrapper script, or from + inside your IDE, that you are sure knows about your wrapper script. Any IDE in which you import this project + should pick that up and grab the appropriate runtimes and dependencies. + + *It is recommended that you *do not* keep a Gradle executable on your system path to build the project, but + instead use the Gradle wrapper script (or its IDE integration) for every task you want to execute.* + +10.Build and run the server. + + You can provide the password for your keystore as a Gradle property, by adding a line on the form + `keystorePassword=` to the `$GRADLE_USER_HOME/gradle.properties`. This is + the customary way to add secrets outside source control. You can also place it in the environment + variable `ORG_GRADLE_PROJECT_password`, or send it as a Gradle property on the launcher line, like this: + + * Note: The password you choose during the very first run will be used to encrypt the platform key storage. + You will need the same password for all subsequent runs. If you do not provide a password, executing + the launcher through one of the two methods described in this paragraph, will prompt for the password + on `stdin`, which, while it works, is not compatible with scenarios like automatic CI/CD testing. + +Either build and run the server "the traditional way" (requires a local XDK installation) with: + +``` + ./gradlew build + xec --verbose -L ./build/platform/ kernel.xqiz.it [password] +``` + +or with the experimental: -5. Make sure you have the latest [gradle](https://gradle.org/), [node](https://nodejs.org/en), [yarn](https://yarnpkg.com/) and [xdk-latest](https://github.com/xtclang/xvm#readme) installed. If you are using `brew`, you can simply say: - - brew install gradle node yarn +``` + ./gradlew run -PkeystorePassword=` +``` -6. Change your directory to the `./platformUI/gui` directory inside the local git repo installed above. +Note: the ./gradlew run task described in this paragraph is not meant as a production way of running +the platform. However, it can be quite handy to use for debugging poses with the debug = true flag +set in the XTC run configuration. - cd ~/Development/platform/platformUI/gui +12. Open the [locally hosted platform web page](https://xtc-platform.localhost.xqiz.it): -7. Make sure all necessary *node* modules are installed within that directory using the following command: + Note: Using the locally-created (self-signed) certificate from step 5 above, you will receive warnings from the + browser about the unverifiability of the website and its certificate. - npm install +13. Follow the instructions from the [Examples](https://github.com/xtclang/examples) repository to build and "upload" a web application. -8. If you plan to use `quasar` dev environment, please install it globally by the following command: +14. Log into the "Ecstasy Cloud" platform using the pre-defined test user "admin" and the password "password". - npm install -g @quasar/cli - -9. Build the platform services using the gradle command (from within the "platform" directory): +15. Go to the "Modules" panel and install any of the example module (e.g. "welcome.examples.org"). - cd ~/Development/platform/ - gradle clean build +16. Go to the "Application" panel, register a deployment (e.g. "welcome") and "start" it -10. Start the platform using the command (from within the "platform" directory): +17. Click on the URL to launch your application web page. - xec -L lib/ lib/kernel.xtc [password] +18. To stop the server cleanly, from a separate shell or process, run this command: - Note: The password you choose during the very first run will be used to encrypt the platform key storage. You will need the same password for all subsequent runs. +``` + curl -k -b cookies.txt -L -i -w '\n' -X POST https://xtc-platform.localhost.xqiz.it/host/shutdown +``` -11. Open the [locally hosted platform web page](https://xtc-platform.localhost.xqiz.it): +If you do not stop the server cleanly, the next start-up will be much slower, since the databases on the server will +need to be recovered. - https://xtc-platform.localhost.xqiz.it +### Third Part Installation Dependencies - Note: Using the locally-created (self-signed) certificate from step 5 above, you will receive warnings from the browser about the unverifiability of the website and its certificate. +Previously, to build and run, we required that NodeJS, NPM and Yarn were installed, and available in the +environment and PATH on the system where you execute the platform build and/or run. This is problematic +since you may have several different applications that you work on that require different versions of +the software. It's problematic to have to switch between different versions of an external software +installation, and there are even meta-frameworks like NVM to do that, but it adds complexity, and +it's hard to always detect that you are running the right version. -12. Follow the instructions from the [Examples](https://github.com/xtclang/examples) repository to build and "upload" a web application. +It is even more problematic if you have to install or configure the dependent software with root/admin +privileges, since this alters the global state of your development machine, perhaps breaking something +else you need to work on as well. -13. Log into the "Ecstasy Cloud" platform using the pre-defined test user "admin" and the password "password". +Luckily, this is 2024, and it's industry best practice to keep exactly versioned dependencies referenced +and resolvable inside the scope of a project. For those dependencies that still have to live as installed +system executables, we containerize. -14. Go to the "Modules" panel and install any of the example module (e.g. "welcome.examples.org"). +For the Platform, you don't need to install any additional software as long as you have a bootstrap +Java runtime for the Gradle wrapper. The Platform project will make sure that it downloads and uses +the correct and tested versions of its external dependencies. This includes NodeJS and all the other +things required to build the frontend. The build will always resolve and execute a specific version +of an artifact, without any need for configuration. The build will always override any system +installation of its dependencies with its own, of a known and tested version and configuration. -15. Go to the "Application" panel, register a deployment (e.g. "welcome") and "start" it +#### Quasar -16. Click on the URL to launch your application web page. +While the default behavior is to only install external software dependency at the XTC platform project repo +level, and in the system Gradle caches, e.g. under $GRADLE_USER_HOME/..., Quasar may still be installed by +the build as a "global" scope Node application. This should not be necessary for basic use cases, but since +previous XTC Platform repository supported this option, and we strive to preserve exact semantics of a +build or system, even when performing large changes, it is supported by the build, through the property: -17. To stop the server cleanly, from a separate shell run this command: +``` + org.lang.platform.quasarGlobal=[true|false] +``` - curl -k -b cookies.txt -L -i -w '\n' -X POST https://xtc-platform.localhost.xqiz.it/host/shutdown +If the property is not defined, it will default to "false". - If you do not stop the server cleanly, the next start-up will be much slower, since the databases on the server will need to be recovered. +As with all other Gradle properties, the installation mode for Quasar can be declared on the Gradle wrapper +command line, or in a gradle.properties file in the root of the repository, or under your GRADLE_USER_HOME +directory. ## License diff --git a/bin/stop-server.sh b/bin/stop-server.sh new file mode 100755 index 0000000..a44afb0 --- /dev/null +++ b/bin/stop-server.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +curl -k -b cookies.txt -L -i -w '\n' -X POST https://xtc-platform.localhost.xqiz.it/host/shutdown diff --git a/build.gradle.kts b/build.gradle.kts index 017bc6e..6088858 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,37 +1,165 @@ -/* +/** * Main build file for the "platform" project. */ +import org.xtclang.plugin.tasks.XtcCompileTask +import org.xtclang.plugin.tasks.XtcRunTask -group = "platform.xqiz.it" -version = "0.1.0" - -val libDir = "${projectDir}/lib" +/** + * 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. +} -tasks.register("clean") { - group = "Build" - description = "Delete previous build results" - delete(libDir) +/** + * 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 { + xdkDistribution(libs.xdk) + xtcModule(project(":kernel")) // main module to run. + xtcModule(project(":common")) // runtime library path + xtcModule(project(":host")) // runtime library path + xtcModule(project(":platformDB")) // runtime library path + xtcModule(project(":platformUI")) // runtime library path + xtcModule(project(":platformCLI")) // runtime library path } -val build = tasks.register("build") { - group = "Build" - description = "Build all" +/** + * Gather all compiled XTC modules from subprojects into a single location: $rootProject/build/platform. + */ +val commonXtcOutputDir = layout.buildDirectory.dir("platform") + +allprojects { + tasks.withType().configureEach { + //outputs.dir(commonXtcOutputDir) + doLast { + copy { + val compilerOutput = outputs.files.asFileTree + compilerOutput.forEach { + logger.lifecycle("XTC module output: ${it.absolutePath} -> ${commonXtcOutputDir.get().asFile.absolutePath}") + } + from(compilerOutput) + into(commonXtcOutputDir) + } + } + } +} - dependsOn(project(":kernel") .tasks["build"]) - dependsOn(project(":host") .tasks["build"]) - dependsOn(project(":platformDB") .tasks["build"]) - dependsOn(project(":platformUI") .tasks["build"]) - dependsOn(project(":platformCLI").tasks["build"]) +/** + * 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 { + debug = false // Set to true to get the launcher to pause and wait for a debugger to attach. + verbose = true + stdin = System.`in` // Prevent Gradle from eating stdin; make it interactive with the Gradle process that executes the kernel. + module { + moduleName = "kernel" + moduleArg(passwordProvider) + //findProperty("keystorePassword")?.also { + // moduleArg(it.toString()) + //} + } } -tasks.register("run") { - group = "Run" - description = "Run the platform" +/** + * Lazy password resolution provider. + * + * The password must support both: + * + * 1) Entering it on stdin when the platform kernel is getting started. + * 2) Retrieve it from the environment as described below, or through + * similar methods. SUPPORTING THIS USE CASE IS ABSOLUTELY NECESSARY + * FOR AUTOMATIC CI/CD INTEGRATION (e.g. with GitLab/GitHub/TeamCity + * or other industrial strength integration testing frameworks.) + * + * Read the password. Typically, the password is either placed as a Gradle property + * with the key "keystorePassword" in an external gradle.properties or init + * file outside the project repository. The most common choice is + * $GRADLE_USER_HOME/.gradle.properties, which generally 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 -PkeystorePassword=Uhlers0th" + * + * If you do not provide a password, i.e., defining that property from the command line + * or a "*.properties" file, the XTC Platform will ask the user to input the password + * from stdin. The default behavior if this happens from Gradle, is to show stdin from + * the Gradle run process to the user and allow inputs there. (Or from the actual + * execution command line, of course, if you do it manually). + */ + +internal val passwordProvider: Provider = provider { + logger.lifecycle("Resolving password for XTC platform...") + findProperty("keystorePassword")?.toString() ?: "" +} - dependsOn(build) +/** + * Run the XTC Platform. Note that this is a Gradle job, and as such gets is dependencies from the module path + * in the Gradle plugin for all source in the project. It will use a module path precisely including + * the correct dependencies. + * + * PLEASE Read the rest of this comment if you are interested how we can best model the architectural + * support for the XTC Platform, and why. + * + * This is very neat, but of course we don't want to start a Gradle task to run the platform, as the task + * never completes, given the standard operation. Thus, the more kludgy solution if you want to run the + * project is the "classic" use-a-commandline-method. + * + * To derive a working command line, you can execute "./gradlew run --info" and look for "JavaExec" in the log. + * Or you can do "XTC_PLUGIN_VERBOSE_PROPERTY=true ./gradlew run" for less info. + * + * Ongoing XTC Plugin improvements (TODO): + * + * 1) The ability to retrieve a complete self-contained command line from the XTCPlugin launcher tasks instead + * of having to scrape logs. + * + * 2) The ability explicitly ask the plugin for that command line, or at least programmatically represent + * it as part of an output configuration for the task. + * + * 2) Implement an XtcChildProcessLauncher that inherits the XtcLauncher interface. This would use the Java + * process builder to spawn the platform in the background instead of with JavaExec. That would give us + * 2.1) A state where Gradle finishes and exists after the run task (and any cleanups after that), but + * leaves the platform running in the background. + * 2.2) Still custom input and output stream configuration, so the log is not lost, and we get + * interactive mode with the created child process by just "foregrounding" it, when we need to. + * + * Typically, these improvements make sense, as they follow the law of least astonishment for Docker Compose. + * The user would typically do "./gradlew build" (or install/distribution or any other bundling tasks + * that is required in the environment), followed by a "./gradlew up". This starts the platform in the background + * and the Gradle process goes away. To take the server down, we can execute "./gradlew down". + * + * 3) Touch op the existing Dockerfile, add a docker-compose.yaml, and provide the build and run + * semantics with docker-compose. Here we both have the avantage that we don't need to set up + * various things on our local machine, remember to run some "sudo" command very reboot, and so on. + * For the equivalent of ~/xqiz.it, it's trivial to add that very directory as a Docker volume + * in the compose script, or even better create a docker volume, that can be reused, closed, moved, + * suspended, aggregated with the Example apps in one virtual environment, and so on. This will + * be fundamental both for devleoping "examples" and other XTC platform applications, as well as + * the platform itself. + */ +tasks.withType().configureEach { + verbose = true +} - doLast { - println("Please run the platform directly using the following command:") - println(" xec -L lib/ kernel.xtc [password]") +val run by tasks.registering { + group = "application" + description = "Build (if necessary) and run the platform (equivalent to 'xec [-L ]+ kernel.xtc )" + dependsOn(tasks.runXtc) + doFirst { + logger.lifecycle("Starting the XTC platform (kernel).") } -} \ No newline at end of file +} diff --git a/common/build.gradle.kts b/common/build.gradle.kts index d9aa2d8..35a04a6 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -2,23 +2,10 @@ * Build the "common" module. */ -val libDir = "${rootProject.projectDir}/lib" +plugins { + alias(libs.plugins.xtc) +} -tasks.register("build") { - group = "Build" - description = "Build this module" - - 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) - } - } -} \ No newline at end of file +dependencies { + xdkDistribution(libs.xdk) +} diff --git a/common/src/main/x/common/names.x b/common/src/main/x/common/names.x index 9c6ff24..d3d7a15 100644 --- a/common/src/main/x/common/names.x +++ b/common/src/main/x/common/names.x @@ -32,6 +32,6 @@ package names { /** * The platform landing page URI. */ - static String PlatformUri = "xtc-platform.localhost.xqiz.it"; + static String PlatformUri = "xtc-platform2.localhost.xqiz.it"; } diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..679950c --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,77 @@ +# +# Platform independent Docker compose configuration that syncs out a git branch +# (master is default) and/or a tag, and produced distribution installers for it. +# This is equivalent to ./gradlew installDist, where all platform archives, +# including Windows exe files, are built in the container. +# +# The build volume persists, and is rebuilt whenever it is detected that we +# want to build a branch at a change that doesn't correspond to the last build +# state. The cache volume also persiscat ~ts, so significant info is reused. +# + +version: '3.8' + +# +# Set up secrets from the default locations, so that we can do things like +# publications, artifact signing or other Gradle operations, where sensitive +# information is stored outside the repository. +# +secrets: + gradle_properties: + file: ~/.gradle/gradle.properties + +# gradle_cache is placed under /.root +# we also add a platform volume to reuse ~/xqiz.it +volumes: + persistent: + +services: + # TODO: Create a platform that is more "dev container" style, and + platform: + image: ghcr.io/xtclang/xdk-platform:latest + env_file: + - .env.local + build: + context: . + dockerfile: docker/Dockerfile.platform + args: + PLATFORM_PASSWORD: ${PLATFORM_PASSWORD:-password} + hostname: ${PLATFORM_HOSTNAME:-xtc-platform.local.xqiz.it} + ports: + - "8080:8080" + - "8090:8090" + + # + # Container that grabs, updates and persists source code for 'platform' + # from GitHub, and enabled cached builds in a container. + # + # A third option would be to start a dev container where the root directory + # of the platform project just is symlinked, so you can build and test yourself + # in a normal environment but get deployed elsewhere. + # + dev: + image: ghcr.io/xtclang/xdk-platform-dev:latest + depends_on: + - platform + build: + context: . + dockerfile: docker/Dockerfile.platform.dev + args: + #PLATFORM_PASSWORD: ${PLATFORM_PASSWORD:-password} + GITHUB_BRANCH_PLATFORM: $GITHUB_BRANCH_PLATFORM + environment: + DEV_CONTAINER: 1 + secrets: + - gradle_properties + volumes: + - persistent:/persistent + env_file: + - .env.local + hostname: ${PLATFORM_HOSTNAME:-xtc-platform.local-dev.xqiz.it} + #extra_hosts: + # - "xtc-platform.localhost.xqiz.it:127.0.0.1" + # - "xtc-platform.xqiz.it:127.0.0.10" + ports: + - "8080:8080" + - "8090:8090" + entrypoint: ['entrypoint.sh'] diff --git a/docker/Dockerfile.platform b/docker/Dockerfile.platform new file mode 100644 index 0000000..9f7ed71 --- /dev/null +++ b/docker/Dockerfile.platform @@ -0,0 +1,65 @@ +FROM openjdk:21-jdk-slim-bookworm + +ENV LANG C.UTF-8 +ENV LC_ALL C.UTF-8 +ENV DEBIAN_FRONTEND=noninteractive + +ARG XTC_USER=xtc +ARG XTC_USER_HOME=/home/$XTC_USER + +ENV XTC_USER=$XTC_USER +ENV XTC_USER_HOME=$XTC_USER_HOME +ENV XQIZIT_HOME=$XTC_USER_HOME/xqiz.it +ENV PLATFORM_HOME=$XQIZIT_HOME/platform + +RUN apt-get update && apt-get install --no-install-recommends -y \ + iputils-ping jq sudo wget curl git emacs-nox + +COPY docker/scripts/*.sh /usr/local/bin + +RUN useradd -ms /bin/bash $XTC_USER \ + && passwd -d $XTC_USER \ + && passwd -d root \ + && usermod -aG sudo $XTC_USER \ + && echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers \ + && chown -R $XTC_USER:$XTC_USER $XTC_USER_HOME + +USER $XTC_USER + +RUN mkdir -p $PLATFORM_HOME +RUN mkdir -p $XTC_USER_HOME/lib/xdk + +# Copy the local build (todo could also bind mount it) +COPY build/platform/* $XTC_USER_HOME/lib +COPY build/xtc/xdk/lib/* $XTC_USER_HOME/lib/xdk + +# We should probably use a real XDK, + +# From README.md create: a self-signed certificate for the platform web server. For example: +ARG PLATFORM_PASSWORD +ENV PLATFORM_PASSWORD=$PLATFORM_PASSWORD + +RUN keytool \ + -genkeypair \ + -alias platform \ + -keyalg RSA \ + -keysize 2048 \ + -validity 365 \ + -dname "OU=Platform, O=${XTC_USER}, C=US" \ + -keystore ${PLATFORM_HOME}/certs.p12 \ + -storetype PKCS12 \ + -storepass $PLATFORM_PASSWORD + +# From README.md: Add a symmetric key to encode the cookies: +RUN keytool \ + -genseckey \ + -alias cookies \ + -keyalg AES \ + -keysize 256 \ + -keystore ${PLATFORM_HOME}/certs.p12 \ + -storetype PKCS12 \ + -storepass $PLATFORM_PASSWORD + +WORKDIR $XTC_USER_HOME + +ENTRYPOINT ["entrypoint.sh"] diff --git a/docker/Dockerfile.platform.dev b/docker/Dockerfile.platform.dev new file mode 100644 index 0000000..a111543 --- /dev/null +++ b/docker/Dockerfile.platform.dev @@ -0,0 +1,7 @@ +FROM ghcr.io/xtclang/xdk-platform:latest as platform + +ARG GITHUB_BRANCH_PLATFORM +ENV GITHUB_BRANCH_PLATFORM=$GITHUB_BRANCH_PLATFORM + +USER root + diff --git a/docker/cert/README.md b/docker/cert/README.md new file mode 100644 index 0000000..8a8df26 --- /dev/null +++ b/docker/cert/README.md @@ -0,0 +1,3 @@ +This directory contains prebuild self signed certificates for the XTC Platform hostname, and with the +key store password 'password'. They should not be used in production, naturally. The dev container +generates these values itself. \ No newline at end of file diff --git a/docker/config/cfg.json b/docker/config/cfg.json new file mode 100644 index 0000000..b4bbb37 --- /dev/null +++ b/docker/config/cfg.json @@ -0,0 +1,5 @@ +{ + "hostName": "xtc-platform.container.xqiz.it", + "httpPort": 8080, + "httpsPort": 8090 +} diff --git a/docker/config/port-forwarding.conf b/docker/config/port-forwarding.conf new file mode 100644 index 0000000..2dd82d6 --- /dev/null +++ b/docker/config/port-forwarding.conf @@ -0,0 +1,4 @@ +; TODO this should not be necessary, it's merely a matter of container network setup / container hosts and ports. + +rdr pass on lo0 inet proto tcp from any to self port 80 -> 127.0.0.1 port 8080 +rdr pass on lo0 inet proto tcp from any to self port 443 -> 127.0.0.1 port 8090 diff --git a/docker/scripts/entrypoint.sh b/docker/scripts/entrypoint.sh new file mode 100755 index 0000000..0e180f0 --- /dev/null +++ b/docker/scripts/entrypoint.sh @@ -0,0 +1,85 @@ +#!/bin/bash + +# This is more of a devcontainer than a production container, since we set up a build enviromnment and +# make it possible to update and build sources from it. This should be split into separate responsibiliteis +# as +echo "Entrypoint for Platform..." + +export PLATFORM_DIR=$HOME/build + +function check_file() { + if [ -z "$1" ]; then + echo "No file name provided to check." + exit 1 + fi + if [ ! -f "$1" ]; then + echo "File $1 not found." + exit 1 + fi + _len=$(state --printf "%s" "$1") + if [ $_len -le 0 ]; then + echo "File "$1" reports size as <= bytes." + exit 1 + fi +} + +function check_name_resolution() { + ping -c 1 xtc-platform.localhost.xqiz.it + if [ $? != 0 ]; then + echo "Ping to localhost failed using xtc-platform.localhost.xqiz.it" + exit 1 + fi + echo "xtc-platform.localhost.xqiz.it resolves and responds to ping." +} + +# Ensure persistent docker volume is set up, and link our secrets and build and Gradle cache dirs from it. +function ensure_volume() { + sudo chown -R $XTC_USER:$XTC_USER /persistent + ln -s /persistent $PLATFORM_DIR + export GRADLE_USER_HOME=$HOME/.gradle + if [ ! -d $PLATFORM_DIR/gradle ]; then + echo "Creating Gradle user home: $GRADLE_USER_HOME" + mkdir -p $PLATFORM_DIR/gradle + fi + ln -s $PLATFORM_DIR/gradle $GRADLE_USER_HOME + if [ ! -e $GRADLE_USER_HOME/gradle.properties ]; then + echo "Linking gradle.properties" + ln -s /var/run/secrets/gradle_properties $GRADLE_USER_HOME/gradle.properties + fi +} + +check_name_resolution + +if [ -n "$DEV_CONTAINER" ]; then + echo "Dev container detected - syncing out source." + ensure_volumes + source /usr/local/bin/platform-build.sh + check_updated_source + check_platform_build + pushd $SRC_DIR + ./gradlew run + popd +else + echo "Prod container detected - everything should be installed already." + echo "Verifying installation." + pushd $HOME/lib + check_file "common.xtc" + check_file "host.xtc" + check_file "kernel.xtc" + check_file "platformDB.xtc" + check_file "platformUI.xtc" + check_file "xdk/javatools.jar" + popd + + export "Setting up aliases." + alias xcc="java -jar $HOME/lib/xdk/javatools.jar xcc" + alias xec"=java -jar $HOME/lib/xdk/ +fi + +# Pass any remaining args or CMD on to the run command. +if [ -z "${@}" ]; then + echo "No extra entrypoint arguments. Container exiting from $0." +else + echo "Handing over entrypoint arguments to exec: ${@}" + exec "${@}" +fi diff --git a/docker/scripts/platform-build.sh b/docker/scripts/platform-build.sh new file mode 100755 index 0000000..a2e9a57 --- /dev/null +++ b/docker/scripts/platform-build.sh @@ -0,0 +1,52 @@ +#!/bin/bash + +echo "Ensuring the platform is built." + +export SRC_DIR=$PLATFORM_DIR/src + +function clone_platform() { + pushd $PLATFORM_DIR + echo "Cloning to platform" + git clone --branch $GITHUB_BRANCH_PLATFORM --depth=1 https://github.com/xtclang/platform.git src + echo "Writing last commit" + pushd $SRC_DIR + git rev-parse --verify HEAD > last_commit + popd + popd +} + +function check_updated_source() { + echo "Checking for $SRC_DIR" + ls -lart $PLATFORM_DIR + if [ -e $SRC_DIR ]; then + echo "Found existing source." + + pushd $SRC_DIR + _last_commit="unknown" + if [ -e last_commit ]; then + _last_commit=$(cat last_commit) + fi + _last_remote_commit=$(git ls-remote https://github.com/xtclang/platform.git platform-xtcplugin | awk '{ print $1 }') + popd + + echo "last_commit : $_last_commit" + echo "last_remote_commit: $_last_remote_commit" + if [ "$_last_commit" != "$_last_remote_commit" ]; then + echo "Existing source out of date. REMOVING existing source dir: $SRC_DIR" + rm -fr $SRC_DIR + clone_platform + else + echo "Existing platform is up date - good!" + fi + else + echo "Cloning new platform (no previous version found)." + clone_platform + fi +} + +# Verify that the platform is built +function check_platform_build() { + pushd $SRC_DIR + ./gradlew build + popd +} diff --git a/docker/scripts/platform-down.sh b/docker/scripts/platform-down.sh new file mode 100755 index 0000000..9d192d3 --- /dev/null +++ b/docker/scripts/platform-down.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +_platform="htts://xtc-platform.localhost.xqiz.it" +if [ -z $1 ]; then + echo "No platform URL given, defaulting to 'https://xtc-platform.localhost.xqiz.it'" +else + _platform=$1 +fi + +echo "Taking down platform: '$_platform'..." +https://xtc-platform2.localhost.xqiz.itecho "Done." diff --git a/docker/scripts/platform-up.sh b/docker/scripts/platform-up.sh new file mode 100755 index 0000000..3d3247d --- /dev/null +++ b/docker/scripts/platform-up.sh @@ -0,0 +1,7 @@ +#!/bin/sh + + +#./gradlew build +#xec --verbose -L ./build/platform/ kernel.xqiz.it + +# ./gradlew run -PkeystorePassword=` diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..8627244 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,25 @@ +# Common properties for the build. + +group=platform.xqiz.it +version=1.0.0 + +org.gradle.parallel=true +org.gradle.caching=true + +# NOTE: See the comment about this property in the sibling file "build.gradle.kts". In a production system, +# this password would of course be stored outside the project, and never ever checked into source control. +# This declaration is just for demo purposes and to support headless test environments like GitHub Runners. +# However, it does contribute to dev process efficiency as well, by verifying that password input +#keystorePassword=password + +gitHubUrl=https://maven.pkg.github.com/xtclang/xvm + +# This is a secret, but for testing, this shows you can define it either here, outside the repository, +# or by using properties or system environment variables, following the correct convention. + +# These are secrets and should be defined outside the repository +#gitHubUser= +#gitHubToken= + +mavenLocalRepo=false +xtclangGitHubRepo=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..1196df9 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,27 @@ +# +# This is the only file in the project where we keep version information and artifact +# names for our plugins and dependencies. +# + +[versions] +xdk = "0.4.43" + +gradle-node = "7.0.1" +node = "20.10.0" +npm = "10.4.0" +yarn = "1.22.21" +tasktree = "2.1.1" + +[plugins] +xtc = { id = "org.xtclang.xtc-plugin", version.ref = "xdk" } +node = { id = "com.github.node-gradle.node", version.ref = "gradle-node" } + +# taskTree is a helper that we can use to view task dependencies +# for example: ./gradlew run taskTree, or ./gradle run taskTree --with-inputs --with-outputs +tasktree = { id = "com.dorongold.task-tree", version.ref = "tasktree" } + +[libraries] +xdk = { group = "org.xtclang", name = "xdk", version.ref = "xdk" } + +[bundles] +# No bundles so far diff --git a/host/build.gradle.kts b/host/build.gradle.kts index b2b1452..5e599fe 100644 --- a/host/build.gradle.kts +++ b/host/build.gradle.kts @@ -2,28 +2,15 @@ * Build the host module. */ -val libDir = "${rootProject.projectDir}/lib" +plugins { + alias(libs.plugins.xtc) +} -tasks.register("build") { - group = "Build" - description = "Build this module" +xtcCompile { + verbose = true +} - 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 +dependencies { + xdkDistribution(libs.xdk) + xtcModule(project(":common")) +} diff --git a/kernel/build.gradle.kts b/kernel/build.gradle.kts index 0c761d9..a909c07 100644 --- a/kernel/build.gradle.kts +++ b/kernel/build.gradle.kts @@ -2,29 +2,12 @@ * Build the "kernel" module. */ -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 +plugins { + alias(libs.plugins.xtc) +} + +dependencies { + xdkDistribution(libs.xdk) + xtcModule(project(":common")) + xtcModule(project(":platformDB")) +} diff --git a/kernel/src/main/resources/cfg.json b/kernel/src/main/resources/cfg.json index b3a3d3f..848a64b 100644 --- a/kernel/src/main/resources/cfg.json +++ b/kernel/src/main/resources/cfg.json @@ -1,5 +1,5 @@ { -"hostName":"xtc-platform.localhost.xqiz.it", +"hostName":"xtc-platform2.localhost.xqiz.it", "httpPort":8080, "httpsPort":8090 } \ No newline at end of file diff --git a/kernel/src/main/x/kernel.x b/kernel/src/main/x/kernel.x index 87a3564..4b4db61 100644 --- a/kernel/src/main/x/kernel.x +++ b/kernel/src/main/x/kernel.x @@ -59,9 +59,13 @@ module kernel.xqiz.it { @Inject ModuleRepository repository; // get the password - String password = args.size == 0 - ? console.readLine("Enter password:", suppressEcho=True) - : args[0]; + String password = ""; + if (password.size == 0) { + console.print("Enter password:"); + password = console.readLine(suppressEcho=True); + } else { + password = args[0]; + } // ensure necessary directories Directory platformDir = homeDir.dirFor("xqiz.it/platform").ensure(); @@ -192,4 +196,4 @@ module kernel.xqiz.it { errors.reportAll(msg -> console.print(msg)); } } -} \ No newline at end of file +} diff --git a/platformDB/build.gradle.kts b/platformDB/build.gradle.kts index 19a2c28..649763e 100644 --- a/platformDB/build.gradle.kts +++ b/platformDB/build.gradle.kts @@ -1,29 +1,12 @@ -/* - * Build the platformDB module. +/** + * The platform database subproject. */ -val libDir = "${rootProject.projectDir}/lib" +plugins { + alias(libs.plugins.xtc) +} -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/platformDB.xtc").lastModified() - - if (src > dst) { - val srcModule = "${projectDir}/src/main/x/platformDB.x" - - project.exec { - commandLine("xcc", "--verbose", - "-o", libDir, - "-L", libDir, - srcModule) - } - } - } -} \ No newline at end of file +dependencies { + xdkDistribution(libs.xdk) + xtcModule(project(":common")) +} diff --git a/platformUI/build.gradle.kts b/platformUI/build.gradle.kts index defb80d..fd1aebf 100644 --- a/platformUI/build.gradle.kts +++ b/platformUI/build.gradle.kts @@ -1,68 +1,168 @@ -/* - * Build the "platformUI" module. +/** + * 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. */ -val common = project(":common") +import com.github.gradle.node.yarn.task.YarnTask -val libDir = "${rootProject.projectDir}/lib" +node { + // Retrieve tested versions of Node, Npm and Yarn from the version catalog (see gradle/libs.versions.toml) + version = libs.versions.node.get() + npmVersion = libs.versions.npm.get() + yarnVersion = libs.versions.yarn.get() -val guiDir = "$projectDir/gui" -val webContent = "$guiDir/dist" - -tasks.register("clean") { - group = "Build" - description = "Delete previous build results" + // Download any Node, Npm and Yarn versions that aren't available locally, and use them from within the build. + download = true + // See settings.gradle.kts; workaround to make the Node plugin work, while still allowing repository declarations outside of settings.gradle.kts. + distBaseUrl = null +} - delete(webContent) +plugins { + alias(libs.plugins.node) + alias(libs.plugins.xtc) } -tasks.register("build") { - group = "Build" - description = "Build this module" +dependencies { + xdkDistribution(libs.xdk) + xtcModule(project(":common")) +} - dependsOn(common.tasks["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") - // there must be a way to tell quasar not to rebuild if nothing changed, but I cannot - // figure it out and have to use a manual timestamp check - dependsOn(checkGui) +/** + * 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 { + srcDir(files("gui/dist/")) + } + } +} +val clean by tasks.existing { doLast { - val srcModule = "${projectDir}/src/main/x/platformUI.x" - - project.exec { - commandLine("xcc", "--verbose", - "-o", libDir, - "-L", libDir, - "-r", webContent, - srcModule) + for (buildDir in buildDirs) { + logger.info("Want to clean build dir: $buildDir") + delete(layout.files(buildDir)) } } } -val checkGui = tasks.register("checkGui") { - group = "Build" - description = "Build the web app content" +/** + * Task that will make sure yarn updates all node_modules. + */ +val yarnAddDependencies by tasks.registering(YarnTask::class) { + workingDir = gui + dependsOn(tasks.yarnSetup) - val src1 = fileTree("$projectDir/gui/src").files.stream(). - mapToLong{f -> f.lastModified()}.max().orElse(0) - val src2 = fileTree("$projectDir/gui/public").files.stream(). - mapToLong{f -> f.lastModified()}.max().orElse(0) - val dest = fileTree(webContent).files.stream(). - mapToLong{f -> f.lastModified()}.max().orElse(0) + // 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.dir("gui/node_modules") - if (src1 > dest || src2 > dest) { - dependsOn(buildGui) + // Add a dependency to quasar. If one exists in the yarn/lock file, it may be used instead, so + // if the state of global/local installation changed, that may still rebuild, though, if it's + // not installed in both places. + val quasarGlobal = providers.gradleProperty("org.xtclang.platform.quasarGlobal") + args = buildList { + add("add") + if (quasarGlobal.isPresent && quasarGlobal.get().toBoolean()) { + add("global") } - else { - println("$webContent is up to date") + add("quasar") + add("@quasar/cli") + } + + doFirst { + logger.lifecycle("Task '$name' installing Quasar (${if (quasarGlobal.isPresent && quasarGlobal.get().toBoolean()) "globally" else "locally, only for ${rootProject.name})"}.") + printTaskInputsAndOutputs(LogLevel.INFO) } } -val buildGui = tasks.register("buildGui") { +/** + * Task that defines the inputs and outputs for the Quasar webapp, and builds it. This means that the task + * should detect, e.g. if someone changes index.html or a single Vue file, and then rerun the task. Otherwise + * the task will be treated as "up to date". + * + * The quasar build creates the gui/dist/spa folder and files. + */ +val yarnQuasarBuild by tasks.registering(YarnTask::class) { + workingDir = gui + dependsOn(yarnAddDependencies) + + inputs.files("gui/public") + inputs.files("gui/index.html", "gui/src") + outputs.dir("gui/.quasar") + outputs.dir("gui/dist/spa") // Declare output file collection, even though empty, or we can never cache the yarnQuasarBuild task. + args = listOf("quasar", "build") doLast { - project.exec { - workingDir(guiDir) - commandLine("yarn", "--ignore-engines", "quasar", "build") - } + printTaskInputsAndOutputs(LogLevel.INFO) + } +} + + +/** + * Compile the XTC PlatformUI Module. + */ +val compileXtc by tasks.existing { + //dependsOn(verifySourceSets) + dependsOn(yarnQuasarBuild) +} + +val processResources by tasks.existing { + enabled = false + // TODO get rid of processResources from the XTC build cycle. +} + +// This implicitly copies the yarnQuasarBuild outputs to platformUI/build/xtc/main/resources, which +// is where the compileXtc tasks expects to find its resource path. Since the yarnQuasarBuild task +// does not output its files in src/main/resources, which it should for Java or XTC, for the normal +// process resources task to pick it up, we have add the paths where they end up as a resource directory +// too. We do that in the source set configuration. Process resources then does any available transforms, +// which here is the default identity transform, i.e. copy, to the build resources directory for the +// source set main. That is what we want, because that is what compileXtc will use as its -r flag. In +// XTC resources have to be processed before the build, since the build compiles them in. Any resource +// processing hence haver to take place before the compileXtc task, and its sourceset resource outputs +// (under build/sourceset/resources) by convention, will be fed into the XTC Compiler. +val processXtcResources by tasks.existing { + dependsOn(yarnQuasarBuild) + inputs.files(yarnQuasarBuild.map { it.outputs.files }) + doLast { + printTaskInputsAndOutputs() } -} \ No newline at end of file +} + +private fun Task.printTaskInputsAndOutputs(level: LogLevel = LogLevel.LIFECYCLE) { + val inputFiles = inputs.files.asFileTree + logger.log(level, "Inputs: $name: ${inputFiles.toList()}") + val outputFiles = outputs.files.asFileTree + logger.log(level, "Outputs: $name: ${outputFiles.toList()}") + val ni = inputFiles.count() + val no = outputFiles.count() + logger.log(level, "${project.name} Task '$name' finished.") + logger.log(level, "${project.name} Inputs (count: $no):") + inputs.files.asFileTree.forEachIndexed { + i, it -> logger.log(level, "${project.name} '$name' input $i (of $ni): $it") + } + logger.log(level, "${project.name} Outputs (count: $no):") + outputs.files.asFileTree.forEachIndexed { + i, it -> logger.log(level, "${project.name} '$name' output $i (of $no): $it") + } +} diff --git a/platformUI/gradle.properties b/platformUI/gradle.properties new file mode 100644 index 0000000..926d4dc --- /dev/null +++ b/platformUI/gradle.properties @@ -0,0 +1 @@ +org.xtclang.platform.quasar.global=false diff --git a/platformUI/gui/.gitignore b/platformUI/gui/.gitignore index 7da5a9a..f1d913c 100644 --- a/platformUI/gui/.gitignore +++ b/platformUI/gui/.gitignore @@ -30,4 +30,4 @@ yarn-error.log* *.sln # local .env files -.env.local* \ No newline at end of file +.env.local* diff --git a/platformUI/gui/.npmrc b/platformUI/gui/.npmrc index 1f56332..32bd84d 100644 --- a/platformUI/gui/.npmrc +++ b/platformUI/gui/.npmrc @@ -1,3 +1,3 @@ # pnpm-related options shamefully-hoist=true -strict-peer-dependencies=false \ No newline at end of file +strict-peer-dependencies=false diff --git a/platformUI/gui/src/App.vue b/platformUI/gui/src/App.vue index 5a2ae0d..38442ee 100644 --- a/platformUI/gui/src/App.vue +++ b/platformUI/gui/src/App.vue @@ -8,4 +8,4 @@ import { defineComponent } from 'vue' export default defineComponent({ name: 'App' }) - \ No newline at end of file + diff --git a/platformUI/src/main/x/platformUI.x b/platformUI/src/main/x/platformUI.x index f47ba01..521be6a 100644 --- a/platformUI/src/main/x/platformUI.x +++ b/platformUI/src/main/x/platformUI.x @@ -43,7 +43,7 @@ module platformUI.xqiz.it { */ void configure(HttpServer server, String hostAddr, KeyStore keystore, Realm realm, AccountManager accountManager, HostManager hostManager, ErrorLog errors) { - // the 'hostAddr' is a full URI of the platform server, e.g. "xtc-platform.localhost.xqiz.it"; + // the 'hostAddr' is a full URI of the platform server, e.g. "xtc-platform2.localhost.xqiz.it"; // we need to extract the base domain ("localhost.xqiz.it") String baseDomain; if (Int dot := hostAddr.indexOf('.')) { diff --git a/settings.gradle.kts b/settings.gradle.kts index 3ddba87..7c0168d 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,8 +1,133 @@ +/** + * settings.gradle.kts is used for bootstrapping a build. + * + * This is based on the xtc-application-template repository, so understand how it works + * and how to supply credentials. If you have put working GitHub credentials in your + * $GRADLE_USER_HOME/gradle.properties already, this should just work. + * + * You will need properties named "gitHubUrl" , "gitHubUser" an "gitHubToken" + * available to the system, in order for it to work. Please see the README.md + * on how to set this up, and why you have to do this. + */ + +pluginManagement { + repositories { + val mavenLocalRepo: String? by settings + val xtclangGitHubRepo: String? by settings + + val gitHubUser: String? by settings + val gitHubToken: String? by settings + val gitHubUrl: String by settings + + println("Plugin: mavenLocal=$mavenLocalRepo, xtclangGitHubRepo=$xtclangGitHubRepo") + if (mavenLocalRepo != "true" && xtclangGitHubRepo != "true") { + throw GradleException("Error: either or both of mavenLocalRepo and xtclangGitHubRepo must be set.") + } + + if (xtclangGitHubRepo == "true") { + maven { + url = uri(gitHubUrl) + credentials { + username = gitHubUser + password = gitHubToken + } + } + } + + if (mavenLocalRepo == "true") { + // Define mavenLocal as an artifact repository (disabled by default) + mavenLocal() + } + + // Define Gradle Plugin Portal as a plugin repository + gradlePluginPortal() + } + + plugins { + id("org.xtclang.xtc-plugin") + id("com.github.node-gradle.node") + } +} + +@Suppress("UnstableApiUsage") +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + // Define XTC org GitHub Maven as a plugin repository + val mavenLocalRepo: String? by settings + val xtclangGitHubRepo: String? by settings + + val gitHubUser: String? by settings + val gitHubToken: String? by settings + val gitHubUrl: String by settings + + println("Repos: mavenLocal=$mavenLocalRepo, xtclangGitHubRepo=$xtclangGitHubRepo") + if (mavenLocalRepo != "true" && xtclangGitHubRepo != "true") { + throw GradleException("Error: either or both of mavenResolveFromMavenLocal and mavenResolveFromXtcGitHub must be set.") + } + + if (xtclangGitHubRepo == "true") { + maven { + url = uri(gitHubUrl) + credentials { + username = gitHubUser + password = gitHubToken + } + } + } + + if (mavenLocalRepo == "true") { + // Define mavenLocal as an artifact repository (disabled by default) + mavenLocal() + } + + /** + * Patch the Node configuration, so that the Node plugin doesn't try to add hardcoded + * repositories to the Platform project during build. We are following the best-practice + * of forbidding any repository declaration anywhere else but project settings. The Node + * plugin does not. However, it's way more important to be able to specify an exact Node + * version, and integrate that with the build, than having to fall back on a system wide + * version of NodeJS that may or may not be installed on your machine, and may or may + * not work well the Platform build. The Platform build religiously declares all its + * required dependencies its repository, and SHOULD NEVER rely on any other system state + * of its host machine. This also very easily paves the way for integration testing, CI/CD, + * containerization and avoids contaminating your machine with multiple NodeJS versions. + * In 2024, we do not install and rely on system wide software on a dev machine, unless + * we are completely out of alternatives. + * + * For the NodeJS Gradle plugin, this is a known bug, and the workaround is the one + * recommended by the plugin developers: + * @see https://github.com/node-gradle/gradle-node-plugin/blob/main/docs/faq.md#is-this-plugin-compatible-with-centralized-repositories-declaration + */ + ivy { + name = "NodeJS" + setUrl("https://nodejs.org/dist/") + patternLayout { + artifact("v[revision]/[artifact](-v[revision]-[classifier]).[ext]") + ivy("v[revision]/ivy.xml") + } + metadataSources { + artifact() + } + content { + includeModule("org.nodejs", "node") + } + } + } +} + +// Set the name of the main project. rootProject.name = "platform" -include(":common") -include(":kernel") -include(":host") -include(":platformDB") -include(":platformUI") -include(":platformCLI") \ No newline at end of file +listOfNotNull( + "kernel", + "common", + "host", + "platformDB", + "platformUI", + "platformCLI" +).forEach { + include(":$it") + logger.info("[platform] Added subproject '$it' to build.") +} +