From 6da08ee562ee5d7bd6c8721eae09c30d9131d639 Mon Sep 17 00:00:00 2001 From: Marcus Lagergren <1062473+lagergren@users.noreply.github.com> Date: Sun, 28 Jan 2024 13:03:54 +0100 Subject: [PATCH] XTC Kotlin build DSL through XTC Plugin, XDK artifact publications. (#114) --- .editorconfig | 1 + .github/workflows/gradle.yml | 26 - .github/workflows/xvm-verify-push.yml | 47 + .gitignore | 13 +- GROUP | 1 + README.md | 433 ++++-- VERSION | 1 + bin/check-docker-version.sh | 13 + bin/check-github-token-privileges.sh | 13 + bin/git-show-parent-branch.sh | 8 + bin/gradle-update-wrapper.sh | 9 + bin/purge-all-build-state.sh | 28 + build-logic/aggregator/build.gradle.kts | 8 + build-logic/aggregator/settings.gradle.kts | 9 + .../org.xtclang.build.aggregator.gradle.kts | 61 + .../common-plugins/common-plugins.gradle.kts | 23 + .../common-plugins/settings.gradle.kts | 6 + .../src/main/kotlin/DebugBuild.kt | 232 ++++ .../src/main/kotlin/GitHubPackages.kt | 163 +++ .../src/main/kotlin/SemanticVersion.kt | 46 + .../src/main/kotlin/XdkBuildLogic.kt | 236 ++++ .../src/main/kotlin/XdkDistribution.kt | 79 ++ .../src/main/kotlin/XdkProperties.kt | 213 +++ .../src/main/kotlin/XdkVersionHandler.kt | 94 ++ .../kotlin/org.xtclang.build.debug.gradle.kts | 57 + .../kotlin/org.xtclang.build.java.gradle.kts | 129 ++ .../org.xtclang.build.publish.gradle.kts | 164 +++ ...rg.xtclang.build.xdk.versioning.gradle.kts | 15 + build-logic/settings-plugins/build.gradle.kts | 8 + .../settings-plugins/settings.gradle.kts | 5 + ...g.xtclang.build.common.settings.gradle.kts | 55 + build.gradle.kts | 99 +- gradle.properties | 69 +- .../repos/xtc-repo.init.gradle.kts.template | 115 -- .../config/repos/xtc-repo.properties.template | 18 - gradle/config/user/.editorconfig_lagergren | 1209 +++++++++++++++++ gradle/libs.versions.toml | 81 ++ gradle/wrapper/gradle-wrapper.jar | Bin 63721 -> 43462 bytes gradlew | 19 +- gradlew.bat | 2 +- javatools/build.gradle.kts | 167 ++- javatools/javatools.properties | 12 + javatools/settings.gradle.kts | 10 + .../src/main/java/org/xvm/asm/Constants.java | 2 +- .../src/main/java/org/xvm/tool/Compiler.java | 6 +- .../src/main/java/org/xvm/tool/Launcher.java | 20 +- .../src/main/java/org/xvm/tool/Runner.java | 6 +- javatools_bridge/build.gradle.kts | 28 +- javatools_launcher/README.md | 8 +- javatools_launcher/build.gradle.kts | 37 +- .../main/resources}/exe/linux_launcher | Bin .../main/resources}/exe/macos_launcher | Bin .../main/resources}/exe/windows_launcher.exe | Bin .../main/resources}/javatools/README.md | 0 .../main/resources}/javatools/javatools.jar | Bin javatools_turtle/build.gradle.kts | 23 + .../src/main/{x => resources}/mack.x | 0 javatools_unicode/build.gradle.kts | 92 +- javatools_unicode/settings.gradle.kts | 10 + .../java/org/xvm/tool/BuildUnicodeTables.java | 682 +++++----- javatools_utils/build.gradle.kts | 44 +- javatools_utils/javatools-utils.properties | 2 + javatools_utils/settings.gradle.kts | 10 + .../src/main/java/org/xvm/util/Handy.java | 6 +- .../src/test/java/org/xvm/util/HandyTest.java | 2 +- lib_aggregate/build.gradle.kts | 9 + lib_collections/build.gradle.kts | 9 + lib_crypto/build.gradle.kts | 9 + lib_ecstasy/build.gradle.kts | 79 +- lib_json/build.gradle.kts | 9 + lib_jsondb/build.gradle.kts | 13 + lib_net/build.gradle.kts | 10 + lib_oodb/build.gradle.kts | 9 + lib_web/build.gradle.kts | 15 + lib_webauth/build.gradle.kts | 16 + lib_xenia/build.gradle.kts | 15 + manualTests/build.gradle.kts | 322 +++-- manualTests/settings.gradle.kts | 13 + manualTests/src/main/x/FizzBuzz.x | 25 + plugin/build.gradle.kts | 131 ++ plugin/settings.gradle.kts | 10 + .../org/xtclang/plugin/ProjectDelegate.java | 212 +++ .../main/java/org/xtclang/plugin/Usage.java | 10 + .../org/xtclang/plugin/XtcBuildException.java | 45 + .../plugin/XtcBuildRuntimeException.java | 28 + .../xtclang/plugin/XtcCompilerExtension.java | 17 + .../java/org/xtclang/plugin/XtcExtension.java | 4 + .../plugin/XtcLauncherTaskExtension.java | 43 + .../org/xtclang/plugin/XtcModulePath.java | 12 + .../java/org/xtclang/plugin/XtcPlugin.java | 47 + .../xtclang/plugin/XtcPluginConstants.java | 75 + .../org/xtclang/plugin/XtcPluginUtils.java | 135 ++ .../xtclang/plugin/XtcProjectDelegate.java | 583 ++++++++ .../java/org/xtclang/plugin/XtcRunModule.java | 35 + .../xtclang/plugin/XtcRuntimeExtension.java | 28 + .../xtclang/plugin/XtcSourceDirectorySet.java | 6 + .../internal/DefaultXtcCompilerExtension.java | 61 + .../plugin/internal/DefaultXtcExtension.java | 18 + .../DefaultXtcLauncherTaskExtension.java | 123 ++ .../plugin/internal/DefaultXtcRunModule.java | 116 ++ .../internal/DefaultXtcRuntimeExtension.java | 117 ++ .../DefaultXtcSourceDirectorySet.java | 15 + .../plugin/launchers/BuildThreadLauncher.java | 130 ++ .../launchers/ChildProcessLauncher.java | 6 + .../xtclang/plugin/launchers/CommandLine.java | 114 ++ .../plugin/launchers/JavaExecLauncher.java | 145 ++ .../launchers/NativeBinaryLauncher.java | 61 + .../plugin/launchers/XtcExecResult.java | 194 +++ .../xtclang/plugin/launchers/XtcLauncher.java | 64 + .../xtclang/plugin/tasks/XtcCompileTask.java | 264 ++++ .../xtclang/plugin/tasks/XtcDefaultTask.java | 70 + .../plugin/tasks/XtcExtractXdkTask.java | 81 ++ .../xtclang/plugin/tasks/XtcLauncherTask.java | 339 +++++ .../xtclang/plugin/tasks/XtcRunAllTask.java | 26 + .../org/xtclang/plugin/tasks/XtcRunTask.java | 279 ++++ .../xtclang/plugin/tasks/XtcSourceTask.java | 233 ++++ plugin/xtc-plugin.properties | 30 + settings.gradle.kts | 60 +- xdk.properties | 59 + xdk/build.gradle.kts | 801 ++++------- xdk/settings.gradle.kts | 49 + 121 files changed, 8850 insertions(+), 1554 deletions(-) create mode 120000 .editorconfig delete mode 100644 .github/workflows/gradle.yml create mode 100644 .github/workflows/xvm-verify-push.yml create mode 100644 GROUP create mode 100644 VERSION create mode 100755 bin/check-docker-version.sh create mode 100755 bin/check-github-token-privileges.sh create mode 100755 bin/git-show-parent-branch.sh create mode 100755 bin/gradle-update-wrapper.sh create mode 100755 bin/purge-all-build-state.sh create mode 100644 build-logic/aggregator/build.gradle.kts create mode 100644 build-logic/aggregator/settings.gradle.kts create mode 100644 build-logic/aggregator/src/main/kotlin/org.xtclang.build.aggregator.gradle.kts create mode 100644 build-logic/common-plugins/common-plugins.gradle.kts create mode 100644 build-logic/common-plugins/settings.gradle.kts create mode 100644 build-logic/common-plugins/src/main/kotlin/DebugBuild.kt create mode 100644 build-logic/common-plugins/src/main/kotlin/GitHubPackages.kt create mode 100644 build-logic/common-plugins/src/main/kotlin/SemanticVersion.kt create mode 100644 build-logic/common-plugins/src/main/kotlin/XdkBuildLogic.kt create mode 100644 build-logic/common-plugins/src/main/kotlin/XdkDistribution.kt create mode 100644 build-logic/common-plugins/src/main/kotlin/XdkProperties.kt create mode 100644 build-logic/common-plugins/src/main/kotlin/XdkVersionHandler.kt create mode 100644 build-logic/common-plugins/src/main/kotlin/org.xtclang.build.debug.gradle.kts create mode 100644 build-logic/common-plugins/src/main/kotlin/org.xtclang.build.java.gradle.kts create mode 100644 build-logic/common-plugins/src/main/kotlin/org.xtclang.build.publish.gradle.kts create mode 100644 build-logic/common-plugins/src/main/kotlin/org.xtclang.build.xdk.versioning.gradle.kts create mode 100644 build-logic/settings-plugins/build.gradle.kts create mode 100644 build-logic/settings-plugins/settings.gradle.kts create mode 100644 build-logic/settings-plugins/src/main/kotlin/org.xtclang.build.common.settings.gradle.kts delete mode 100644 gradle/config/repos/xtc-repo.init.gradle.kts.template delete mode 100644 gradle/config/repos/xtc-repo.properties.template create mode 100644 gradle/config/user/.editorconfig_lagergren create mode 100644 gradle/libs.versions.toml create mode 100644 javatools/javatools.properties create mode 100644 javatools/settings.gradle.kts rename javatools_launcher/{build => src/main/resources}/exe/linux_launcher (100%) rename javatools_launcher/{build => src/main/resources}/exe/macos_launcher (100%) rename javatools_launcher/{build => src/main/resources}/exe/windows_launcher.exe (100%) rename javatools_launcher/{build => src/main/resources}/javatools/README.md (100%) rename javatools_launcher/{build => src/main/resources}/javatools/javatools.jar (100%) create mode 100644 javatools_turtle/build.gradle.kts rename javatools_turtle/src/main/{x => resources}/mack.x (100%) create mode 100644 javatools_unicode/settings.gradle.kts create mode 100644 javatools_utils/javatools-utils.properties create mode 100644 javatools_utils/settings.gradle.kts create mode 100644 lib_aggregate/build.gradle.kts create mode 100644 lib_collections/build.gradle.kts create mode 100644 lib_crypto/build.gradle.kts create mode 100644 lib_json/build.gradle.kts create mode 100644 lib_jsondb/build.gradle.kts create mode 100644 lib_net/build.gradle.kts create mode 100644 lib_oodb/build.gradle.kts create mode 100644 lib_web/build.gradle.kts create mode 100644 lib_webauth/build.gradle.kts create mode 100644 lib_xenia/build.gradle.kts create mode 100644 manualTests/settings.gradle.kts create mode 100644 manualTests/src/main/x/FizzBuzz.x create mode 100644 plugin/build.gradle.kts create mode 100644 plugin/settings.gradle.kts create mode 100644 plugin/src/main/java/org/xtclang/plugin/ProjectDelegate.java create mode 100644 plugin/src/main/java/org/xtclang/plugin/Usage.java create mode 100644 plugin/src/main/java/org/xtclang/plugin/XtcBuildException.java create mode 100644 plugin/src/main/java/org/xtclang/plugin/XtcBuildRuntimeException.java create mode 100644 plugin/src/main/java/org/xtclang/plugin/XtcCompilerExtension.java create mode 100644 plugin/src/main/java/org/xtclang/plugin/XtcExtension.java create mode 100644 plugin/src/main/java/org/xtclang/plugin/XtcLauncherTaskExtension.java create mode 100644 plugin/src/main/java/org/xtclang/plugin/XtcModulePath.java create mode 100644 plugin/src/main/java/org/xtclang/plugin/XtcPlugin.java create mode 100644 plugin/src/main/java/org/xtclang/plugin/XtcPluginConstants.java create mode 100644 plugin/src/main/java/org/xtclang/plugin/XtcPluginUtils.java create mode 100644 plugin/src/main/java/org/xtclang/plugin/XtcProjectDelegate.java create mode 100644 plugin/src/main/java/org/xtclang/plugin/XtcRunModule.java create mode 100644 plugin/src/main/java/org/xtclang/plugin/XtcRuntimeExtension.java create mode 100644 plugin/src/main/java/org/xtclang/plugin/XtcSourceDirectorySet.java create mode 100644 plugin/src/main/java/org/xtclang/plugin/internal/DefaultXtcCompilerExtension.java create mode 100644 plugin/src/main/java/org/xtclang/plugin/internal/DefaultXtcExtension.java create mode 100644 plugin/src/main/java/org/xtclang/plugin/internal/DefaultXtcLauncherTaskExtension.java create mode 100644 plugin/src/main/java/org/xtclang/plugin/internal/DefaultXtcRunModule.java create mode 100644 plugin/src/main/java/org/xtclang/plugin/internal/DefaultXtcRuntimeExtension.java create mode 100644 plugin/src/main/java/org/xtclang/plugin/internal/DefaultXtcSourceDirectorySet.java create mode 100644 plugin/src/main/java/org/xtclang/plugin/launchers/BuildThreadLauncher.java create mode 100644 plugin/src/main/java/org/xtclang/plugin/launchers/ChildProcessLauncher.java create mode 100644 plugin/src/main/java/org/xtclang/plugin/launchers/CommandLine.java create mode 100644 plugin/src/main/java/org/xtclang/plugin/launchers/JavaExecLauncher.java create mode 100644 plugin/src/main/java/org/xtclang/plugin/launchers/NativeBinaryLauncher.java create mode 100644 plugin/src/main/java/org/xtclang/plugin/launchers/XtcExecResult.java create mode 100644 plugin/src/main/java/org/xtclang/plugin/launchers/XtcLauncher.java create mode 100644 plugin/src/main/java/org/xtclang/plugin/tasks/XtcCompileTask.java create mode 100644 plugin/src/main/java/org/xtclang/plugin/tasks/XtcDefaultTask.java create mode 100644 plugin/src/main/java/org/xtclang/plugin/tasks/XtcExtractXdkTask.java create mode 100644 plugin/src/main/java/org/xtclang/plugin/tasks/XtcLauncherTask.java create mode 100644 plugin/src/main/java/org/xtclang/plugin/tasks/XtcRunAllTask.java create mode 100644 plugin/src/main/java/org/xtclang/plugin/tasks/XtcRunTask.java create mode 100644 plugin/src/main/java/org/xtclang/plugin/tasks/XtcSourceTask.java create mode 100644 plugin/xtc-plugin.properties create mode 100644 xdk.properties create mode 100644 xdk/settings.gradle.kts diff --git a/.editorconfig b/.editorconfig new file mode 120000 index 0000000000..54acf4e7df --- /dev/null +++ b/.editorconfig @@ -0,0 +1 @@ +gradle/config/user/.editorconfig_lagergren \ No newline at end of file diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml deleted file mode 100644 index 265a3afc4a..0000000000 --- a/.github/workflows/gradle.yml +++ /dev/null @@ -1,26 +0,0 @@ -# -# Action when a push event happens in our github repository. -# -# (It may be overkill, and we may want to switch to 'on: pull_request' -# when we have tested this out enough. It is the branch developer's -# responsibility to manually nsure the branch tests clean during -# development and before submission as pull request) -# -name: Build and verify Ecstasy project on push -on: push -jobs: - gradle: - strategy: - matrix: - os: [ubuntu-latest, windows-latest] - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-java@v3 - with: - distribution: 'adopt' - java-version: '17' - - name: Setup Gradle - uses: gradle/gradle-build-action@v2 - - name: Execute Gradle build - run: ./gradlew build --info diff --git a/.github/workflows/xvm-verify-push.yml b/.github/workflows/xvm-verify-push.yml new file mode 100644 index 0000000000..21b8f110fc --- /dev/null +++ b/.github/workflows/xvm-verify-push.yml @@ -0,0 +1,47 @@ +# +# GitHub runner workflow for building, verifying and testing the XVM repo. +# +# It also does some quick sanity check that we can provide release artifacts. This latter part +# will be implemented in much better detail when we have a marge to master and minimal effort +# GitHub release plugin integration, so we can automate release generation. +# +# TODO: Add a release workflow, and a distribution creation workflow. Reuse parts of the +# "xdk-release" repo, that can run the cross product of aarch64, amd64, Linux, Windows and +# MacOS, including creating the Windows "exe" installer, on any platform with Nsis. +# +# TODO: Add workflow jobs in another GitHub workflow configuration that builds SNAPSHOT releases +# when a PR is merged into master. +# +# TODO: Discuss what other kinds of GitHub workflow actions we need. This can be anything +# from cron jobs that run every night, to containerization tests/efforts/creations. +# + +name: XVM Repository build, test and verification runner. + +on: push + +env: + ORG_XTCLANG_PLUGIN_VERBOSE: true + ORG_XTCLANG_BUILD_SANITY_CHECK_RUNTIME: true + ORG_XTCLANG_BUILD_SANITY_CHECK_RUNTIME_FORCE_REBUILD: false + +jobs: + gradle: + # If we do not specify a version here, the runner will pick up whatever Gradle version + # that is defined by our wrapper, which is exactly what we want. + strategy: + matrix: + os: [ ubuntu-latest, windows-latest ] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '21' + - name: Setup Gradle + uses: gradle/gradle-build-action@v2 + - name: Verify Gradle Build + run: ./gradlew build --info + - name: Verify Gradle Install + run: ./gradlew installLocalDist --info diff --git a/.gitignore b/.gitignore index 79cb29050f..22bbd66491 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,6 @@ *.com *.class *.dll -*.exe *.o *.so *.dSYM @@ -58,9 +57,21 @@ prj/ .idea/ .gradle/ !gradle-wrapper.jar +!javatools_launcher/src/main/resources/javatools/javatools.jar +!javatools_launcher/src/main/resources/exe/* # File-based project format IDEA (legacy) *.iml *.ipr *.iws +# Eventually, after code reformatting and package renaming, we want an .editorconfig in the root. +# Right now, to phase that in, we allow an .editorconfig symlink in the root pointing out another +# not yet 100% project standard .editorconfig personal to the user, symlinked from here. Hence, +# right now, the root directory .editorconfig should be ignored by Git until after the new code +# conventions have been decided upon, and have enforcement tools in place as part of the build. +.editorconfig + +# Gradle profiler output +gradle-user-home/ +profile-out/ diff --git a/GROUP b/GROUP new file mode 100644 index 0000000000..d02c5d525b --- /dev/null +++ b/GROUP @@ -0,0 +1 @@ +org.xtclang diff --git a/README.md b/README.md index 92ce599700..d99fd90f32 100644 --- a/README.md +++ b/README.md @@ -28,11 +28,12 @@ The Ecstasy language supports first class modules, including versioning and cond class functions, including currying and partial application; type-safe object orientation, including support for auto-narrowing types, type-safe covariance, mixins, and duck-typed interfaces; complete type inference; first class immutable types; first class asynchronous services, including -both automatic `async/await`-style and promises-based (`@Future`) programming models; and first +both automatic `async/await`-style and promises-based (`@Future`) programming models; and first class software containers, including resource injection and transitively-closed, immutable type systems. _And much, much more._ - -Read more at [https://xtclang.blogspot.com/](https://xtclang.blogspot.com/2016/11/welcome-to-ecstasy-language-first.html) + +Read more +at [https://xtclang.blogspot.com/](https://xtclang.blogspot.com/2016/11/welcome-to-ecstasy-language-first.html) Follow us on Twitter [@xtclang](https://twitter.com/xtclang) @@ -40,93 +41,122 @@ Find out more about [how you can contribute to Ecstasy](CONTRIBUTING.md). And please respect our [code of conduct](CODE_OF_CONDUCT.md) and each other. -## Installation - +## Binary Installation + For **macOS** and **Linux**: 1. If you do not already have the `brew` command available, install [Homebrew](https://brew.sh/) - -2. Add a "tap" to access the XDK CI builds, and install the latest XDK CI build: + +2. Add a "tap" to access the XDK CI builds, and install the latest XDK CI build: + ``` - brew tap xtclang/xvm && brew install xdk-latest +brew tap xtclang/xvm && brew install xdk-latest ``` -3. To upgrade to the latest XDK CI build at any time: +3. To upgrade to the latest XDK CI build at any time: + ``` - brew update && brew upgrade xdk-latest +brew update && brew upgrade xdk-latest ``` For **Windows**: - + * Visit [http://xtclang.org/xdk-latest.html](http://xtclang.org/xdk-latest.html) to download a - Windows installer for the latest XDK build + Windows installer for the latest XDK build Manual local build for **any computer** (for advanced users): - + * Install Java (version 17 or later) and Gradle * Use `git` to obtain the XDK: + ``` git clone https://github.com/xtclang/xvm.git ``` - + * `cd` into the git repo (the directory will contain [these files](https://github.com/xtclang/xvm/)) and execute the Gradle build: + ``` ./gradlew build ``` -## Workflow and source control - -### Local git configuration +## Development -The project comes with a local git configuration, stored in the file ".gitconfig" in the root -of the repository. The configuration also contains various powerful and conventient shortcuts -for common git operations. - -To apply the local git configuration, execute this commit from the repository root: - -``` -git config --local include.path ../.gitconfig -``` - -### Recommended git workflow +### Recommended Git workflow *A note about this section: this workflow is supported by pretty much every common GUI in any common IDE, in one way or another. But in the interest of not having to document several instances with slightly different naming convention, or deliver a confusing tutorial, this section only describes the exact bare -bones commmand line git commands that can be used to implement our workflow, +bones command line git commands that can be used to implement our workflow, which is also a common developer preference. All known IDEs just wrap these commands in one way or another.* +#### Make sure "pull.rebase" is set to "true" in your git configuration + +In order to maintain linear git history, and at any cost avoid merges being created +and persisted in the code base, please make sure that your git configuration will +run "pull" with "rebase" as its default option. Preferably globally, but at least +for the XVM repository. + +``` +git config --get pull.rebase +``` + +Output should be "true". + +If it's not, execute + +``` +git config --global pull.rebase true +``` + +or from a directory inside the repository: + +``` +git config --local pull.rebase true +``` + +The latter will only change the pull semantics for the repository itself, and +the config may or may not be rewritten by future updates. + +#### Always work in a branch. Do not work directly in master + +XTC will very soon switch to only allowing putting code onto the master branch through +a pull request in a sub branch. + In order to minimize git merges, and to keep master clean, with a minimum of complexity, the recommended workflow for submitting a pull request is as follows: -Create a new branch for your change, and connect it to the upstream: +##### 1) Create a new branch for your change, and connect it to the upstream: ``` git checkout -B decriptive-branch-name git push --set-upstream origin descriptive-branch-name ``` -Perform your changes, and commit them. We currently do not have any syntax requirements +##### 2) Perform your changes, and commit them. We currently do not have any syntax requirements + on commit descriptions, but it's a good idea to describe the purpose of the commit. ``` git commit -m "Descriptive commit message, including a github issue reference, if one exists" +``` + +##### 3) Push your changes to the upstream and create a pull request, when you are ready for review + +``` git push ``` -Whenever you need to, and this is encouraged, you should rebase your local branch, -so that your changes gets transplanted on top of everything that has been pushed to -master, during the time you have been working on the branch. +##### Resolving conflicts, and keeping your branch up to date with master -Furthermore, we recommend that you configure git pull to use rebase mode as -its default, rather than merge. This is already enabled in our repository local -git settings. +Whenever you need to, and this is encouraged, you should rebase your local branch, +so that your changes get ripped out and re-transplanted on top of everything that has +been pushed to master, during the time you have been working on the branch. -Before you submit a pull request, you *need* to rebase it agaist master. We will +Before you submit a pull request, you *need* to rebase it against master. We will gradually add build pipeline logic for helping out with this, and other things, but it's still strongly recommended that you understand the process. @@ -138,9 +168,17 @@ git fetch git rebase origin/master ``` -If there are any conflicts, the rebase will be halted. Should this be the case, change -your code to resolve the conflicts, and verify that it builds clean again. After it does, -add the resolved commit and tell git to continue with the rebase: +The fetch command ensures that the global state of the world, whose local copy is stored +in the ".git" directory of the repository, gets updated. Remember that git allows you to +work completely offline, should you chose to do so, after you have cloned a repository. +This means that, in order to get the latest changes from the rest of the world, and make +sure you are working in an up-to-date environment, you need to fetch that state from the +upstream. + +If there are any conflicts, the rebase command above will halt and report conflict. +Should this be the case, change your code to resolve the conflicts, and verify that it +builds clean again. After it does, add the resolved commit and tell git to continue +with the rebase: ``` git add . @@ -164,27 +202,29 @@ git status git push -f # if needed ``` +##### Do not be afraid to mess around in your local branch + You should feel free to commit and push as much as you want in your local branch, if your workflow so requires. However, before submitting the finished branch as a pull -request, do an interactive rebase and replace "pick" with "fixup" to merge any -temporary commits with their predecessor. +request, please do an interactive rebase and collapse any broken commits that don't +build, or any small commits that just fix typos and things of a similar nature. -* It is considered bad form to submit a pull request where there are unncessary -or intermediate commits, with vague descriptions. +* _It is considered bad form to submit a pull request where there are unnecessary + or intermediate commits, with vague descriptions._ -* It is considered bad form to submit a pull request where there are commits, which -do not build and test cleanly. This is important, because it enables things like -automating git bisection to narrow down commits that may have introduced bugs, -and it has various other benefits. The ideal state for master, should be that -you can check it out at any change in its commit history, and that it will build -and test clean on that head. +* _It is considered bad form to submit a pull request where there are commits, which + do not build and test cleanly._ This is important, because it enables things like + automating git bisection to narrow down commits that may have introduced bugs, + and it has various other benefits. The ideal state for master, should be that + you can check it out at any change in its commit history, and that it will build + and test clean on that head. -Most pull requests are small in scope, should and contain only one commit, when +Most pull requests are small in scope, and should contain only one commit, when they are put up for review. If there are distinct unrelated commits, that both contribute to solving the issue you are working on, it's naturally fine to not squash those together, -as it's easier to read and shows clear separation of concerns. +as it's easier to read and shows clear separation of concerns. -If you need to get rid of temporary, broken, or unbuildable commits in your branch, +If you need to get rid of temporary, broken, or non-buildable commits in your branch, do an interactive rebase before you submit it for review. You can execute: ``` @@ -193,22 +233,22 @@ git rebase -i HEAD~n to do this, where *n* is the number of commits you are interested in modifying. -*According to the git philosophy, branches should be thought of as private, plentiful -and ephemeral. They should be created at the drop of a hat, and the branch should be -automatically or manually deleted after its changes have been merged to master. -A branch should never be reused.* +* *According to the git philosophy, branches should be thought of as private, plentiful + and ephemeral. They should be created at the drop of a hat, and the branch should be + automatically or manually deleted after its changes have been merged to master. + A branch should never be reused.* The described approach is a good one to follow, since it moves any complicated source control issues completely to the author of a branch, without affecting master, and potentially -breaking things for other developers. Having to modify the master branch, due to +breaking things for other developers. Having to modify the master branch, due to unintended merge state or changes having made their way into it, is a massively more complex problem than handling all conflicts and similar issues in the private local branches. ## Status -Version 0.4. That's way _before_ a 1.0. In other words, Ecstasy is about as mature as Windows 3.1 -was. +Version 0.4. That's way _before_ version 1.0. In other words, Ecstasy is about as mature as +Windows 3.1 was. **Warning:** The Ecstasy project is not yet certified for production use. This is a large and extremely ambitious project, and _it may yet be several years before this project is certified for @@ -219,7 +259,7 @@ contribute and use the project by facilitating a healthy, active community, and high-quality project. Whether you are looking to learn about language design and development, compiler technology, or the applicability of language design to the serverless cloud, we have a place for you here. Feel free to lurk. Feel free to fork the project. Feel free to contribute. - + We only "_get one chance to make a good first impression_", and we are determined not to waste it. We will not ask developers to waste their time attempting to use an incomplete project, so if you are here for a work reason, it's probably still a bit too early for you to be using this for your @@ -258,39 +298,45 @@ The project is organized as a number of subprojects, with the important ones to at `xdk/lib/ecstasy.xtc`. This module contains portions of the Ecstasy tool chain, including the lexer and parser. (Ecstasy source files use an `.x` extension, and are compiled into a single module file with an `.xtc` extension.) - -* The Java tool chain (including an Ecstasy compiler and interpreter) is located in the - [xvm/javatools](./javatools) directory. When the XDK is built, the resulting `.jar` file is + +* The Java tool chain (including an Ecstasy compiler and interpreter) is located in the + [xvm/javatools](./javatools) directory. When the XDK is built, the resulting `.jar` file is located at `xdk/javatools/javatools.jar`. - + * There is an Ecstasy library in [xvm/javatools_bridge](./javatools_bridge) that is used by the Java - interpreter to boot-strap the runtime. When the XDK is built, the resulting module is located at + interpreter to boot-strap the runtime. When the XDK is built, the resulting module is located at `xdk/javatools/javatools_bridge.xtc`. - + * The wiki documentation is [online](https://github.com/xtclang/xvm/wiki). There is an [introduction to Ecstasy](https://github.com/xtclang/xvm/wiki/lang-intro) that is being written for new users. The wiki source code will (eventually) be found in the `xvm/wiki` project directory, - and (as a distributable) in the `xdk/doc` directory of the built XDK. - + and (as a distributable) in the `xdk/doc` directory of the built XDK. + * Various other directories will have a `README.md` file that explains their purpose. To download the entire project from the terminal, you will need [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) installed. From the terminal, -go to the directory where you want to create a local copy of the Ecstasy project, and: +go to the directory where you want to create a local copy of the Ecstasy project, and: + +``` +git clone https://github.com/xtclang/xvm.git +``` - git clone https://github.com/xtclang/xvm.git - (There is excellent online documentation for git at [git-scm.com](https://git-scm.com/book/en/v2/Git-Basics-Getting-a-Git-Repository).) To build the entire project, you need to have [gradle](https://gradle.org/install/), or you use the included Gradle Wrapper from within the `xvm` directory, which is the recommended method: - ./gradlew build +``` +./gradlew build +``` -Or on Windows: +Or on Windows: - gradlew.bat build +``` +C:\> gradlew.bat build +``` Note that Windows may require the `JAVA_TOOLS_OPTIONS` environment variable to be set to `-Dfile.encoding=UTF-8` in the Environment Variables window that can be accessed from Control Panel. @@ -298,24 +344,247 @@ This allows the Java compiler to automatically handle UTF-8 encoded files, and s source files used in the Ecstasy toolchain contain UTF-8 characters. Also, to change the default encoding used in Windows, go to the "Administrative" tab of the "Region" settings Window (also accessed from Control Panel), click the "Change system locale..." button and check the box labeled -"Beta: Use UTF-8 for worldwide language support". +"Beta: Use UTF-8 for worldwide language support". Instructions for getting started can be found in our [Contributing to Ecstasy](CONTRIBUTING.md) document. +## Cleaning the build + +You can clean everything in a build by running + + ./gradlew clean + +However, note that if you restart the build, a lot of intermediary outputs will be cached. +The XDK and XTC plugin use the Gradle build system intrinsics to only compile what has provably +mutated its outputs and inputs. This should be stable, and is by design. You should really never have to +clean your build to test any incremental change. This is even true if you modify the plugin implementation +in the XDK repo, as we are using included builds everywhere we should. The end goal is that any +change will only require rebuilding, and that rebuild should only build exactly what is necessary. + +Should you, for any reason, need to clear the caches, and really start fresh, you can run the script + +./bin/purge-all-build-state.sh + +Or do the equivalent actions manually: + +1) Close any open XTC projects in your IDEs, to avoid restarting them with a large state change under the hood. + Optionally, also close your IDE processes. +2) Kill all Gradle daemons. +3) Delete the `$GRADLE_USER_HOME/cache` and `$GRADLE_USER_HOME/daemons` directories. *NOTE: this invalidates + caches for all Gradle builds on your current system, and rebuilds a new Gradle version.* +4) Run `git clean -xfd` in your build root. Note that this may also delete any IDE configuration that resides + in your build. You may want to preserve e.g. the `.idea` directory, and then you can do `git clean -xfd -e .idea` + or perform a dry run `git clean -xfdn`, to see what will be deleted. Note that if you are at this level of + purging stuff, it's likely a bad idea to hang on to your IDE state anyway. + +## Debugging the build + +The build should be debuggable through any IDE, for example IntelliJ, using its Gradle tooling API +hook. You can run any task in the project in debug mode from within the IDE, with breakpoints in +the build scripts and/or the underlying non-XTC code, for example in Javatools, to debug the +compiler, runner or disassembler. + +### Augmenting the build output + +XTC follow Gradle best practise, and you can run the build, or any task therein, with the standard +verbosity flags. For example, to run the build with more verbose output, use: + +``` +./gradlew build --info --stacktrace +``` + +The build also supports Gradle build scans, which can be generated with: + +``` +./gradlew build --scan --stacktrace +``` + +Note that build scans are published to the Gradle online build scan repository (as configured +through the `gradle-enterprise` settings plugin.), so make sure that you aren't logging any +secrets, and avoid publishing build scans in "--debug" mode, as it may be a potential security +hazard. + +You can also combine the above flags, and use all other standard Gradle flags, like `--stacktrace`, +and so on. + +### Tasks + +To see the list of available tasks for the XDK build, use: + +``` +./gradlew tasks +``` + +#### Versioning and Publishing XDK artifacts + +* Use `publishLocal`to publish an XDK build to the local Maven repository and a build specific repository directory. +* Use `publishRemote`to publish and XDK build to the xtclang organization package repo on GitHub (a GitHub token with + permissions is required). +* Use `publish` to run both of the above tasks. + +*Note*: At the moment some publish tasks may have some raciness in execution, due to Gradle issues. Should you +get some kind of error during the publishing task, it may be a good idea to clean, and then rerun that task +with the Gradle flag `--no-parallel`. + +The group and version of the current XDK build and the XTC Plugin are currently defined in +the properties file "version.properties". Here, we define the version of the current XDK +and XTC Plugin, as well as their group. The default behavior is to only define the XDK, since +at this point, the Plugin, while decoupled, tracks and maps to the XDK version pretty much 1-1. +This can be taken apart with different semantic versioning, should we need to. Nothing is assuming +the plugin has the same version or group as the XDK. It's just convenient for time being. + +The file `gradle/libs.versions.toml` contains all internal and external by-artifact version +dependencies to the XDK project. If you need to add a new plugin, library, or bundle, always define +its details in this version catalog, and nowhere else. The XDK build logic, will dynamically plugin +in values for the XDK and XTC Plugin artifacts that will be used only as references outside this file. + +*TODO*: In the future we will also support tagging and publishing releases on GitHub, using JReleaser or a +similar framework. + +Typically, the project version of anything that is unreleased should be "x.y.z-SNAPSHOT", and the first +action after tagging and uploading a release of the XDK, is usually changing the release version in +"VERSION" in the xvm repository root, and (if the plugin is versioned separately, optionally in "plugin/VERSION") +both by incrementing the micro version, and by adding a SNAPSHOT suffix. You will likely find yourself +working in branches that use SNAPSHOT versions until they have made it into a release train. The CI/CD +pipeline can very likely handle this automatically. + ## Bleeding Edge for Developers If you would like to contribute to the Ecstasy Project, it might be an idea to use the -very latest version by invoking +very latest version by invoking: + +``` +./gradlew installLocalDist +``` + +This copies the build from the xvm directory into the brew cellar, or other local installation, +that is deduced from the location of the `xec` launcher on the system PATH. + +*Note*: this would be done after installing the XDK via `brew`, or through any other installation +utility, depending on your platform. This will overwrite several libraries and files in any +local installation. + +For more information about the XTC DSL, please see the README.md file in the "plugin" project. + +### Releasing and Publishing - gradlew dist-local +This is mostly relevant to the XDK development team with release management privileges. A version +of the workflow for adding XTC releases is described [here](https://www.baeldung.com/maven-snapshot-release-repository). -This copies the build from the xvm directory into the brew cellar. +We plan to move to an automatic release model in the very near future, utilizing JRelease +(and JPackage to generate our binary launchers). As an XTC/XDK developer, you do not have +to understand all the details of the release model. The somewhat incomplete and rather +manual release mode is current described here for completeness. It will soon be replaced +with something familiar. -Note: this would be done after installing the XDK via brew. +### XDK Platform Releases + +1) Take the current version of master and create a release branch. +2) Set the VERSION in the release branch project root to reflect the version of the release. +Typically an ongoing development branch will be a "-SNAPSHOT" suffixed release, but not +an official XTC release, which just has a group:name:version number +3) Build, tag and add the release using the GitHub release plugin. + +### XDK Platform Publishing + +We have verified credentials for artifacts with the group "org.xtclang" at the best known +community portals, and will start publishing there, as soon as we have an industrial +strength release model completed. + +The current semi-manual process looks like this: + +1) ./gradlew publish to build the artifacts and verify they work. This will publish the artifacts +to a local repositories and the XTC GitHub org repository. +2) To publish the plugin to Gradle Plugin Portal: ./gradlew :plugin:publishPlugins (publish the plugin to gradlePortal) +3) To publish the XDK distro to Maven Central: (... TODO ... ) + +You can already refer to the XDK and the XTC Plugin as external artifacts for your favourite +XTC project, either by mnaually setting up a link to the XTC Org GitHub Maven Repository like this: + +``` +repositories { + maven { + url = https://maven.pkg.github.com/xtclang/xvm + credentials { + username = + token = + } +} +``` + +or by simply publishing the XDK and XDK Plugin to your mavenLocal repository, and adding +that to the configuration of your XTC project, if it's not there already: + +``` +repositories { + mavenLocal() +} +``` ## Questions? To submit a contributor agreement, sign up for very hard work, fork over a giant pile of cash, or in case of emergency: "info _at_ xtclang _dot_ org", but please -understand if we cannot respond to every email. Thank you. +understand if we cannot respond to every e-mail. Thank you. + +## Appendix: Gradle fundamentals + +We have tried very hard to create an easy-to-use build system based on industry standards +and expected behavior. These days, most software is based on the Maven/Gradle model, which +provides repositories of semantically versioned artifacts, cached incremental builds and +mature support for containerization. + +The principle of least astonishment permeates the philosophy behind the entire build system. +This means that a modern developer, should be immediately familiar with how to build and run +the XDK project, i.e. clone it from GitHub and execute "./gradlew build". It should also +import complaint free, and with dependency chains understood by any IDE that has support +for Gradle projects. "It should just work", out of the box, and should look familiar to any +developer with basic experience as a Gradle user. Nothing should require more than a single +command like to build or execute the system or anything built on top of it. + +Implementing language support for an alien language on top of Gradle, however, is a fairly +complex undertaking, and requires deeper knowledge of the Gradle architecture. It is +our firm belief, though, that the user should not have to drill down to these levels, unless he/she +specifically wants to. As it is, any open source developer today still needs to grasp some basic +fundamentals about artifacts and the Gradle build system. This is not just our assumption; it is +actually industry-wide. + +We believe the following concepts are necessary to understand, in order to work with XDK +projects or the XDK. None of them are at all specific to XTC: + +* The concept of "gradlew" and "mvnw" (or "gradlew.bat" and "mvnw.bat" on Windows) wrappers, + and why it should ALWAYS be used instead of a "gradle" binary on the local system, for any + repository that ships it with its build. +* The concept of a versioned Maven artifact, and that its descriptor "group:artifactId:version" + is its "global address", no matter how it is resolved on the lower abstraction layer. +* The concept of release vs snapshot artifact versions in the Maven model. +* The concept of local (mostly mavenLocal()) and remote artifact repositories, and how they are used + by a maven build. +* The concept of the Maven/Gradle build lifecycle, its fundamental tasks, and how they depend + on each other ("clean", "assemble", "build" and "check"). +* The concept of the Gradle/Maven cache, build daemons, and why "clean" is not what you think + of as "clean" in a C++ Makefile and why is it often better not to use it, in a cached, incrementally + built Gradle project. +* The concept of Maven/Gradle source sets, like "main", "resources" and "test". +* The concept of a Gradle build scan, and understanding how to inspect it and how to use it to + spot build issues. +* The standard flags that can be used to control Gradle debug log levels, --info, -q, --stacktrace + and so on. +* The concept of goal of self-contained software, which specifies its complete dependencies + as part of its source controlled configuration. + 1) On the Maven model level, this means semantically versioned Maven artifacts. + 2) On the software build and execution level, this also means specific versions of external + pieces of software, for example Java, NodeJS or Yarn. This also means that we CAN and SHOULD + always be able to containerize for development purposes. + +Today, it is pretty safe to assume that most open source developers who has worked on any Gradle +or Maven based project has at least the most important parts of the above knowledge. +We have spent significant architectural effort to ensure that an adopter who wants to become an +XTC or XDK user or developer does not need to acquire *any* knowledge that is +more domain specific than concepts listed above. None of these concepts are specific to the +XTC platform, but should be familiar to most software developers who have worked on projects +with Maven style build systems. + +We will also work on IDE Language support as soon as we have enough cycles to do so, which +should make getting up to speed with XTC and even less complicated process. diff --git a/VERSION b/VERSION new file mode 100644 index 0000000000..17b2ccd9bf --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.4.3 diff --git a/bin/check-docker-version.sh b/bin/check-docker-version.sh new file mode 100755 index 0000000000..f1834c5d2f --- /dev/null +++ b/bin/check-docker-version.sh @@ -0,0 +1,13 @@ +#!/bin/sh + +# Minimum required Docker version. Also supportes dot versions, and not just major versions. +required_version="24.0" +docker_version=$(docker version --format '{{.Server.Version}}') + +if [[ "$(printf '%s\n' "$required_version" "$docker_version" | sort -V | head -n1)" != "$required_version" ]]; then + echo "Required Docker version >= $required_version is required to build." + echo "Current Docker version is $docker_version" + exit 1 +else + echo "Docker version is recent enough for build: $docker_version" +fi diff --git a/bin/check-github-token-privileges.sh b/bin/check-github-token-privileges.sh new file mode 100755 index 0000000000..f146be1347 --- /dev/null +++ b/bin/check-github-token-privileges.sh @@ -0,0 +1,13 @@ +#!/bin/sh + +# Helper script that prints the privileges associated with a GitHub token. +# This can be useful if you want to expose a token to a non-private group, so that +# it can be programmatically or manually double checked whether it only had the +# intended privileges. For example: no write privileges, and read:package only. + +token=$1 +if [ -z $token ]; then + echo "Missing argument: GitHub token to inspect." +else + curl -sS -f -I -H "Authorization: token $token" https://api.github.com | grep -i x-oauth-scopes +fi diff --git a/bin/git-show-parent-branch.sh b/bin/git-show-parent-branch.sh new file mode 100755 index 0000000000..a2a6d830e1 --- /dev/null +++ b/bin/git-show-parent-branch.sh @@ -0,0 +1,8 @@ +#!/bin/sh + +git show-branch -a \ +| sed "s/].*//" \ +| grep "\*" \ +| grep -v "$(git rev-parse --abbrev-ref HEAD)" \ +| head -n1 \ +| sed "s/^.*\[//" diff --git a/bin/gradle-update-wrapper.sh b/bin/gradle-update-wrapper.sh new file mode 100755 index 0000000000..b5eb336cff --- /dev/null +++ b/bin/gradle-update-wrapper.sh @@ -0,0 +1,9 @@ +#!/bin/sh + +wrapper_version=$1 +if [ -z $wrapper_version ]; then + echo "No version argument suppied." +else + root_dir=$(git rev-parse --abbrev-ref HEAD) + $root_dir/gradlew --gradle-version $wrapper_version +fi diff --git a/bin/purge-all-build-state.sh b/bin/purge-all-build-state.sh new file mode 100755 index 0000000000..b531d8c64e --- /dev/null +++ b/bin/purge-all-build-state.sh @@ -0,0 +1,28 @@ +#!/bin/bash -x + +echo "WARNING: This script will clear all caches and kill all processes with build state." +read -p "Are you sure? " -n 1 -r +if [[ $REPLY =~ ^[Yy]$ ]] +then + echo "Killing all running daemons..." + jps | grep Gradle | awk {'print $1'} | xargs kill -9 + jps | grep Kotlin | awk {'print $1'} | xargs kill -9 + + git_root=$(git rev-parse --show-toplevel) + echo "Running git clean on: $git_root (sparing .idea directory, you may want to delete it manually)." + pushd $git_root + git clean -xfd -e .idea + popd + + echo "Deleting remaining build folders..." + find $git_root -name build -type d | grep -v src | xargs rm -rf + + echo "Deleting build and daemon cache from GRADLE_USER_HOME" + rm -fr $HOME/.gradle/caches + rm -fr $HOME/.gradle/daemons + + echo "Deleting local maven repository" + rm -fr $HOME/.m2/repository + + echo "Purged." +fi diff --git a/build-logic/aggregator/build.gradle.kts b/build-logic/aggregator/build.gradle.kts new file mode 100644 index 0000000000..e4a377cb50 --- /dev/null +++ b/build-logic/aggregator/build.gradle.kts @@ -0,0 +1,8 @@ +plugins { + `kotlin-dsl` +} + +repositories { + gradlePluginPortal() + mavenCentral() +} diff --git a/build-logic/aggregator/settings.gradle.kts b/build-logic/aggregator/settings.gradle.kts new file mode 100644 index 0000000000..fdb6989376 --- /dev/null +++ b/build-logic/aggregator/settings.gradle.kts @@ -0,0 +1,9 @@ +pluginManagement { + includeBuild("../settings-plugins") +} + +plugins { + id("org.xtclang.build.common") +} + +rootProject.name = "aggregator" diff --git a/build-logic/aggregator/src/main/kotlin/org.xtclang.build.aggregator.gradle.kts b/build-logic/aggregator/src/main/kotlin/org.xtclang.build.aggregator.gradle.kts new file mode 100644 index 0000000000..7044ced4d9 --- /dev/null +++ b/build-logic/aggregator/src/main/kotlin/org.xtclang.build.aggregator.gradle.kts @@ -0,0 +1,61 @@ +import org.gradle.language.base.plugins.LifecycleBasePlugin.ASSEMBLE_TASK_NAME +import org.gradle.language.base.plugins.LifecycleBasePlugin.BUILD_GROUP +import org.gradle.language.base.plugins.LifecycleBasePlugin.BUILD_TASK_NAME +import org.gradle.language.base.plugins.LifecycleBasePlugin.CHECK_TASK_NAME +import org.gradle.language.base.plugins.LifecycleBasePlugin.CLEAN_TASK_NAME + +plugins { + base +} + +private class XdkBuildAggregator(project: Project) : Runnable { + companion object { + private val lifeCycleTasks = listOfNotNull(ASSEMBLE_TASK_NAME, BUILD_TASK_NAME, CHECK_TASK_NAME, CLEAN_TASK_NAME) + } + + private val prefix = "[${project.name}]" + + override fun run() { + logger.info("$prefix Aggregating included build tasks:") + gradle.includedBuilds.forEachIndexed { i, includedBuild -> + logger.info("$prefix Included build #$i: ${includedBuild.name} [project dir: ${includedBuild.projectDir}]") + } + + checkStartParameterState() + aggregateLifeCycleTasks() + } + + private fun aggregateLifeCycleTasks() { + lifeCycleTasks.forEach { taskName -> + logger.info("$prefix Creating aggregated lifecycle task: ':$taskName' in project '${project.name}'") + tasks.named(taskName) { + group = BUILD_GROUP + description = "Aggregates and executes the '$taskName' task for all included builds." + gradle.includedBuilds.forEach { includedBuild -> + dependsOn(includedBuild.task(":$taskName")) + logger.info("$prefix Attaching: dependsOn(':$name' <- ':${includedBuild.name}:$name')") + } + } + } + } + + private fun checkStartParameterState() { + val startParameter = gradle.startParameter + with (startParameter) { + logger.info(""" + $prefix Start parameter tasks: $taskNames + $prefix Start parameter init scripts: $allInitScripts + """.trimIndent()) + + if (taskNames.count { !it.startsWith("-") && !it.contains("taskTree") } > 1) { + val msg = "$prefix Multiple start parameter tasks are not guaranteed to in order/in parallel. Please run each task individually." + logger.error(msg) + throw GradleException(msg) + } + } + + logger.info("$prefix Start Parameter(s): $startParameter") + } +} + +XdkBuildAggregator(project).run() diff --git a/build-logic/common-plugins/common-plugins.gradle.kts b/build-logic/common-plugins/common-plugins.gradle.kts new file mode 100644 index 0000000000..a0c7637e75 --- /dev/null +++ b/build-logic/common-plugins/common-plugins.gradle.kts @@ -0,0 +1,23 @@ +plugins { + `kotlin-dsl` +} + +dependencies { + implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:$embeddedKotlinVersion") + + // TODO: Figure out how to get this to live in the version catalog instead. Since + // build logic for talking with GitHub needs this, and because that is compiled + // as a build-logic plugin, we are too early in the lifecycle to resolve from the + // version catalog. Not even with the "best practice hacks", that are mostly applicable + // for this. + val kohttpVersion = "0.12.0" + implementation("io.github.rybalkinsd:kohttp:$kohttpVersion") + implementation("io.github.rybalkinsd:kohttp-jackson:$kohttpVersion") +} + +repositories { + mavenCentral() + gradlePluginPortal() +} + +logger.info("[${project.name}] Gradle version: v${gradle.gradleVersion} (embedded Kotlin: v$embeddedKotlinVersion).") diff --git a/build-logic/common-plugins/settings.gradle.kts b/build-logic/common-plugins/settings.gradle.kts new file mode 100644 index 0000000000..a138c1a405 --- /dev/null +++ b/build-logic/common-plugins/settings.gradle.kts @@ -0,0 +1,6 @@ +pluginManagement { + includeBuild("../settings-plugins") +} + +rootProject.name = "common-plugins" +rootProject.buildFileName = "common-plugins.gradle.kts" diff --git a/build-logic/common-plugins/src/main/kotlin/DebugBuild.kt b/build-logic/common-plugins/src/main/kotlin/DebugBuild.kt new file mode 100644 index 0000000000..b11745d35e --- /dev/null +++ b/build-logic/common-plugins/src/main/kotlin/DebugBuild.kt @@ -0,0 +1,232 @@ +import org.gradle.api.Project +import org.gradle.api.Task +import org.gradle.api.artifacts.Configuration +import org.gradle.api.artifacts.repositories.MavenArtifactRepository +import org.gradle.api.logging.LogLevel +import org.gradle.api.logging.LogLevel.LIFECYCLE +import org.gradle.api.publish.PublishingExtension +import org.gradle.api.publish.maven.MavenPublication +import org.gradle.kotlin.dsl.findByType +import org.gradle.kotlin.dsl.withType +import java.io.File +import java.net.URI +import java.util.Enumeration +import java.util.jar.JarEntry +import java.util.jar.JarFile +import kotlin.collections.Collection +import kotlin.collections.List +import kotlin.collections.Set +import kotlin.collections.count +import kotlin.collections.filter +import kotlin.collections.forEach +import kotlin.collections.forEachIndexed +import kotlin.collections.map +import kotlin.collections.mutableMapOf +import kotlin.collections.set +import kotlin.collections.toSet + +/** + * Simple helper functions, mostly printing out dependencies, inputs and outputs and other + * useful information for an XDK build, that is a bit too brief to deserve tasks of its own. + * + * They are wrapped in helper tasks. + */ +data class RepositoryData(val name: String, val url: URI) + +fun Project.printAllTaskInputs(level: LogLevel = LIFECYCLE) { + tasks.forEach { printTaskInputs(level, it.name) } +} + +fun Project.printAllTaskOutputs(level: LogLevel = LIFECYCLE) { + tasks.forEach { printTaskInputs(level, it.name) } +} + +fun Project.printAllTaskDependencies(level: LogLevel = LIFECYCLE) { + tasks.forEach { printTaskDependencies(level, it.name) } +} + +fun Task.printTaskInputs(level: LogLevel = LIFECYCLE) { + return project.printTaskInputs(level, name) +} + +fun Task.printTaskOutputs(level: LogLevel = LIFECYCLE) { + return project.printTaskOutputs(level, name) +} + +fun Task.printTaskDependencies(level: LogLevel = LIFECYCLE) { + return project.printTaskDependencies(level, name) +} + +@Suppress("unused") +fun Project.printRepos(level: LogLevel = LIFECYCLE) { + repositories.map { it.name }.forEach { + logger.log(level, "$prefix Repository: '$it'") + } +} + +fun Project.printMavenRepos(level: LogLevel = LIFECYCLE): Int { + val mavenRepos = repositories.withType().map { RepositoryData(it.name, it.url) } + mavenRepos.forEach { + logger.log(level, "$prefix Maven Repository: ${it.name} ('${it.url}')") + } + if (mavenRepos.isEmpty()) { + logger.log(level, "$prefix No Maven repositories found.") + } + return mavenRepos.size +} + +fun Project.printResolvedConfigFiles(level: LogLevel = LIFECYCLE, configNames: Collection) { + configNames.forEach { printResolvedConfigFile(level, it) } +} + +fun Project.printAllResolvedConfigFiles(level: LogLevel = LIFECYCLE) { + return printResolvedConfigFiles(level, project.configurations.map { it.name }) +} + +fun Project.checkTask(taskName: String): Task { + return project.tasks.findByName(taskName) ?: throw buildException("No task named '$taskName' found.") +} + +fun Project.printResolvedConfigFiles(level: LogLevel = LIFECYCLE, configName: String) { + // This only works on resolved configurations, and after the configuration phase. + val config = configurations.getByName(configName) + if (!config.isCanBeResolved) { + logger.warn("$prefix Configuration '$configName' is not resolvable.") + return + } + val files = config.resolvedConfiguration.resolvedArtifacts.map { it.file } + logger.log(level, "$prefix Configuration '$configName' has ${files.size} files:") + files.forEach { + logger.log(level, "$prefix file: '$it'") + } +} + +fun Project.printTaskInputs(level: LogLevel = LIFECYCLE, taskName: String) { + val task = tasks.getByName(taskName) + val inputs = task.inputs.files + logger.log(level, "$prefix Task '$taskName' has ${inputs.count()} inputs:") + inputs.forEach { logger.log(level, "$prefix input: '$it' (type: ${it.javaClass.name})") } + //inputs.asFileTree.forEach { logger.log(level, "$prefix input : '$it'") } +} + +fun Project.printTaskOutputs(level: LogLevel = LIFECYCLE, taskName: String) { + val task = tasks.getByName(taskName) + val outputs = task.outputs.files + logger.log(level, "$prefix Task '$taskName' has ${outputs.count()} outputs:") + outputs.forEach { logger.log(level, "$prefix output: '$it' (type: ${it.javaClass.name})") } + //outputs.asFileTree.forEach { logger.log(level, "$prefix output: '$it'") } +} + +fun Project.printResolvedConfigFile(level: LogLevel = LIFECYCLE, configName: String) { + // This only works on resolved configurations, and after the configuration phase. + val files = DebugBuild.resolvableConfig(project, configName) + files?.resolvedConfiguration?.resolvedArtifacts?.map { it.file }?.also { f -> + logger.log(level, "$prefix Configuration '$configName' has ${f.size} files:") + f.forEach { + logger.log(level, "$prefix Path: '${it.absolutePath}'") + } + } +} + +fun Project.printTaskDependencies(level: LogLevel = LIFECYCLE, taskName: String) { + // NOTE: Calling this method from a task action is not supported when configuration caching is enabled. + val task = checkTask(taskName) + val projectName = project.name + + logger.log(level, "$prefix $projectName.printTaskDependencies('$taskName'):") + + val parents = task.taskDependencies.getDependencies(task).toSet() + logger.log(level, "$prefix Task '$projectName:$taskName' depends on ${parents.size} other tasks.") + parents.forEach { + logger.log(level, "$prefix Task '$projectName:$taskName' <- dependsOn: '${it.project.name}:${it.name}'") + } + val children = project.tasks.filter { + var match = false // TODO: Better kotlin. + for (d in it.dependsOn) { + if (d.toString() == taskName) { + match = true + break + } + } + match + }.toSet() + logger.log(level, "$prefix Task '$projectName:$taskName' is a dependency of ${children.size} other tasks.") + children.forEach { + logger.log(level, "$prefix Task '$projectName:$taskName' -> isDependencyOf: '$projectName:${it.name}'") + } +} + +fun Project.printPublications(level: LogLevel = LIFECYCLE) { + val publicationContainer: PublishingExtension? = project.extensions.findByType() + if (publicationContainer == null) { + logger.warn("$prefix Does not declare any publications. Task has no effect.") + return + } + + val projectName = project.name + val publications = publicationContainer.publications + if (publications.isEmpty()) { + logger.log(level, "$prefix Project has no declared publications.") + } + val count = publications.size + logger.log(level, "$prefix Project '$projectName' has $count publications.") + publications.forEachIndexed { i, it -> + logger.log(level, "$prefix (${i + 1} / $count) Publication: '$projectName:${it.name}' (type: ${it::class}") + if (it is MavenPublication) { + logger.log(level, "$prefix Publication '${projectName}.${it.name}' has ${it.artifacts.size} artifacts.") + it.artifacts.forEachIndexed { j, artifact -> + logger.log(level, "$prefix (${j + 1} / ${it.artifacts.size}) Artifact: '$artifact'") + } + } + } +} + +/** + * Sanity checker for jar files. Triggers a build error if specified path elements are + * not present in the jar file, and/or if the number of entries in the jar file was + * not equal to an optional specified size. + */ +class DebugBuild(project: Project) : XdkProjectBuildLogic(project) { + companion object { + fun verifyJarFileContents(project: Project, required: List, size: Int = -1) { + val jar = project.tasks.getByName("jar").outputs.files.singleFile + val contents = jarContents(jar) + + // TODO: Very hacky sanity check verification. Need to keep this updated or remove it when we are confident artifact creation is race free + if (size >= 0 && contents.size != size) { + throw project.buildException("ERROR: Expected '$jar' to contain $size entries (was: ${contents.size})") + } + + required.forEach { + fun matches(contents: Collection, match: String): Boolean { + // Get all keys with match in them, but without '$' in them (inner classes do not count) + return contents.filter { name -> name.contains(match) && !name.contains("$") }.size == 1 + } + if (!matches(contents, it)) { + throw project.buildException("ERROR: Corrupted jar file; needs to contain entry matching '$it'") + } + } + } + + private fun jarContents(jarFile: File): Set { + val contents = mutableMapOf() + JarFile(jarFile).use { jar -> + val enumEntries: Enumeration = jar.entries() + while (enumEntries.hasMoreElements()) { + val entry: JarEntry = enumEntries.nextElement() as JarEntry + contents[entry.name] = entry.size + } + } + return contents.keys + } + + fun resolvableConfig(project: Project, configName: String): Configuration? { + val config = project.configurations.getByName(configName) + if (!config.isCanBeResolved) { + project.logger.warn("${project.prefix} Configuration '$configName' is not resolvable. Skipped.") + return null + } + return config + } + } +} diff --git a/build-logic/common-plugins/src/main/kotlin/GitHubPackages.kt b/build-logic/common-plugins/src/main/kotlin/GitHubPackages.kt new file mode 100644 index 0000000000..bb2e27b721 --- /dev/null +++ b/build-logic/common-plugins/src/main/kotlin/GitHubPackages.kt @@ -0,0 +1,163 @@ +import XdkPropertiesImpl.Companion.REDACTED +import com.fasterxml.jackson.databind.JsonNode +import io.github.rybalkinsd.kohttp.dsl.context.Method +import io.github.rybalkinsd.kohttp.dsl.context.Method.DELETE +import io.github.rybalkinsd.kohttp.dsl.context.Method.GET +import io.github.rybalkinsd.kohttp.dsl.http +import io.github.rybalkinsd.kohttp.jackson.ext.toJson +import okhttp3.Response +import org.gradle.api.Project +import kotlin.io.encoding.Base64 +import kotlin.io.encoding.ExperimentalEncodingApi + +/** + * Helper class to access GitHub packages for the "xtclang" org, and other build logic + * for publishing XDK build artifacts. + */ +class GitHubPackages(project: Project) : XdkProjectBuildLogic(project) { + companion object Protocol { + private const val GITHUB_HOST = "api.github.com" + private const val GITHUB_SCHEME = "https" + private const val GITHUB_JSON_PACKAGE_NAME = "name" + + private const val GITHUB_PREFIX = "org.xtclang.repo.github" + private const val GITHUB_ORG = "$GITHUB_PREFIX.org" + private const val GITHUB_USER = "$GITHUB_PREFIX.user" + private const val GITHUB_TOKEN = "$GITHUB_PREFIX.token" + private const val GITHUB_URL = "$GITHUB_PREFIX.url" + private const val GITHUB_READONLY = "$GITHUB_PREFIX.readonly" + + private const val GITHUB_URL_DEFAULT_VALUE = "https://maven.pkg.github.com/xtclang" + private const val GITHUB_ORG_DEFAULT_VALUE = "xtclang" + private const val GITHUB_USER_RO_DEFAULT_VALUE = "xtclang-bot" + private const val GITHUB_TOKEN_RO_DEFAULT_VALUE = "Z2hwX0ZjNGRWeDhNYmxPcnZDYWZrRW96Q0NrQXAzaVZ5RjBUb0NheAo=" + + val publishTaskPrefixes = listOfNotNull("list", "delete") + val publishTaskSuffixesRemote = listOfNotNull("AllRemotePublications") + val publishTaskSuffixesLocal = listOfNotNull("AllLocalPublications") + + fun restHeaders(token: String): List> = listOfNotNull( + "Accept" to "application/vnd.github+json", + "X-GitHub-Api-Version" to "2022-11-28", + "Authorization" to "Bearer $token") + } + + private val org: String + private val packagesUrl: String + private val credentials: Pair + + init { + with(project) { + val user = getXdkProperty(GITHUB_USER, GITHUB_USER_RO_DEFAULT_VALUE) + val ro = getXdkPropertyBoolean(GITHUB_READONLY) || user == GITHUB_USER_RO_DEFAULT_VALUE + credentials = user to (if (ro) decodeToken(GITHUB_TOKEN_RO_DEFAULT_VALUE) else getXdkProperty(GITHUB_TOKEN, "")) + packagesUrl = getXdkProperty(GITHUB_URL, GITHUB_URL_DEFAULT_VALUE) + org = getXdkProperty(GITHUB_ORG, GITHUB_ORG_DEFAULT_VALUE) + } + } + + @OptIn(ExperimentalEncodingApi::class) + private fun decodeToken(str: String): String { + return runCatching { Base64.decode(str).toString(Charsets.UTF_8).trim() }.getOrDefault("") + } + + val uri: String get() = packagesUrl + + val user: String get() = credentials.first + + val token: String get() = credentials.second + + val organization: String get() = this.org + + val isReadOnly: Boolean get() = + user == GITHUB_TOKEN_RO_DEFAULT_VALUE && token == decodeToken(GITHUB_TOKEN_RO_DEFAULT_VALUE) + + fun queryXtcLangPackageNames(): List { + return buildList { + val (_, json) = restCall( + GET, + "/orgs/$org/packages", + "package_type" to "maven" + ) + json?.forEach { node -> node[GITHUB_JSON_PACKAGE_NAME]?.asText()?.also { add(it) } } + }.filter { + it.contains(project.group.toString()) && it.contains(project.name) + } + } + + fun queryXtcLangPackageVersions(packageName: String): List { + val (_, json) = restCall(GET, "/orgs/$org/packages/maven/$packageName/versions") + return buildList { + json?.forEach { node -> node[GITHUB_JSON_PACKAGE_NAME]?.asText()?.also { add(it) } } + } + } + + fun deleteXtcLangPackages(): Int { + val packageNames = queryXtcLangPackageNames() + if (packageNames.isEmpty()) { + logger.warn("$prefix No Maven packages found to delete.") + return 0 + } + packageNames.forEach { + deleteXtcLangPackage(it) + } + return packageNames.size + } + + private fun deleteXtcLangPackage(packageName: String) { + logger.lifecycle("$prefix Deleting package: '$packageName'") + restCall(DELETE, "/orgs/$org/packages/maven/$packageName") + } + + fun verifyGitHubConfig(): Boolean { + val (user, token) = credentials + val hasGitHubUser = user.isNotEmpty() + val hasGitHubToken = token.isNotEmpty() + val hasGitHubUrl = uri.isNotEmpty() + val hasGitHubConfig = hasGitHubUser && hasGitHubToken && hasGitHubUrl + if (!hasGitHubConfig) { + logger.warn( + """ + $prefix GitHub credentials are not completely set; publication to GitHub will be disabled. + $prefix '$GITHUB_PREFIX.url' [configured: $hasGitHubUrl ($uri)] + $prefix '$GITHUB_PREFIX.user' [configured: $hasGitHubUser ($user)] + $prefix '$GITHUB_PREFIX.token' [configured: $hasGitHubToken ($REDACTED)] + $prefix '$GITHUB_PREFIX.readonly' [configured: ${isReadOnly}] + """.trimIndent() + ) + return false + } + + logger.info("$prefix Checking GitHub repo URL: '$uri'") + if (uri != uri.lowercase()) { + throw project.buildException("The repository URL '$uri' needs to contain all-lowercase owner and repository names.") + } + + logger.info("$prefix GitHub credentials appear to be well-formed. (user: '$user')") + return true + } + + private fun restCall(mtd: Method, httpPath: String, vararg params: Pair): Pair { + return http(method = mtd) { + scheme = GITHUB_SCHEME + host = GITHUB_HOST + path = httpPath + header { + val token = credentials.second + if (token.isEmpty()) { + throw project.buildException("Could not resolve an access token for GitHub from the properties and/or environment.") + } + restHeaders(token).forEach { (k, v) -> k to v } + } + param { + params.forEach { (k, v) -> k to v } + } + }.use { + logger.info("$prefix REST $mtd response status code: ${it.code()}") + if (!it.isSuccessful) { + throw project.buildException("REST $mtd response not successful: $it (code: ${it.code()})") + } + it to runCatching { it.toJson() }.getOrNull() + } + } +} diff --git a/build-logic/common-plugins/src/main/kotlin/SemanticVersion.kt b/build-logic/common-plugins/src/main/kotlin/SemanticVersion.kt new file mode 100644 index 0000000000..c8f268b14c --- /dev/null +++ b/build-logic/common-plugins/src/main/kotlin/SemanticVersion.kt @@ -0,0 +1,46 @@ +import kotlin.IllegalArgumentException + +data class SemanticVersion(val artifactGroup: String, val artifactId: String, val artifactVersion: String) { + + override fun toString(): String { + return "$artifactGroup:$artifactId:$artifactVersion" + } + + /** + * Increase the microVersion with one, and potentially add or remove a SNAPSHOT suffix from + * this semantic version. + * + * @return New SemanticVersion, as SemanticVersion objects are immutable. + */ + fun bump(toSnapshot: Boolean = true): SemanticVersion { + return run { + val lastDot = artifactVersion.lastIndexOf('.') + if (lastDot == -1) { + throw IllegalArgumentException("Illegal version format: '$artifactVersion'") + } + val majorMinorVersion = artifactVersion.substring(0, lastDot) + val microVersionFull = artifactVersion.substring(lastDot + 1) + val microVersion = microVersionFull.trim { !it.isDigit() } + val nextMicroVersion = microVersion.toInt() + 1 + if (isSnapshot()) { + throw IllegalArgumentException("Bumping semantic version that is already a snapshot. Is this intentional?") + } + val next = SemanticVersion( + artifactGroup, + artifactId, + buildString { + append(majorMinorVersion) + append('.') + append(nextMicroVersion) + if (toSnapshot) { + append("-SNAPSHOT") + } + }) + next + } + } + + fun isSnapshot(): Boolean { + return artifactVersion.endsWith("-SNAPSHOT") + } +} diff --git a/build-logic/common-plugins/src/main/kotlin/XdkBuildLogic.kt b/build-logic/common-plugins/src/main/kotlin/XdkBuildLogic.kt new file mode 100644 index 0000000000..8d53d2d234 --- /dev/null +++ b/build-logic/common-plugins/src/main/kotlin/XdkBuildLogic.kt @@ -0,0 +1,236 @@ +import org.gradle.api.GradleException +import org.gradle.api.Project +import org.gradle.api.Task +import org.gradle.api.file.Directory +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.ProjectLayout +import org.gradle.api.invocation.Gradle +import org.gradle.api.logging.LogLevel +import org.gradle.api.logging.LogLevel.LIFECYCLE +import org.gradle.api.provider.Provider +import java.io.ByteArrayOutputStream +import java.io.File +import java.nio.file.Path +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +abstract class XdkProjectBuildLogic(protected val project: Project) { + protected val logger = project.logger + protected val prefix = project.prefix + + override fun toString(): String { + return this::class.simpleName?.let { "$it('${project.name}')" } ?: throw IllegalStateException("Unknown class: ${this::class}") + } +} + +class XdkBuildLogic private constructor(project: Project) : XdkProjectBuildLogic(project) { + private val xdkGitHub: GitHubPackages by lazy { + GitHubPackages(project) + } + + private val xdkVersions: XdkVersionHandler by lazy { + XdkVersionHandler(project) + } + + private val xdkDistributions: XdkDistribution by lazy { + XdkDistribution(project) + } + + private val xdkProperties: XdkProperties by lazy { + logger.info("$prefix Created lazy XDK Properties for project ${project.name}") + XdkPropertiesImpl(project) + } + + fun props(): XdkProperties { + return xdkProperties + } + + fun versions(): XdkVersionHandler { + return xdkVersions + } + + fun distro(): XdkDistribution { + return xdkDistributions + } + + fun github(): GitHubPackages { + return xdkGitHub + } + + fun resolveLocalXdkInstallation(): File { + return findLocalXdkInstallation() ?: throw project.buildException("Could not find local installation of XVM.") + } + + companion object { + const val DEFAULT_JAVA_BYTECODE_VERSION = 20 // TODO: We still have to compile to 20 bytecode, because Kotlin 1.9 does not support 21. + const val XDK_TASK_GROUP_DEBUG = "debug" + const val XDK_ARTIFACT_NAME_DISTRIBUTION_ARCHIVE = "xdk-distribution-archive" + const val XDK_ARTIFACT_NAME_JAVATOOLS_FATJAR = "javatools-fatjar" + const val XDK_ARTIFACT_NAME_MACK_DIR = "mack-dir" + + private const val ENV_PATH = "PATH" + private const val XTC_LAUNCHER = "xec" + private const val DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss.SSS" + + private val singletonCache: MutableMap = mutableMapOf() + + fun instanceFor(project: Project): XdkBuildLogic { + if (singletonCache.contains(project)) { + return singletonCache[project]!! + } + + val instance = XdkBuildLogic(project) + singletonCache[project] = instance + project.logger.info( + """ + ${project.prefix} Creating new XdkBuildLogic for project '${project.name}' + ${project.prefix} (singletonCache) ${System.identityHashCode(singletonCache)} + ${project.prefix} (project -> instance) ${System.identityHashCode(project)} -> ${System.identityHashCode(instance)} + """.trimIndent()) + return instance + } + + fun getDateTimeStampWithTz(ms: Long = System.currentTimeMillis()): String { + return SimpleDateFormat(DATE_TIME_FORMAT, Locale.getDefault()).format(Date(ms)) + } + + fun findExecutableOnPath(executable: String): Path? { + return System.getenv(ENV_PATH)?.split(File.pathSeparator)?.map { File(it, executable) } + ?.find { it.exists() && it.canExecute() }?.toPath()?.toRealPath() + } + + fun findLocalXdkInstallation(): File? { + return findExecutableOnPath(XTC_LAUNCHER)?.toFile()?.parentFile?.parentFile?.parentFile // xec -> bin -> libexec -> "x.y.z.ppp" + } + + /** + * Generate listing of a directory tree (recursively) with modification timestamps + * for all its files. This is useful, e.g. for making sure that an installation was + * updated, and, e.g., not erroneously considered as cached or some other hard-to-debug + * scenario like that. + */ + fun listDirWithTimestamps(dir: File): String { + val truncate = dir.absolutePath + return buildString { + appendLine("Recursively listing '$dir' with modification timestamps:") + dir.walkTopDown().forEach { + assert(it.absolutePath.startsWith(truncate)) + val path = it.absolutePath.substring(truncate.length) + val timestamp = getDateTimeStampWithTz(it.lastModified()) + appendLine(" [$timestamp] '${dir.name}$path'") + } + }.trim() + } + } +} + +// TODO: Can we move these guys to the versions handler? +val Gradle.rootGradle: Gradle + get() { + var dir: Gradle? = this + while (dir!!.parent != null) { + dir = dir.parent + } + return dir + } + +val Gradle.rootLayout: ProjectLayout get() = rootGradle.rootProject.layout + +val Project.compositeRootProjectDirectory: Directory get() = gradle.rootLayout.projectDirectory + +val Project.compositeRootBuildDirectory: DirectoryProperty get() = gradle.rootLayout.buildDirectory + +val Project.userInitScriptDirectory: File get() = File(gradle.gradleUserHomeDir, "init.d") + +val Project.buildRepoDirectory get() = compositeRootBuildDirectory.dir("repo") + +val Project.xdkBuildLogic: XdkBuildLogic get() = XdkBuildLogic.instanceFor(this) + +val Project.prefix: String get() = "[$name]" + +val Task.prefix: String get() = "[${project.name}:$name]" + +// TODO: A little bit hacky: use a config, but there is a mutual dependency between the lib_xtc and javatools. +// Better to add the resource directory as a source set? +val Project.xdkIconFile: String get() = "$compositeRootProjectDirectory/javatools_launcher/src/main/c/x.ico" + +// TODO: A little bit hacky, for same reason as above; Better to add the resource directory as a source set? +val Project.xdkImplicitsPath: String get() = "$compositeRootProjectDirectory/lib_ecstasy/src/main/resources/implicit.x" + +val Project.xdkImplicitsFile: File get() = File(xdkImplicitsPath) + +fun Project.executeCommand(vararg args: String): String = project.run { + val output = ByteArrayOutputStream() + val result = project.exec { + commandLine(*args) + standardOutput = output + isIgnoreExitValue = false + } + if (result.exitValue != 0) { + logger.error("$prefix ERROR: Command '${args.joinToString(" ")}' failed with exit code ${result.exitValue}") + return "" + } + return output.toString().trim() +} + +// TODO these should probably be lazy for input purposes + +fun Project.isXdkPropertySet(key: String): Boolean { + return xdkBuildLogic.props().has(key) +} + +fun Project.getXdkPropertyBoolean(key: String, defaultValue: Boolean? = null): Boolean { + return xdkBuildLogic.props().get(key, defaultValue) +} + +fun Project.getXdkPropertyInt(key: String, defaultValue: Int? = null): Int { + return xdkBuildLogic.props().get(key, defaultValue) +} + +fun Project.getXdkProperty(key: String, defaultValue: String? = null): String { + return xdkBuildLogic.props().get(key, defaultValue) +} + +private fun registerXdkPropertyInput(task: Task, key: String, value: T): T { + with(task) { + logger.info("$prefix Task tunneling property for $key to project. Can be set as input provider.") + } + return value +} + +fun Task.getXdkPropertyBoolean(key: String, defaultValue: Boolean? = null): Boolean { + return registerXdkPropertyInput(this, key, project.getXdkPropertyBoolean(key, defaultValue)) +} + +fun Task.getXdkPropertyInt(key: String, defaultValue: Int? = null): Int { + return registerXdkPropertyInput(this, key, project.getXdkPropertyInt(key, defaultValue)) +} + +fun Task.getXdkProperty(key: String, defaultValue: String? = null): String { + return registerXdkPropertyInput(this, key, project.getXdkProperty(key, defaultValue)) +} + +fun Project.buildException(msg: String, level: LogLevel = LIFECYCLE): Throwable { + val prefixed = "$prefix $msg" + logger.log(level, prefixed) + return GradleException(prefixed) +} + +/** + * Extension method that can be called during the configuration phase, marking its + * task instance as forever out of date. + */ +fun Task.considerNeverUpToDate() { + outputs.cacheIf { false } + outputs.upToDateWhen { false } + logger.info("${project.prefix} WARNING: Task '${project.name}:$name' is configured to always be treated as out of date, and will be run. Do not include this as a part of the normal build cycle...") +} + +/** + * Extension method to flag a task as always up to date. Declaring no outputs will + * cause a task to rerun, even an extended task. + */ +fun Task.considerAlwaysUpToDate() { + outputs.upToDateWhen { true } +} diff --git a/build-logic/common-plugins/src/main/kotlin/XdkDistribution.kt b/build-logic/common-plugins/src/main/kotlin/XdkDistribution.kt new file mode 100644 index 0000000000..ecb34b030f --- /dev/null +++ b/build-logic/common-plugins/src/main/kotlin/XdkDistribution.kt @@ -0,0 +1,79 @@ +import XdkBuildLogic.Companion.getDateTimeStampWithTz +import org.gradle.api.Project +import org.gradle.api.file.Directory +import org.gradle.api.file.RegularFile +import org.gradle.api.provider.Provider +import org.gradle.internal.os.OperatingSystem + +class XdkDistribution(project: Project): XdkProjectBuildLogic(project) { + companion object { + const val DISTRIBUTION_TASK_GROUP = "distribution" + const val MAKENSIS = "makensis" + + private const val BUILD_NUMBER = "BUILD_NUMBER" + private const val CI = "CI" + private const val LOCALDIST_BACKUP_DIR = "localdist-backup" + + private val currentOs = OperatingSystem.current() + private val isCiEnabled = System.getenv(CI) == "true" + + val distributionTasks = listOfNotNull("distTar", "distZip", "distExe") + } + + init { + logger.info(""" + $prefix Configuring XVM distribution: '$this' + $prefix Name : '$distributionName' + $prefix Version : '$distributionVersion' + $prefix Current OS : '$currentOs' + $prefix Environment: + $prefix CI : '$isCiEnabled' (CI property can be overwritten) + $prefix GITHUB_ACTIONS : '${System.getenv("GITHUB_ACTIONS") ?: "[not set]"}' + """.trimIndent()) + } + + val distributionName: String get() = project.name // Default: "xdk" + + val distributionVersion: String get() = buildString { + append(project.version) + if (isCiEnabled) { + val buildNumber = System.getenv(BUILD_NUMBER) ?: "" + val gitCommitHash = project.executeCommand("git", "rev-parse", "HEAD") + if (buildNumber.isNotEmpty() || gitCommitHash.isNotEmpty()) { + logger.warn("This is a CI run, BUILD_NUMBER and git hash must both be available: (BUILD_NUMBER='$buildNumber', commit='$gitCommitHash')") + return@buildString + } + append("-ci-$buildNumber+$gitCommitHash") + } + } + + fun resolveLauncherFile(localDistDir: Provider): RegularFile { + val launcher = if (currentOs.isMacOsX) { + "macos_launcher" + } else if (currentOs.isLinux) { + "linux_launcher" + } else if (currentOs.isWindows) { + "windows_launcher.exe" + } else { + throw UnsupportedOperationException("Cannot build distribution for currentOs: $currentOs") + } + return localDistDir.get().file("libexec/bin/$launcher") + } + + fun shouldCreateWindowsDistribution(): Boolean { + val runDistExe = project.getXdkPropertyBoolean("org.xtclang.install.distExe", false) + if (runDistExe) { + logger.info("$prefix 'distExe' task is enabled; will attempt to build Windows installer.") + if (XdkBuildLogic.findExecutableOnPath(MAKENSIS) == null) { + throw project.buildException("Illegal configuration; project is set to weave a Windows installer, but '$MAKENSIS' is not on the PATH.") + } + return true + } + logger.warn("$prefix 'distExe' is disabled for building distributions. Only 'tar.gz' and 'zip' are allowed.") + return false + } + + override fun toString(): String { + return "$distributionName-$distributionVersion" + } +} diff --git a/build-logic/common-plugins/src/main/kotlin/XdkProperties.kt b/build-logic/common-plugins/src/main/kotlin/XdkProperties.kt new file mode 100644 index 0000000000..a0a42cd24c --- /dev/null +++ b/build-logic/common-plugins/src/main/kotlin/XdkProperties.kt @@ -0,0 +1,213 @@ +import org.gradle.api.Project +import java.io.File +import java.io.FileInputStream +import java.util.Objects.requireNonNull +import java.util.Properties + +/** + * Property helper for all XDK projects. + * + * This is specific to the XDK build, and we really don't want it to be part of the plugin. + * The properties are read from any "*.properties" file, starting in a given project directory. + * The directory tree is then walked upwards to (and including) the ancestral Gradle project + * root directory. Any value assigned deeper in the tree, overwrites any value defined at a more + * shallow level, so that you can override global default property values in a more specific + * context, such as a subproject. + * + * Finally, the property resolver checks the GRADLE_USER_HOME directory, which is a recommended + * place to keep property files with secrets, like GitHub credentials, etc. + * + * The property helper tries to ensure that no standard or inherited task that derives from, ore + * uses "./gradlew properties" will inadvertently dump secrets to the console. + */ +interface XdkProperties { + fun get(key: String, defaultValue: String? = null): String + fun get(key: String, defaultValue: Int? = 0): Int + fun get(key: String, defaultValue: Boolean? = false): Boolean + fun has(key: String): Boolean +} + +class XdkPropertiesImpl(project: Project): XdkProjectBuildLogic(project), XdkProperties { + companion object { + const val REDACTED = "[REDACTED]" + private const val PROPERTIES_EXT = "properties" + + /** + * Convert e.g. org.xtclang.something.testWithCamelCase -> ORG_XVM_SOMETHING_TEST_WITH_CAMEL_CASE + */ + private fun toSystemEnvKey(key: String): String { + // TODO: Unit test keys and make sure no secrets are kept in memory or printed/logged. + return buildString { + for (ch in key) { + when { + ch == '.' -> append('_') + ch.isLowerCase() -> append(ch.uppercaseChar()) + ch.isUpperCase() -> append('_').append(ch) + else -> append(ch) + } + } + } + } + } + + private val secrets = mutableSetOf() + private val properties: Properties + + init { + this.properties = resolve() + toString().lines().forEach { logger.info("$prefix $it") } + logger.info("$prefix Resolved ${properties.size} properties for project.") + } + + /** + * Get a property value from the nested property resolution for this project, or + * throw an exception if the property does not exist, and no default value is given. + * + * The method first checks for the property by its "xdk" name, i.e., on the format + * "org.xtclang.group.someThing". If that fails, it tries to check the System environment + * for "ORG_XVM_GROUP_SOME_THING", as a handy shortcut, if you need to modify something + * in a build run, that isn't declared in a property file. Examples could be testing out + * a new GitHub token, or adding an environment variable that the plugin needs at start + * time (for example, because the project logger outputs aren't inherited by default in + * a plugin, even if it has access to the actual project logger instance), for the + * project to which the plugin is applied. + * + * If the method fails to find the value of a property, both in its resolved property + * map, and in the system environment, it will return the supplied defaultValue + * parameter. If no default value was supplied, an exception will be thrown, and the + * build breaks. + */ + override fun get(key: String, defaultValue: String?): String { + logger.info("$prefix get($key) invoked (props: ${System.identityHashCode(this)})") + if (!key.startsWith("org.xtclang")) { + // TODO: Remove this artificial limitation. + throw project.buildException("ERROR: XdkProperties are currently expected to start with org.xtclang. Remove this artificial limitation.") + } + if (!has(key)) { + return defaultValue?.also { + logger.info("$prefix XdkProperties; resolved property '$key' to its default value.") + } ?: throw project.buildException("ERROR: XdkProperty '$key' has no value, and no default was given.") + } + + // First check a system env override + val envKey = toSystemEnvKey(key) + val envValue = System.getenv(envKey) + if (envValue != null) { + logger.info("$prefix XdkProperties; resolved System ENV property '$key' (${envKey}).") + return envValue.toString() + } + + val sysPropValue = System.getProperty(key) + if (sysPropValue != null) { + logger.info("$prefix XdkProperties; resolved Java System property '$key'.") + return sysPropValue.toString() + } + + logger.info("$prefix XdkProperties; resolved property '$key' from properties table.") + return properties[key]!!.toString() + } + + override fun get(key: String, defaultValue: Int?): Int { + return get(key, defaultValue?.toString()).toInt() + } + + override fun get(key: String, defaultValue: Boolean?): Boolean { + return get(key, defaultValue.toString()).toBoolean() + } + + override fun has(key: String): Boolean { + return properties[key] != null || System.getenv(toSystemEnvKey(key)) != null || System.getProperty(key) != null + } + + override fun toString(): String { + return buildString { + append("XdkProperties('${project.name}'):") + if (properties.isEmpty) { + append(" [empty]") + } + appendLine() + properties.keys.map { it.toString() }.sorted().forEach { key -> + append(" $key = ") + val value = if (isSecret(key)) REDACTED else "'${properties[key]}'" + appendLine(value) + } + } + } + + private fun resolve(includeExternal: Boolean = true): Properties { + val all = Properties() + val ext = Properties() + resolveProjectDirs().forEach { mergeFromDir(all, it) } + if (includeExternal) { + resolveExternalDirs().forEach { mergeFromDir(ext, it) } + ext.keys.map { it.toString() }.forEach(secrets::add) + } + logger.info("$prefix Internals; loaded properties (${all.size} internal, ${ext.size} external).") + return merge(all, ext) + } + + private fun resolveProjectDirs(): List { + val compositeRoot = project.compositeRootProjectDirectory.asFile + var dir = project.projectDir + return buildList { + do { + add(dir) + dir = dir.parentFile + } while (dir != compositeRoot.parentFile) + } + } + + private fun resolveExternalDirs(): List { + return buildList { + add(project.gradle.gradleUserHomeDir) + val gradleInitDir = project.userInitScriptDirectory + if (gradleInitDir.isDirectory) { + add(gradleInitDir) + } + } + } + + /** + * Accessor to check if a property key resolved by this project has a secret value. + * This means that we should never log it, print it, or in any way leak it from a run. + * Secret values <=> properties defined outside the project hierarchy, e.g. in + * GRADLE_USER_HOME or GRADLE_USER_HOME/init.d, or similar well-defined places. + */ + private fun isSecret(key: String): Boolean { + return secrets.contains(key) + } + + /** + * For each property, check if this property is set already, then it's declared on a deeper + * level, and should not be reset. + */ + private fun merge(to: Properties, from: Properties): Properties { + from.forEach { key, value -> + val old = to.putIfAbsent(key, value) + if (old == null) { + logger.info("$prefix Defined new property: '$key'") + } else { + logger.info("$prefix Property '$key' already defined, not overwriting.") + if (old != value) { + logger.info("$prefix WARNING: Property '$key' has different values at different levels.") + } + } + } + return to + } + + private fun mergeFromDir(to: Properties, dir: File): Properties = project.run { + assert(dir.isDirectory) + val files = requireNonNull(dir.listFiles()).filter { it.isFile && it.extension == PROPERTIES_EXT }.toSet() + val local = Properties() + if (files.isEmpty()) { + return local + } + for (f in files) { + assert(f.exists() && f.isFile) + FileInputStream(f).use { local.load(it) } + logger.info("$prefix Loaded ${local.size} properties from ${f.absolutePath}") + } + return merge(to, local) + } +} diff --git a/build-logic/common-plugins/src/main/kotlin/XdkVersionHandler.kt b/build-logic/common-plugins/src/main/kotlin/XdkVersionHandler.kt new file mode 100644 index 0000000000..414967bbe0 --- /dev/null +++ b/build-logic/common-plugins/src/main/kotlin/XdkVersionHandler.kt @@ -0,0 +1,94 @@ +import org.gradle.api.Project +import org.gradle.api.artifacts.Dependency +import org.gradle.api.artifacts.VersionCatalogsExtension +import org.gradle.api.provider.Provider +import org.gradle.kotlin.dsl.findByType + +class XdkVersionHandler(project: Project): XdkProjectBuildLogic(project) { + companion object { + private const val XDK_VERSION_CATALOG_NAME = "libs" + private const val XDK_VERSION_CATALOG_VERSION = "xdk" + private const val XDK_VERSION_CATALOG_GROUP = "group-xdk" + private const val XTC_VERSION_PLUGIN_CATALOG_VERSION = "xtc-plugin" + private const val XTC_VERSION_PLUGIN_CATALOG_GROUP = "group-xtc-plugin" + private const val XDK_ROOT_PROJECT_NAME = "xvm" + + fun semanticVersionFor(dependency: Provider): SemanticVersion { + with(dependency.get()) { + return SemanticVersion(group!!, name, version!!) + } + } + } + + fun assignSemanticVersionFromCatalog(): SemanticVersion { + val verXdk = catalogSemanticVersion(XDK_VERSION_CATALOG_GROUP, XDK_VERSION_CATALOG_VERSION) + val verXtcPlugin = catalogSemanticVersion(XTC_VERSION_PLUGIN_CATALOG_GROUP, XTC_VERSION_PLUGIN_CATALOG_VERSION) + if (verXdk != verXtcPlugin) { + throw project.buildException("Illegal state: version mismatch between XDK and XTC plugin: '$verXdk' != '$verXtcPlugin'") + } + return assignSemanticVersion(catalogSemanticVersion(XDK_VERSION_CATALOG_GROUP, XDK_VERSION_CATALOG_VERSION)) + } + + private fun assignSemanticVersion(semanticVersion: SemanticVersion): SemanticVersion { + val (group, name, version) = semanticVersion + + ensureNotVersioned(project) + + if (project.name != name) { + throw project.buildException("Illegal state: project name '${project.name}' does not match the name in the semantic version: '$name'") + } + + project.group = group + project.version = version + + if (name == XDK_ROOT_PROJECT_NAME) { + return semanticVersion + } + + logger.info("$prefix XDK Project '$name' versioned as: '$semanticVersion'") + with (project) { + logger.info(""" + $prefix project.group : $group + $prefix project.name : $name + $prefix project.version: $version + """.trimIndent() + ) + } + + return semanticVersion + } + + private fun catalogSemanticVersion(catalogGroup: String, catalogVersion: String): SemanticVersion { + // Try to resolve group and version to assign for an unversioned project in this repo (XDK). + return SemanticVersion(catalogVersion(catalogGroup), project.name, catalogVersion(catalogVersion)) + } + + /** + * Returns the settings phase equivalent of doing the type safe shorthand "libs.versions.", when + * the project has been evaluated. + */ + private fun catalogVersion(name: String, catalog: String = XDK_VERSION_CATALOG_NAME): String { + // TODO: Do not pass projects to companion object functions. This is usually just a contamination to get the logger, and we + // should avoid that. This is the same kind of code smell as static methods that take an instance to operate on as first + // parameter in Java. + project.extensions.findByType()?.also { catalogs -> + val versionCatalog = catalogs.named(catalog) + val value = versionCatalog.findVersion(name) + if (value.isPresent) { + logger.info("$prefix Version catalog '$catalog': '$name' = '${value.get()}'") + return value.get().toString() + } + } + throw project.buildException("Version catalog entry '$name' has no value for '$catalog:$name'") + } + + private fun ensureNotVersioned(project: Project): Unit = project.run { + val group = group.toString() + val version = version.toString() + val hasGroup = group.isNotEmpty() // Not always empty by default. Can be parent project hierarchy too. + val hasVersion = Project.DEFAULT_VERSION == version + if ((hasGroup || hasVersion) && group.indexOf('.') != -1) { + logger.warn("$prefix Project '$name' is not expected to have hierarchical group and version configured at init: (version: group='$group', name='$name', version='$version')") + } + } +} diff --git a/build-logic/common-plugins/src/main/kotlin/org.xtclang.build.debug.gradle.kts b/build-logic/common-plugins/src/main/kotlin/org.xtclang.build.debug.gradle.kts new file mode 100644 index 0000000000..f5a85dd88b --- /dev/null +++ b/build-logic/common-plugins/src/main/kotlin/org.xtclang.build.debug.gradle.kts @@ -0,0 +1,57 @@ +import XdkBuildLogic.Companion.XDK_TASK_GROUP_DEBUG + +/** + * Utilities for debugging the build. Please note that some of these tasks extract contents from + * Providers, which means that it may slow the build down, both configuring and/or running the tasks. + * + * Use with caution. In general; just remember the caveat: this logic may slow the build down, + * by evaluating lazy information too early. + */ + +val printResolvedConfigFiles by tasks.registering { + group = XDK_TASK_GROUP_DEBUG + description = "Prints the files in a resolved configuration." + doLast { + printAllResolvedConfigFiles() + } +} + +val printTaskDependencies by tasks.registering { + group = XDK_TASK_GROUP_DEBUG + description = "Prints the files in a resolved configuration." + doLast { + printAllTaskDependencies() + } +} + +val printPublications by tasks.registering { + group = XDK_TASK_GROUP_DEBUG + description = "Prints the declared publications in a project" + doLast { + printPublications() + } +} + +val printTaskInputs by tasks.registering { + group = XDK_TASK_GROUP_DEBUG + description = "Prints the inputs for all tasks." + doLast { + printAllTaskInputs() + } +} + +val printTaskOutputs by tasks.registering { + group = XDK_TASK_GROUP_DEBUG + description = "Prints the outputs for all tasks." + doLast { + printAllTaskOutputs() + } +} + +val printMavenRepos by tasks.registering { + group = XDK_TASK_GROUP_DEBUG + description = "Prints all Maven repositories in configuration." + doLast { + printMavenRepos() + } +} diff --git a/build-logic/common-plugins/src/main/kotlin/org.xtclang.build.java.gradle.kts b/build-logic/common-plugins/src/main/kotlin/org.xtclang.build.java.gradle.kts new file mode 100644 index 0000000000..c4665dbf0d --- /dev/null +++ b/build-logic/common-plugins/src/main/kotlin/org.xtclang.build.java.gradle.kts @@ -0,0 +1,129 @@ +import XdkBuildLogic.Companion.DEFAULT_JAVA_BYTECODE_VERSION +import org.gradle.api.tasks.testing.logging.TestLogEvent.STANDARD_OUT +import org.gradle.api.tasks.testing.logging.TestLogEvent.STANDARD_ERROR +import org.gradle.api.tasks.testing.logging.TestLogEvent.SKIPPED +import org.gradle.api.tasks.testing.logging.TestLogEvent.STARTED +import org.gradle.api.tasks.testing.logging.TestLogEvent.PASSED +import org.gradle.api.tasks.testing.logging.TestLogEvent.FAILED + +import java.nio.charset.StandardCharsets.UTF_8 + +plugins { + id("org.xtclang.build.xdk.versioning") + java +} + +private val pprefix = "org.xtclang.java" +private val lintProperty = "$pprefix.lint" + +private val jdkVersion: Provider = provider { + getXdkPropertyInt("$pprefix.jdk", DEFAULT_JAVA_BYTECODE_VERSION) +} + +java { + toolchain { + val xdkJavaVersion = JavaLanguageVersion.of(jdkVersion.get()) + val buildProcessJavaVersion = JavaLanguageVersion.of(JavaVersion.current().majorVersion.toInt()) + if (!buildProcessJavaVersion.canCompileOrRun(xdkJavaVersion)) { + throw buildException("Error in Java Toolchain config. The builder can't compile requested Java version: $xdkJavaVersion") + } + logger.info("$prefix Java Toolchain config; binary format version: 'JDK $xdkJavaVersion' (build process version: 'JDK $buildProcessJavaVersion')") + languageVersion.set(xdkJavaVersion) + } +} + +testing { + suites { + @Suppress("UnstableApiUsage") val test by getting(JvmTestSuite::class) { + useJUnitJupiter() + } + } +} + +tasks.withType().configureEach { + inputs.property("jdkVersion", jdkVersion); + logger.info("$prefix Configuring JavaExec task $name from toolchain (Java version: ${java.toolchain.languageVersion})") + javaLauncher.set(javaToolchains.launcherFor(java.toolchain)) + if (enablePreview()) { + jvmArgs("--enable-preview") + } + doLast { + logger.info("$prefix JVM arguments: $jvmArgs") + } +} + +val checkWarnings by tasks.registering { + if (!getXdkPropertyBoolean(lintProperty, false)) { + val lintPropertyHasValue = isXdkPropertySet(lintProperty) + logger.warn("$prefix WARNING: Java warnings are ${if (lintPropertyHasValue) "explicitly" else ""} disabled for project.") + } +} + +val assemble by tasks.existing { + dependsOn(checkWarnings) +} + +tasks.withType().configureEach { + // TODO: These xdk properties may have to be declared as inputs. + val lint = getXdkPropertyBoolean(lintProperty, false) + val maxErrors = getXdkPropertyInt("$pprefix.maxErrors", 0) + val maxWarnings = getXdkPropertyInt("$pprefix.maxWarnings", 0) + val warningsAsErrors = getXdkPropertyBoolean("$pprefix.warningsAsErrors", true) + if (!warningsAsErrors) { + logger.warn("$prefix WARNING: Task '$name' XTC Java convention warnings are not treated as errors, which is best practice (Enable -Werror).") + } + + val args = buildList { + add("-Xlint:${if (lint) "all" else "none"}") + + if (enablePreview()) { + add("--enable-preview") + if (lint) { + add("-Xlint:preview") + } + } + + if (maxErrors > 0) { + add("-Xmaxerrs") + add("$maxErrors") + } + + if (maxWarnings > 0) { + add("-Xmaxwarns") + add("$maxWarnings") + } + + if (warningsAsErrors) { + add("-Werror") + } + } + + with(options) { + compilerArgs.addAll(args) + isDeprecation = lint + isWarnings = lint + encoding = UTF_8.toString() + //isFork = false + } +} + +tasks.withType().configureEach { + if (enablePreview()) { + jvmArgs("--enable-preview") + } + maxHeapSize = getXdkProperty("$pprefix.maxHeap", "4G") + testLogging { + showStandardStreams = getXdkPropertyBoolean("$pprefix.test.stdout") + if (showStandardStreams) { + events(STANDARD_OUT, STANDARD_ERROR, SKIPPED, STARTED, PASSED, FAILED) + } + } +} + +private fun enablePreview(): Boolean { + val enablePreview = getXdkPropertyBoolean("$pprefix.enablePreview") + if (enablePreview) { + logger.warn("$prefix WARNING: Project has Java preview features enabled.") + } + return enablePreview +} diff --git a/build-logic/common-plugins/src/main/kotlin/org.xtclang.build.publish.gradle.kts b/build-logic/common-plugins/src/main/kotlin/org.xtclang.build.publish.gradle.kts new file mode 100644 index 0000000000..043400d297 --- /dev/null +++ b/build-logic/common-plugins/src/main/kotlin/org.xtclang.build.publish.gradle.kts @@ -0,0 +1,164 @@ +import org.gradle.api.publish.plugins.PublishingPlugin.PUBLISH_TASK_GROUP + +plugins { + id("org.xtclang.build.xdk.versioning") + id("maven-publish") // TODO: Adding the maven publish plugin here, will always bring with it the PluginMaven publication. We don't always want to use that e.g. for the plugin build. Either reuse the publication there, or find a better way to add the default maven publication. +} + +/* + * Should we publish the plugin to a common build repository and copy it to any localDist? + */ +private fun shouldPublishPluginToLocalDist(): Boolean { + return project.getXdkPropertyBoolean("org.xtclang.publish.localDist", false) +} + +/** + * Configure repositories for XDK artifact publication. Currently, we publish the XDK zip "xdkArchive", and + * the XTC plugin, "xtcPlugin". + */ +publishing { + repositories { + logger.info("$prefix Configuring publications for repository mavenLocal().") + mavenLocal() + + if (shouldPublishPluginToLocalDist()) { + logger.info("$prefix Configuring publications for repository local flat dir: '$buildRepoDirectory'") + maven { + name = "build" + description = "Publish all publications to the local build directory repository." + url = uri(buildRepoDirectory) + } + } + + logger.info("$prefix Configuring publications for xtclang.org GitHub repository.") + with (xdkBuildLogic.github()) { + if (verifyGitHubConfig()) { + logger.info("$prefix Found GitHub package credentials for XTC (url: $uri, user: $user, org: $organization, read-only: $isReadOnly)") + maven { + name = "GitHub" + description = "Publish all publications to the xtclang.org GitHub repository." + url = uri(uri) + credentials { + username = user + password = token + } + } + } + } + } + + publications.withType().configureEach { + pom { + licenses { + license { + name = "The XDK License" + url = "https://github.com/xtclang/xvm/tree/master/license" + } + } + developers { + developer { + name = "The XTC Language Organization" + email = "info@xtclang.org" + } + } + scm { + connection = "scm:git:git://github.com/xtclang/xvm.git" + developerConnection = "scm:git:ssh://github.com/xtclang/xvm.git" + url = "https://github.com/xtclang/xvm/tree/master" + } + } + } +} + +val publishLocal by tasks.registering { + group = PUBLISH_TASK_GROUP + description = "Task that publishes project publications to local repositories (e.g. build and mavenLocal)." + dependsOn(publishAllPublicationsToMavenLocalRepository) +} + +val pruneBuildRepo by tasks.registering { + group = PUBLISH_TASK_GROUP + description = "Helper task called internally to make sure the build repo is wiped out before republishing. Used by installLocalDist and remote publishing only." + if (shouldPublishPluginToLocalDist()) { + logger.lifecycle("$prefix Installing copy of the plugin to local distribution when it exists.") + delete(buildRepoDirectory) + } +} + +if (shouldPublishPluginToLocalDist()) { + logger.warn("$prefix Configuring local distribution plugin publication.") + val publishAllPublicationsToBuildRepository by tasks.existing { + dependsOn(pruneBuildRepo) + mustRunAfter(pruneBuildRepo) + } + publishLocal { + dependsOn(publishAllPublicationsToBuildRepository) + } +} + +val publishAllPublicationsToMavenLocalRepository by tasks.existing + +val deleteAllLocalPublications by tasks.registering { + doLast { + logger.warn("Task '$name' is not implemented yet. Delete your \$HOME/.m2 directory and any other local repositories manually.") + } +} + +val listAllLocalPublications by tasks.registering { + group = PUBLISH_TASK_GROUP + description = "Task that lists local Maven publications for this project from the mavenLocal() repository." + doLast { + logger.lifecycle("$prefix '$name' Listing local publications (and their artifacts) for project '${project.group}:${project.name}':") + //private val localPublishTasks = provider { tasks.withType().filter{ it.name.contains("Local") }.toList() } + tasks.withType().filter { it.name.contains("Local") }.forEach { + with(it.publication) { + logger.lifecycle("$prefix '${it.name}' (${artifacts.count()} artifacts):") + val baseUrl = "${it.repository.url}${groupId.replace('.', '/')}/$artifactId/$version/$artifactId" + artifacts.forEach { artifact -> + val desc = buildString { + append(baseUrl) + append("-$version") + if (artifact.classifier != null) { + append("-${artifact.classifier}") + } + append(".${artifact.extension}") + } + logger.lifecycle("$prefix Local Artifact: '$desc'") + } + } + } + } +} + +val listAllRemotePublications by tasks.registering { + group = PUBLISH_TASK_GROUP + description = "Task that lists publications for this project on the 'xtclang' org GitHub package repo." + doLast { + val github = xdkBuildLogic.github() + logger.lifecycle("$prefix '$name' Listing remote publications for project '${project.group}:${project.name}':") + val packageNames = github.queryXtcLangPackageNames() + if (packageNames.isEmpty()) { + logger.lifecycle("$prefix No Maven packages found.") + return@doLast + } + packageNames.forEach { pkg -> + logger.lifecycle("$prefix Maven package: '$pkg':") + val versions = github.queryXtcLangPackageVersions(pkg) + if (versions.isEmpty()) { + logger.warn("$prefix WARNING: No versions found for this package. Corrupted package repo?") + return@forEach + } + versions.forEach { logger.lifecycle("$prefix version: '$it'") } + } + } +} + +val deleteAllRemotePublications by tasks.registering { + group = PUBLISH_TASK_GROUP + description = "Delete all versions of all packages on the 'xtclang' org GitHub package repo. WARNING: ALL VERSIONS ARE PURGED." + doLast { + val github = xdkBuildLogic.github() + github.deleteXtcLangPackages() // TODO: Add a pattern that can be set thorugh a property to get finer granularity here than "kill everything!". + logger.lifecycle("$prefix Finished '$name' deleting publications for project: '${project.group}:${project.name}'.") + } +} diff --git a/build-logic/common-plugins/src/main/kotlin/org.xtclang.build.xdk.versioning.gradle.kts b/build-logic/common-plugins/src/main/kotlin/org.xtclang.build.xdk.versioning.gradle.kts new file mode 100644 index 0000000000..c8c24b5756 --- /dev/null +++ b/build-logic/common-plugins/src/main/kotlin/org.xtclang.build.xdk.versioning.gradle.kts @@ -0,0 +1,15 @@ +/** + * This is a featherweight precompiled script plugin that helps us set the semantic version + * of all XDK components/subprojects. The plugin itself should not do any manipulation of + * versions, since it can be applied from arbitrary third party code. + * + * Any XTC project will have an extension with its resolved SemanticVersion + */ + +plugins { + id("org.xtclang.build.debug") +} + +val semanticVersion by extra { + xdkBuildLogic.versions().assignSemanticVersionFromCatalog() +} diff --git a/build-logic/settings-plugins/build.gradle.kts b/build-logic/settings-plugins/build.gradle.kts new file mode 100644 index 0000000000..e4a377cb50 --- /dev/null +++ b/build-logic/settings-plugins/build.gradle.kts @@ -0,0 +1,8 @@ +plugins { + `kotlin-dsl` +} + +repositories { + gradlePluginPortal() + mavenCentral() +} diff --git a/build-logic/settings-plugins/settings.gradle.kts b/build-logic/settings-plugins/settings.gradle.kts new file mode 100644 index 0000000000..8212b4fbb7 --- /dev/null +++ b/build-logic/settings-plugins/settings.gradle.kts @@ -0,0 +1,5 @@ +plugins { + id("org.gradle.toolchains.foojay-resolver-convention").version("latest.release") +} + +rootProject.name = "settings-plugins" diff --git a/build-logic/settings-plugins/src/main/kotlin/org.xtclang.build.common.settings.gradle.kts b/build-logic/settings-plugins/src/main/kotlin/org.xtclang.build.common.settings.gradle.kts new file mode 100644 index 0000000000..a264fe8d04 --- /dev/null +++ b/build-logic/settings-plugins/src/main/kotlin/org.xtclang.build.common.settings.gradle.kts @@ -0,0 +1,55 @@ +/** + * Settings resolution level (early) configuration. The only purpose of this plugin is to make sure + * that the ancestral root project resolves, so that we can connect any subproject or included build + * task to the aggregator lifecycle, as well as using its version catalog. + * + * It also adds some bootstrapping logic for downloading and accessing Java toolchain components, + * e.g. the JDK, through the Foojay Resolver plugin, which needs to come in at the settings level. + */ + +fun compositeRootRelativeFile(path: String): File? { + var dir = file(".") + var file = File(dir, path) + while (!file.exists()) { + dir = dir.parentFile + file = File(dir, path) + } + return file +} + +val libsVersionCatalog = compositeRootRelativeFile("gradle/libs.versions.toml")!! + +// If we can read properties here, we can also patch the catalog files. +dependencyResolutionManagement { + @Suppress("UnstableApiUsage") + repositories { + mavenCentral() + } + + // For bootstrapping reasons, we manually load the properties file, instead of falling back to the build logic automatic property handler. + val xdkVersionInfo = compositeRootRelativeFile("GROUP")!! to compositeRootRelativeFile("VERSION")!! + val pluginDir = xdkVersionInfo.first.parentFile.resolve("plugin") + val xdkPluginVersionInfo = pluginDir.resolve("GROUP").let { if (it.isFile) it else xdkVersionInfo.first } to pluginDir.resolve("VERSION").let { if (it.isFile) it else xdkVersionInfo.second } + val (xdkGroup, xdkVersion) = xdkVersionInfo.toList().map { it.readText().trim() } + val (xtcPluginGroup, xtcPluginVersion) = xdkPluginVersionInfo.toList().map { it.readText().trim() } + val prefix = "[${rootProject.name}]" + logger.info("$prefix Configuring and versioning artifact: '$xdkGroup:${rootProject.name}:$xdkVersion'") + logger.info( + """ + $prefix XDK VERSION INFO: + $prefix Project : '${rootProject.name}' + $prefix Group : '$xdkGroup' (plugin: '$xtcPluginGroup') + $prefix Version : '$xdkVersion' (plugin: '$xtcPluginVersion') + """.trimIndent() + ) + + versionCatalogs { + val libs by creating { + from(files(libsVersionCatalog)) // load versions + version("xdk", xdkVersion) + version("xtc-plugin", xtcPluginVersion) + version("group-xdk", xdkGroup) + version("group-xtc-plugin", xtcPluginGroup) + } + } +} diff --git a/build.gradle.kts b/build.gradle.kts index f34cb812e0..94069f9ebb 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,35 +1,92 @@ +import XdkDistribution.Companion.DISTRIBUTION_TASK_GROUP +import org.gradle.api.publish.plugins.PublishingPlugin.PUBLISH_TASK_GROUP + /* * Main build file for the XVM project, producing the XDK. */ -group = "org.xvm" -version = "0.4.3" +plugins { + alias(libs.plugins.xdk.build.versioning) + alias(libs.plugins.xdk.build.aggregator) + alias(libs.plugins.tasktree) +} -allprojects { - configurations.all { - resolutionStrategy.dependencySubstitution { - substitute(module("org.xtclang.xvm:javatools_utils" )).using(project(":javatools_utils")) - substitute(module("org.xtclang.xvm:javatools_unicode")).using(project(":javatools_unicode")) - substitute(module("org.xtclang.xvm:javatools" )).using(project(":javatools")) - } +private val xdk = gradle.includedBuild("xdk") +private val plugin = gradle.includedBuild("plugin") +private val includedBuildsWithPublications = listOfNotNull(xdk, plugin) + +/** + * Installation and distribution tasks that aggregate publishable/distributable included + * build projects. The aggregator proper should be as small as possible, and only contains + * LifeCycle dependencies, aggregated through the various included builds. This creates as + * few bootstrapping problems as possible, since by the time we get to the configuration phase + * of the root build.gradle.kts, we have installed convention plugins, resolved version catalogs + * and similar things. + */ +val install by tasks.registering { + group = DISTRIBUTION_TASK_GROUP + description = "Install the XDK distribution in the xdk/build/distributions and xdk/build/install directories." + XdkDistribution.distributionTasks.forEach { + dependsOn(xdk.task(":$it")) } + dependsOn(xdk.task(":installDist")) +} - repositories { - mavenCentral { - content { - excludeGroup("org.xtclang.xvm") - } - } +val installLocalDist by tasks.registering { + group = DISTRIBUTION_TASK_GROUP + description = "Build and overwrite any local distribution with the new distribution produced by the build." + dependsOn(xdk.task(":$name")) +} + +/* + * Register aggregated publication tasks to the top level project, to ensure we can publish both + * the XDK and the XTC plugin (and other future artifacts) with './gradlew publish' or + * './gradlew publishToMavenLocal'. Snapshot builds should only be allowed to be published + * in local repositories. + * + * Publishing tasks can be racy, but it seems that Gradle serializes tasks that have a common + * output directory, which should be the case here. If not, we will have to put back the + * parallel check/task failure condition. + */ +val publishRemote by tasks.registering { + group = PUBLISH_TASK_GROUP + description = "Publish (aggregate) all artifacts in the XDK to the remote repositories." + includedBuildsWithPublications.forEach { + dependsOn(it.task(":publishAllPublicationsToGitHubRepository")) + // TODO: Add gradlePluginPortal() and mavenCentral() here, when we have an official release to publish (will be done immediately after plugin branch gets merged to master) } } -tasks.withType { - options.encoding = "UTF-8" +/* + * Publish local publications, which are the mavenLocal repositry, and the build repo, as part of + * an XTC distribution archive. The former is on by default, the latter is off. + */ +val publishLocal by tasks.registering { + group = PUBLISH_TASK_GROUP + description = "Publish (aggregated) all artifacts in the XDK to the local Maven repository." + includedBuildsWithPublications.forEach { + dependsOn(it.task(":$name")) + } } -tasks.register("build") { - group = "Build" - description = "Build all projects" +val publish by tasks.registering { + group = PUBLISH_TASK_GROUP + description = "Task that aggregates publish tasks for builds with publications." + dependsOn(publishLocal, publishRemote) +} - dependsOn(project("xdk:").tasks["build"]) +GitHubPackages.publishTaskPrefixes.forEach { prefix -> + buildList { + addAll(GitHubPackages.publishTaskSuffixesLocal) + addAll(GitHubPackages.publishTaskSuffixesRemote) + }.forEach { suffix -> + val taskName = "$prefix$suffix" + tasks.register(taskName) { + group = PUBLISH_TASK_GROUP + description = "Task that aggregates '$taskName' tasks for builds with publications." + includedBuildsWithPublications.forEach { + dependsOn(it.task(":$taskName")) + } + } + } } diff --git a/gradle.properties b/gradle.properties index 36d405946a..5ed0588aab 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,52 +1,31 @@ +# +# Gradle properties. +# -# Default is true - daemons enabled by default -org.gradle.daemon=true - -# Default is false. Setting parallel to true optimizes compilation on the amount of cores the heuristic chooses org.gradle.parallel=true - -# To counteract parallel builds grabbing too many cores, we can reset the priority of the daemons -# from normal to low. This still uses multi core compilatoin to the extent it can, but other tasks -# will feel significantly less affected by a build running in the background. -org.gradle.priority=low - -# Default is that caching is switched off. This saves a lot of time on incremental rebuilds, -# but if the dependencies are weird, some might be missed. Usually, this is never a problem -# with command line Gradle, or letting IntelliJ use Gradle for the builds, but rather for things -# like the Gradle build plugin integrated into IntelliJ, etc. However, anyone who follows -# best practice for dependencies and Gradle build files, should not experience any significant -# problems here with fairly recent Gradle versions > 7.4* org.gradle.caching=true org.gradle.caching.debug=false -# Possibly add verbose stack traces when the build processes break to easier be able to grep for -# the cause in the build log output. Right now this is using the default setting "internal", -# because in the general case, a successful build also logs some rather heavy stack traces -# with a more verbose setting, and we should avoid having to page through those if everything -# is fine. -org.gradle.logging.stacktrace=internal - -# Watchman like mechanism built into newer Gradle versions. Definitely makes a difference in -# performance for cached builds on Linux. -org.gradle.vfs.watch=true -org.gradle.vfs.verbose=false - -# There are non ASCII characters in the source tree, so we need to enable UTF-8 encoding -# for any Java compiler that might need it, as it is in a different default locale (usually US_ASCII) -# -# This is semi-redundant, given that the root build.gradle.kts script augments the compiler encodings -# with UTF-8, but this covers builds with slightly different semantics. -org.gradle.jvmargs='-Dfile.encoding=UTF-8' +# TODO: Experiment with enabling the configuration cache later +org.gradle.configuration-cache=false +# The "debug" property (default false) enables gradle debug mode. For the kotlin dsl, you can also step through +# build scripts. # -# Gradle deprecates things quickly, and also removes them quicker than most other incrementally growing -# systems. If a new Java version needs to be supported, historically that has usually meant that Gradle -# would need a verison upgrade to use it. It is common that needing to upgrade for newer Java leads to -# build breakages if deprecations in Gradle dot releases aren't taken care of at least semi-frequently. -# This switches on warnings, so that we can be aware of what isn't future safe in an existing build, and -# to strive to try to keep the tech debt in check, to avoid blockers when major version upgrades are -# required for any significiant business reason. -# -# Possible values: all, fail, summary, none (default is summary) -org.gradle.warning.mode=all - +# (See: https://docs.gradle.org/current/userguide/troubleshooting.html#sec:troubleshooting_build_logic +# This will halt the build and allow you to attach a remote debugger to a specific port. The default is, +# 5005, and you may have to use --no-daemon as well, unless you attach to a suspended task.) +org.gradle.debug=false +org.gradle.debug.port=5005 + +# JVM and Gradle daemon flags. The default memory usage is 700M for a Daemon, which can be slow in extreme environments. +org.gradle.jvmargs=-Dfile.encoding=UTF-8 -Xmx4G + +# Logging and warning levels +# (Console value can be one of: auto, plain, rich, verbose (default is auto), +# warning mode can be one of: none, summary, all, fail (default is summary)) +org.gradle.console=auto +org.gradle.warning.mode=summary +#org.gradle.logging.stacktrace=all + +systemProp.gradle.internal.publish.checksums.insecure=true diff --git a/gradle/config/repos/xtc-repo.init.gradle.kts.template b/gradle/config/repos/xtc-repo.init.gradle.kts.template deleted file mode 100644 index 5f1003ef45..0000000000 --- a/gradle/config/repos/xtc-repo.init.gradle.kts.template +++ /dev/null @@ -1,115 +0,0 @@ -//noinspection - -/** - * This is an init script that can generate an xtc-repo.init.gradle.kts file under the - * GRADLE_USER_HOME/init.d directory, or similar locations. To install it from the XDK - * repository, run './gradlew installInitScripts'. This is something you normally only - * have to do once. - * - * Info about GitHub requiring credentials for public Maven packages, which is crazy: - * https://docs.github.com/en/packages/learn-github-packages/about-github-packages#authenticating-to-github-packages - * https://github.com/orgs/community/discussions/26634 - */ - -import java.io.InputStream -import java.net.URI -import java.util.Properties -import java.nio.file.Path -import kotlin.io.encoding.Base64 -import kotlin.io.encoding.ExperimentalEncodingApi -import kotlin.io.path.isDirectory - -gradle.beforeSettings { - settings.pluginManagement.repositories { - val orgXtcLangRepoPlugins: () -> MavenArtifactRepository by extra { { XtcRepoProvider(this).createGitHubMavenArtifactRepository() } } - val xdkRepoPlugins: () -> MavenArtifactRepository by extra { { XtcRepoProvider(this).createLocalDistMavenArtifactRepository() } } - } - @Suppress("UnstableApiUsage") - settings.dependencyResolutionManagement.repositories { - val orgXtcLangRepo: () -> MavenArtifactRepository by extra { { XtcRepoProvider(this).createGitHubMavenArtifactRepository() } } - val xdkRepo: () -> MavenArtifactRepository by extra { { XtcRepoProvider(this).createLocalDistMavenArtifactRepository() } } - } -} - -private class XtcRepoProvider(private val repos: RepositoryHandler) { - private val initDir = gradle.gradleUserHomeDir.resolve("init.d").resolve("xtc-repo.properties") - - fun createLocalDistMavenArtifactRepository(): MavenArtifactRepository { - return repos.maven { - val repoPath: Path? = findXdkLocalDist()?.resolve("repo") - if (repoPath == null || !repoPath.isDirectory()) { - throw IllegalArgumentException("Cannot resolve any repository data in local XDK distribution.") - } - name = "xdkRepo" - url = repoPath.toUri() - }.also { - logger.lifecycle("*** Added orgXtcLangLocalDist() repository: ${it.name} (url ${it.url})") - } - } - - fun createGitHubMavenArtifactRepository(): MavenArtifactRepository { - return repos.maven { - @OptIn(ExperimentalEncodingApi::class) - fun decodeToken(str: String): String { - return runCatching { Base64.decode(str).toString(Charsets.UTF_8).trim() }.getOrDefault("") - } - with(resolveConfig(logger, initDir)) { - name = "GitHub" - url = uri(getProperty(XTC_REPO_PROPERTY_URL)) - credentials { - username = getProperty(XTC_REPO_PROPERTY_USER) - password = decodeToken(getProperty(XTC_REPO_PROPERTY_TOKEN)) - } - } - }.also { - logger.lifecycle("*** Added orgXtcLang() repository: ${it.name} (url ${it.url})") - } - } - - companion object { - private const val XTC_REPO_INIT_PREFIX = "https://raw.githubusercontent.com/xtclang/xvm/master/gradle/config/repos/" - private const val XTC_REPO_INIT_PROPERTIES_URL = "$XTC_REPO_INIT_PREFIX/xtc-repo.properties.template" - private const val XTC_REPO_INIT_SCRIPT_URL = "$XTC_REPO_INIT_PREFIX/xtc-repo.init.gradle.kts.template" - - private const val XTC_REPO_PROPERTY_PREFIX = "org.xtclang.repo.github" - private const val XTC_REPO_PROPERTY_NAME = "$XTC_REPO_PROPERTY_PREFIX.name" - private const val XTC_REPO_PROPERTY_USER = "$XTC_REPO_PROPERTY_PREFIX.user" - private const val XTC_REPO_PROPERTY_TOKEN = "$XTC_REPO_PROPERTY_PREFIX.token" - private const val XTC_REPO_PROPERTY_URL = "$XTC_REPO_PROPERTY_PREFIX.url" - - private const val LAUNCHER = "xec" - private const val ENV_PATH = "PATH" - - private fun resolveConfig(logger: Logger, initDir: File): Properties { - fun fromStream(stream: InputStream): Properties { - return stream.use { Properties().also { it.load(stream) } } - } - - fun loadFromUrl(url: String): Properties { - return fromStream(URI(url).toURL().openStream()) - } - - fun loadFromFile(): Properties { - return fromStream(initDir.inputStream()) - } - - try { - return loadFromFile().also { logger.lifecycle("Loaded XTC Repo properties from local path: ${initDir.absolutePath}") } - } catch (t: Throwable) { - logger.lifecycle("XTC Plugin: failed to resolve local properties from: $initDir (will attempt remote URL)", t) - } - try { - return loadFromUrl(XTC_REPO_INIT_PROPERTIES_URL).also { logger.lifecycle("Loaded XTC Repo properties from remote URL: $it") } - } catch (t: Throwable) { - logger.error("XTC Plugin: failed to resolve remote properties from: $XTC_REPO_INIT_PROPERTIES_URL", t) - throw GradleException(t.message ?: t.javaClass.simpleName, t) - } - } - - // TODO: Third alternative may be to grab the local dist from the path to use the "repo" subdirectory in an - // existing XDK distribution. - private fun findXdkLocalDist(): Path? { - return System.getenv(ENV_PATH)?.split(File.pathSeparator)?.map { File(it, LAUNCHER) }?.find { it.exists() && it.canExecute() }?.toPath()?.toRealPath()?.parent?.parent?.parent - } - } -} diff --git a/gradle/config/repos/xtc-repo.properties.template b/gradle/config/repos/xtc-repo.properties.template deleted file mode 100644 index ffc6187bed..0000000000 --- a/gradle/config/repos/xtc-repo.properties.template +++ /dev/null @@ -1,18 +0,0 @@ -# -# XTC organization repository configuration (read-only). Used to bootstrap an init.d -# framework for resolving XTC public repositories, and depending on their artifacts. -# -# Currently, the XTC project has a package repository with GitHub. While GitHub allows -# anyone to access any source file in public repositories without credentials, this is -# NOT YET the case for GitHub Maven Package Repositories (and a few others). Annoyingly, -# so that a third party user does not get stuck in configuring obscure access tokens, -# this file is used a the source of truth for the XTC GitHub package repository and its -# credentials. The referred token is safe, replaceable, and grants a single package:read -# privilege to the user. -# - -org.xtclang.repo.github.url=https://maven.pkg.github.com/xtclang/xvm -org.xtclang.repo.github.org=xtclang -org.xtclang.repo.github.user=xtclang-bot -org.xtclang.repo.github.token=Z2hwX0ZjNGRWeDhNYmxPcnZDYWZrRW96Q0NrQXAzaVZ5RjBUb0NheAo= -org.xtclang.repo.github.readonly=true diff --git a/gradle/config/user/.editorconfig_lagergren b/gradle/config/user/.editorconfig_lagergren new file mode 100644 index 0000000000..079529fb0b --- /dev/null +++ b/gradle/config/user/.editorconfig_lagergren @@ -0,0 +1,1209 @@ +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = false +max_line_length = 120 +tab_width = 4 +ij_continuation_indent_size = 8 +ij_formatter_off_tag = @formatter:off +ij_formatter_on_tag = @formatter:on +ij_formatter_tags_enabled = true +ij_smart_tabs = false +ij_visual_guides = +ij_wrap_on_typing = false + +[*.css] +ij_css_align_closing_brace_with_properties = false +ij_css_blank_lines_around_nested_selector = 1 +ij_css_blank_lines_between_blocks = 1 +ij_css_block_comment_add_space = false +ij_css_brace_placement = end_of_line +ij_css_enforce_quotes_on_format = false +ij_css_hex_color_long_format = false +ij_css_hex_color_lower_case = false +ij_css_hex_color_short_format = false +ij_css_hex_color_upper_case = false +ij_css_keep_blank_lines_in_code = 2 +ij_css_keep_indents_on_empty_lines = false +ij_css_keep_single_line_blocks = false +ij_css_properties_order = font,font-family,font-size,font-weight,font-style,font-variant,font-size-adjust,font-stretch,line-height,position,z-index,top,right,bottom,left,display,visibility,float,clear,overflow,overflow-x,overflow-y,clip,zoom,align-content,align-items,align-self,flex,flex-flow,flex-basis,flex-direction,flex-grow,flex-shrink,flex-wrap,justify-content,order,box-sizing,width,min-width,max-width,height,min-height,max-height,margin,margin-top,margin-right,margin-bottom,margin-left,padding,padding-top,padding-right,padding-bottom,padding-left,table-layout,empty-cells,caption-side,border-spacing,border-collapse,list-style,list-style-position,list-style-type,list-style-image,content,quotes,counter-reset,counter-increment,resize,cursor,user-select,nav-index,nav-up,nav-right,nav-down,nav-left,transition,transition-delay,transition-timing-function,transition-duration,transition-property,transform,transform-origin,animation,animation-name,animation-duration,animation-play-state,animation-timing-function,animation-delay,animation-iteration-count,animation-direction,text-align,text-align-last,vertical-align,white-space,text-decoration,text-emphasis,text-emphasis-color,text-emphasis-style,text-emphasis-position,text-indent,text-justify,letter-spacing,word-spacing,text-outline,text-transform,text-wrap,text-overflow,text-overflow-ellipsis,text-overflow-mode,word-wrap,word-break,tab-size,hyphens,pointer-events,opacity,color,border,border-width,border-style,border-color,border-top,border-top-width,border-top-style,border-top-color,border-right,border-right-width,border-right-style,border-right-color,border-bottom,border-bottom-width,border-bottom-style,border-bottom-color,border-left,border-left-width,border-left-style,border-left-color,border-radius,border-top-left-radius,border-top-right-radius,border-bottom-right-radius,border-bottom-left-radius,border-image,border-image-source,border-image-slice,border-image-width,border-image-outset,border-image-repeat,outline,outline-width,outline-style,outline-color,outline-offset,background,background-color,background-image,background-repeat,background-attachment,background-position,background-position-x,background-position-y,background-clip,background-origin,background-size,box-decoration-break,box-shadow,text-shadow +ij_css_space_after_colon = true +ij_css_space_before_opening_brace = true +ij_css_use_double_quotes = true +ij_css_value_alignment = do_not_align + +[*.feature] +indent_size = 2 +ij_gherkin_keep_indents_on_empty_lines = false + +[*.java] +ij_java_align_consecutive_assignments = false +ij_java_align_consecutive_variable_declarations = false +ij_java_align_group_field_declarations = false +ij_java_align_multiline_annotation_parameters = false +ij_java_align_multiline_array_initializer_expression = false +ij_java_align_multiline_assignment = false +ij_java_align_multiline_binary_operation = false +ij_java_align_multiline_chained_methods = false +ij_java_align_multiline_deconstruction_list_components = true +ij_java_align_multiline_extends_list = false +ij_java_align_multiline_for = true +ij_java_align_multiline_method_parentheses = false +ij_java_align_multiline_parameters = true +ij_java_align_multiline_parameters_in_calls = false +ij_java_align_multiline_parenthesized_expression = false +ij_java_align_multiline_records = true +ij_java_align_multiline_resources = true +ij_java_align_multiline_ternary_operation = false +ij_java_align_multiline_text_blocks = false +ij_java_align_multiline_throws_list = false +ij_java_align_subsequent_simple_methods = false +ij_java_align_throws_keyword = false +ij_java_align_types_in_multi_catch = true +ij_java_annotation_parameter_wrap = off +ij_java_array_initializer_new_line_after_left_brace = false +ij_java_array_initializer_right_brace_on_new_line = false +ij_java_array_initializer_wrap = off +ij_java_assert_statement_colon_on_next_line = false +ij_java_assert_statement_wrap = off +ij_java_assignment_wrap = off +ij_java_binary_operation_sign_on_next_line = false +ij_java_binary_operation_wrap = off +ij_java_blank_lines_after_anonymous_class_header = 0 +ij_java_blank_lines_after_class_header = 0 +ij_java_blank_lines_after_imports = 1 +ij_java_blank_lines_after_package = 1 +ij_java_blank_lines_around_class = 1 +ij_java_blank_lines_around_field = 0 +ij_java_blank_lines_around_field_in_interface = 0 +ij_java_blank_lines_around_initializer = 1 +ij_java_blank_lines_around_method = 1 +ij_java_blank_lines_around_method_in_interface = 1 +ij_java_blank_lines_before_class_end = 0 +ij_java_blank_lines_before_imports = 1 +ij_java_blank_lines_before_method_body = 0 +ij_java_blank_lines_before_package = 0 +ij_java_block_brace_style = end_of_line +ij_java_block_comment_add_space = false +ij_java_block_comment_at_first_column = true +ij_java_builder_methods = +ij_java_call_parameters_new_line_after_left_paren = false +ij_java_call_parameters_right_paren_on_new_line = false +ij_java_call_parameters_wrap = off +ij_java_case_statement_on_separate_line = true +ij_java_catch_on_new_line = false +ij_java_class_annotation_wrap = split_into_lines +ij_java_class_brace_style = end_of_line +ij_java_class_count_to_use_import_on_demand = 10000 +ij_java_class_names_in_javadoc = 1 +ij_java_deconstruction_list_wrap = normal +ij_java_do_not_indent_top_level_class_members = false +ij_java_do_not_wrap_after_single_annotation = false +ij_java_do_not_wrap_after_single_annotation_in_parameter = false +ij_java_do_while_brace_force = always +ij_java_doc_add_blank_line_after_description = true +ij_java_doc_add_blank_line_after_param_comments = false +ij_java_doc_add_blank_line_after_return = false +ij_java_doc_add_p_tag_on_empty_lines = true +ij_java_doc_align_exception_comments = true +ij_java_doc_align_param_comments = true +ij_java_doc_do_not_wrap_if_one_line = false +ij_java_doc_enable_formatting = true +ij_java_doc_enable_leading_asterisks = true +ij_java_doc_indent_on_continuation = false +ij_java_doc_keep_empty_lines = true +ij_java_doc_keep_empty_parameter_tag = true +ij_java_doc_keep_empty_return_tag = true +ij_java_doc_keep_empty_throws_tag = true +ij_java_doc_keep_invalid_tags = true +ij_java_doc_param_description_on_new_line = false +ij_java_doc_preserve_line_breaks = false +ij_java_doc_use_throws_not_exception_tag = true +ij_java_else_on_new_line = false +ij_java_entity_dd_prefix = +ij_java_entity_dd_suffix = EJB +ij_java_entity_eb_prefix = +ij_java_entity_eb_suffix = Bean +ij_java_entity_hi_prefix = +ij_java_entity_hi_suffix = Home +ij_java_entity_lhi_prefix = Local +ij_java_entity_lhi_suffix = Home +ij_java_entity_li_prefix = Local +ij_java_entity_li_suffix = +ij_java_entity_pk_class = java.lang.String +ij_java_entity_ri_prefix = +ij_java_entity_ri_suffix = +ij_java_entity_vo_prefix = +ij_java_entity_vo_suffix = VO +ij_java_enum_constants_wrap = off +ij_java_extends_keyword_wrap = off +ij_java_extends_list_wrap = off +ij_java_field_annotation_wrap = split_into_lines +ij_java_field_name_prefix = +ij_java_field_name_suffix = +ij_java_filter_class_prefix = +ij_java_filter_class_suffix = +ij_java_filter_dd_prefix = +ij_java_filter_dd_suffix = +ij_java_finally_on_new_line = false +ij_java_for_brace_force = always +ij_java_for_statement_new_line_after_left_paren = false +ij_java_for_statement_right_paren_on_new_line = false +ij_java_for_statement_wrap = off +ij_java_generate_final_locals = true +ij_java_generate_final_parameters = true +ij_java_if_brace_force = always +ij_java_imports_layout = *,|,javax.**,java.**,|,$* +ij_java_indent_case_from_switch = true +ij_java_insert_inner_class_imports = false +ij_java_insert_override_annotation = true +ij_java_keep_blank_lines_before_right_brace = 2 +ij_java_keep_blank_lines_between_package_declaration_and_header = 2 +ij_java_keep_blank_lines_in_code = 2 +ij_java_keep_blank_lines_in_declarations = 2 +ij_java_keep_builder_methods_indents = false +ij_java_keep_control_statement_in_one_line = false +ij_java_keep_first_column_comment = false +ij_java_keep_indents_on_empty_lines = false +ij_java_keep_line_breaks = false +ij_java_keep_multiple_expressions_in_one_line = false +ij_java_keep_simple_blocks_in_one_line = false +ij_java_keep_simple_classes_in_one_line = false +ij_java_keep_simple_lambdas_in_one_line = false +ij_java_keep_simple_methods_in_one_line = false +ij_java_label_indent_absolute = false +ij_java_label_indent_size = 0 +ij_java_lambda_brace_style = end_of_line +ij_java_layout_static_imports_separately = true +ij_java_line_comment_add_space = false +ij_java_line_comment_add_space_on_reformat = false +ij_java_line_comment_at_first_column = true +ij_java_listener_class_prefix = +ij_java_listener_class_suffix = +ij_java_local_variable_name_prefix = +ij_java_local_variable_name_suffix = +ij_java_message_dd_prefix = +ij_java_message_dd_suffix = EJB +ij_java_message_eb_prefix = +ij_java_message_eb_suffix = Bean +ij_java_method_annotation_wrap = split_into_lines +ij_java_method_brace_style = end_of_line +ij_java_method_call_chain_wrap = off +ij_java_method_parameters_new_line_after_left_paren = false +ij_java_method_parameters_right_paren_on_new_line = false +ij_java_method_parameters_wrap = off +ij_java_modifier_list_wrap = false +ij_java_multi_catch_types_wrap = normal +ij_java_names_count_to_use_import_on_demand = 1000 +ij_java_new_line_after_lparen_in_annotation = false +ij_java_new_line_after_lparen_in_deconstruction_pattern = true +ij_java_new_line_after_lparen_in_record_header = false +ij_java_packages_to_use_import_on_demand = +ij_java_parameter_annotation_wrap = off +ij_java_parameter_name_prefix = +ij_java_parameter_name_suffix = +ij_java_parentheses_expression_new_line_after_left_paren = false +ij_java_parentheses_expression_right_paren_on_new_line = false +ij_java_place_assignment_sign_on_next_line = false +ij_java_prefer_longer_names = true +ij_java_prefer_parameters_wrap = false +ij_java_record_components_wrap = normal +ij_java_repeat_annotations = +ij_java_repeat_synchronized = true +ij_java_replace_instanceof_and_cast = false +ij_java_replace_null_check = true +ij_java_replace_sum_lambda_with_method_ref = true +ij_java_resource_list_new_line_after_left_paren = false +ij_java_resource_list_right_paren_on_new_line = false +ij_java_resource_list_wrap = off +ij_java_rparen_on_new_line_in_annotation = false +ij_java_rparen_on_new_line_in_deconstruction_pattern = true +ij_java_rparen_on_new_line_in_record_header = false +ij_java_servlet_class_prefix = +ij_java_servlet_class_suffix = +ij_java_servlet_dd_prefix = +ij_java_servlet_dd_suffix = +ij_java_session_dd_prefix = +ij_java_session_dd_suffix = EJB +ij_java_session_eb_prefix = +ij_java_session_eb_suffix = Bean +ij_java_session_hi_prefix = +ij_java_session_hi_suffix = Home +ij_java_session_lhi_prefix = Local +ij_java_session_lhi_suffix = Home +ij_java_session_li_prefix = Local +ij_java_session_li_suffix = +ij_java_session_ri_prefix = +ij_java_session_ri_suffix = +ij_java_session_si_prefix = +ij_java_session_si_suffix = Service +ij_java_space_after_closing_angle_bracket_in_type_argument = false +ij_java_space_after_colon = true +ij_java_space_after_comma = true +ij_java_space_after_comma_in_type_arguments = true +ij_java_space_after_for_semicolon = true +ij_java_space_after_quest = true +ij_java_space_after_type_cast = false +ij_java_space_before_annotation_array_initializer_left_brace = false +ij_java_space_before_annotation_parameter_list = false +ij_java_space_before_array_initializer_left_brace = false +ij_java_space_before_catch_keyword = true +ij_java_space_before_catch_left_brace = true +ij_java_space_before_catch_parentheses = true +ij_java_space_before_class_left_brace = true +ij_java_space_before_colon = true +ij_java_space_before_colon_in_foreach = true +ij_java_space_before_comma = false +ij_java_space_before_deconstruction_list = false +ij_java_space_before_do_left_brace = true +ij_java_space_before_else_keyword = true +ij_java_space_before_else_left_brace = true +ij_java_space_before_finally_keyword = true +ij_java_space_before_finally_left_brace = true +ij_java_space_before_for_left_brace = true +ij_java_space_before_for_parentheses = true +ij_java_space_before_for_semicolon = false +ij_java_space_before_if_left_brace = true +ij_java_space_before_if_parentheses = true +ij_java_space_before_method_call_parentheses = false +ij_java_space_before_method_left_brace = true +ij_java_space_before_method_parentheses = false +ij_java_space_before_opening_angle_bracket_in_type_parameter = false +ij_java_space_before_quest = true +ij_java_space_before_switch_left_brace = true +ij_java_space_before_switch_parentheses = true +ij_java_space_before_synchronized_left_brace = true +ij_java_space_before_synchronized_parentheses = true +ij_java_space_before_try_left_brace = true +ij_java_space_before_try_parentheses = true +ij_java_space_before_type_parameter_list = false +ij_java_space_before_while_keyword = true +ij_java_space_before_while_left_brace = true +ij_java_space_before_while_parentheses = true +ij_java_space_inside_one_line_enum_braces = false +ij_java_space_within_empty_array_initializer_braces = false +ij_java_space_within_empty_method_call_parentheses = false +ij_java_space_within_empty_method_parentheses = false +ij_java_spaces_around_additive_operators = true +ij_java_spaces_around_annotation_eq = true +ij_java_spaces_around_assignment_operators = true +ij_java_spaces_around_bitwise_operators = true +ij_java_spaces_around_equality_operators = true +ij_java_spaces_around_lambda_arrow = true +ij_java_spaces_around_logical_operators = true +ij_java_spaces_around_method_ref_dbl_colon = false +ij_java_spaces_around_multiplicative_operators = true +ij_java_spaces_around_relational_operators = true +ij_java_spaces_around_shift_operators = true +ij_java_spaces_around_type_bounds_in_type_parameters = true +ij_java_spaces_around_unary_operator = false +ij_java_spaces_within_angle_brackets = false +ij_java_spaces_within_annotation_parentheses = false +ij_java_spaces_within_array_initializer_braces = false +ij_java_spaces_within_braces = false +ij_java_spaces_within_brackets = false +ij_java_spaces_within_cast_parentheses = false +ij_java_spaces_within_catch_parentheses = false +ij_java_spaces_within_deconstruction_list = false +ij_java_spaces_within_for_parentheses = false +ij_java_spaces_within_if_parentheses = false +ij_java_spaces_within_method_call_parentheses = false +ij_java_spaces_within_method_parentheses = false +ij_java_spaces_within_parentheses = false +ij_java_spaces_within_record_header = false +ij_java_spaces_within_switch_parentheses = false +ij_java_spaces_within_synchronized_parentheses = false +ij_java_spaces_within_try_parentheses = false +ij_java_spaces_within_while_parentheses = false +ij_java_special_else_if_treatment = true +ij_java_static_field_name_prefix = +ij_java_static_field_name_suffix = +ij_java_subclass_name_prefix = +ij_java_subclass_name_suffix = Impl +ij_java_ternary_operation_signs_on_next_line = false +ij_java_ternary_operation_wrap = off +ij_java_test_name_prefix = +ij_java_test_name_suffix = Test +ij_java_throws_keyword_wrap = off +ij_java_throws_list_wrap = off +ij_java_use_external_annotations = false +ij_java_use_fq_class_names = false +ij_java_use_relative_indents = false +ij_java_use_single_class_imports = true +ij_java_variable_annotation_wrap = off +ij_java_visibility = public +ij_java_while_brace_force = always +ij_java_while_on_new_line = false +ij_java_wrap_comments = false +ij_java_wrap_first_method_in_call_chain = false +ij_java_wrap_long_lines = false + +[*.less] +indent_size = 2 +ij_less_align_closing_brace_with_properties = false +ij_less_blank_lines_around_nested_selector = 1 +ij_less_blank_lines_between_blocks = 1 +ij_less_block_comment_add_space = false +ij_less_brace_placement = 0 +ij_less_enforce_quotes_on_format = false +ij_less_hex_color_long_format = false +ij_less_hex_color_lower_case = false +ij_less_hex_color_short_format = false +ij_less_hex_color_upper_case = false +ij_less_keep_blank_lines_in_code = 2 +ij_less_keep_indents_on_empty_lines = false +ij_less_keep_single_line_blocks = false +ij_less_line_comment_add_space = false +ij_less_line_comment_at_first_column = false +ij_less_properties_order = font,font-family,font-size,font-weight,font-style,font-variant,font-size-adjust,font-stretch,line-height,position,z-index,top,right,bottom,left,display,visibility,float,clear,overflow,overflow-x,overflow-y,clip,zoom,align-content,align-items,align-self,flex,flex-flow,flex-basis,flex-direction,flex-grow,flex-shrink,flex-wrap,justify-content,order,box-sizing,width,min-width,max-width,height,min-height,max-height,margin,margin-top,margin-right,margin-bottom,margin-left,padding,padding-top,padding-right,padding-bottom,padding-left,table-layout,empty-cells,caption-side,border-spacing,border-collapse,list-style,list-style-position,list-style-type,list-style-image,content,quotes,counter-reset,counter-increment,resize,cursor,user-select,nav-index,nav-up,nav-right,nav-down,nav-left,transition,transition-delay,transition-timing-function,transition-duration,transition-property,transform,transform-origin,animation,animation-name,animation-duration,animation-play-state,animation-timing-function,animation-delay,animation-iteration-count,animation-direction,text-align,text-align-last,vertical-align,white-space,text-decoration,text-emphasis,text-emphasis-color,text-emphasis-style,text-emphasis-position,text-indent,text-justify,letter-spacing,word-spacing,text-outline,text-transform,text-wrap,text-overflow,text-overflow-ellipsis,text-overflow-mode,word-wrap,word-break,tab-size,hyphens,pointer-events,opacity,color,border,border-width,border-style,border-color,border-top,border-top-width,border-top-style,border-top-color,border-right,border-right-width,border-right-style,border-right-color,border-bottom,border-bottom-width,border-bottom-style,border-bottom-color,border-left,border-left-width,border-left-style,border-left-color,border-radius,border-top-left-radius,border-top-right-radius,border-bottom-right-radius,border-bottom-left-radius,border-image,border-image-source,border-image-slice,border-image-width,border-image-outset,border-image-repeat,outline,outline-width,outline-style,outline-color,outline-offset,background,background-color,background-image,background-repeat,background-attachment,background-position,background-position-x,background-position-y,background-clip,background-origin,background-size,box-decoration-break,box-shadow,text-shadow +ij_less_space_after_colon = true +ij_less_space_before_opening_brace = true +ij_less_use_double_quotes = true +ij_less_value_alignment = 0 + +[*.proto] +indent_size = 2 +tab_width = 2 +ij_continuation_indent_size = 4 +ij_protobuf_keep_blank_lines_in_code = 2 +ij_protobuf_keep_indents_on_empty_lines = false +ij_protobuf_keep_line_breaks = true +ij_protobuf_space_after_comma = true +ij_protobuf_space_before_comma = false +ij_protobuf_spaces_around_assignment_operators = true +ij_protobuf_spaces_within_braces = false +ij_protobuf_spaces_within_brackets = false + +[*.sass] +indent_size = 2 +ij_sass_align_closing_brace_with_properties = false +ij_sass_blank_lines_around_nested_selector = 1 +ij_sass_blank_lines_between_blocks = 1 +ij_sass_brace_placement = 0 +ij_sass_enforce_quotes_on_format = false +ij_sass_hex_color_long_format = false +ij_sass_hex_color_lower_case = false +ij_sass_hex_color_short_format = false +ij_sass_hex_color_upper_case = false +ij_sass_keep_blank_lines_in_code = 2 +ij_sass_keep_indents_on_empty_lines = false +ij_sass_keep_single_line_blocks = false +ij_sass_line_comment_add_space = false +ij_sass_line_comment_at_first_column = false +ij_sass_properties_order = font,font-family,font-size,font-weight,font-style,font-variant,font-size-adjust,font-stretch,line-height,position,z-index,top,right,bottom,left,display,visibility,float,clear,overflow,overflow-x,overflow-y,clip,zoom,align-content,align-items,align-self,flex,flex-flow,flex-basis,flex-direction,flex-grow,flex-shrink,flex-wrap,justify-content,order,box-sizing,width,min-width,max-width,height,min-height,max-height,margin,margin-top,margin-right,margin-bottom,margin-left,padding,padding-top,padding-right,padding-bottom,padding-left,table-layout,empty-cells,caption-side,border-spacing,border-collapse,list-style,list-style-position,list-style-type,list-style-image,content,quotes,counter-reset,counter-increment,resize,cursor,user-select,nav-index,nav-up,nav-right,nav-down,nav-left,transition,transition-delay,transition-timing-function,transition-duration,transition-property,transform,transform-origin,animation,animation-name,animation-duration,animation-play-state,animation-timing-function,animation-delay,animation-iteration-count,animation-direction,text-align,text-align-last,vertical-align,white-space,text-decoration,text-emphasis,text-emphasis-color,text-emphasis-style,text-emphasis-position,text-indent,text-justify,letter-spacing,word-spacing,text-outline,text-transform,text-wrap,text-overflow,text-overflow-ellipsis,text-overflow-mode,word-wrap,word-break,tab-size,hyphens,pointer-events,opacity,color,border,border-width,border-style,border-color,border-top,border-top-width,border-top-style,border-top-color,border-right,border-right-width,border-right-style,border-right-color,border-bottom,border-bottom-width,border-bottom-style,border-bottom-color,border-left,border-left-width,border-left-style,border-left-color,border-radius,border-top-left-radius,border-top-right-radius,border-bottom-right-radius,border-bottom-left-radius,border-image,border-image-source,border-image-slice,border-image-width,border-image-outset,border-image-repeat,outline,outline-width,outline-style,outline-color,outline-offset,background,background-color,background-image,background-repeat,background-attachment,background-position,background-position-x,background-position-y,background-clip,background-origin,background-size,box-decoration-break,box-shadow,text-shadow +ij_sass_space_after_colon = true +ij_sass_space_before_opening_brace = true +ij_sass_use_double_quotes = true +ij_sass_value_alignment = 0 + +[*.scss] +indent_size = 2 +ij_scss_align_closing_brace_with_properties = false +ij_scss_blank_lines_around_nested_selector = 1 +ij_scss_blank_lines_between_blocks = 1 +ij_scss_block_comment_add_space = false +ij_scss_brace_placement = 0 +ij_scss_enforce_quotes_on_format = false +ij_scss_hex_color_long_format = false +ij_scss_hex_color_lower_case = false +ij_scss_hex_color_short_format = false +ij_scss_hex_color_upper_case = false +ij_scss_keep_blank_lines_in_code = 2 +ij_scss_keep_indents_on_empty_lines = false +ij_scss_keep_single_line_blocks = false +ij_scss_line_comment_add_space = false +ij_scss_line_comment_at_first_column = false +ij_scss_properties_order = font,font-family,font-size,font-weight,font-style,font-variant,font-size-adjust,font-stretch,line-height,position,z-index,top,right,bottom,left,display,visibility,float,clear,overflow,overflow-x,overflow-y,clip,zoom,align-content,align-items,align-self,flex,flex-flow,flex-basis,flex-direction,flex-grow,flex-shrink,flex-wrap,justify-content,order,box-sizing,width,min-width,max-width,height,min-height,max-height,margin,margin-top,margin-right,margin-bottom,margin-left,padding,padding-top,padding-right,padding-bottom,padding-left,table-layout,empty-cells,caption-side,border-spacing,border-collapse,list-style,list-style-position,list-style-type,list-style-image,content,quotes,counter-reset,counter-increment,resize,cursor,user-select,nav-index,nav-up,nav-right,nav-down,nav-left,transition,transition-delay,transition-timing-function,transition-duration,transition-property,transform,transform-origin,animation,animation-name,animation-duration,animation-play-state,animation-timing-function,animation-delay,animation-iteration-count,animation-direction,text-align,text-align-last,vertical-align,white-space,text-decoration,text-emphasis,text-emphasis-color,text-emphasis-style,text-emphasis-position,text-indent,text-justify,letter-spacing,word-spacing,text-outline,text-transform,text-wrap,text-overflow,text-overflow-ellipsis,text-overflow-mode,word-wrap,word-break,tab-size,hyphens,pointer-events,opacity,color,border,border-width,border-style,border-color,border-top,border-top-width,border-top-style,border-top-color,border-right,border-right-width,border-right-style,border-right-color,border-bottom,border-bottom-width,border-bottom-style,border-bottom-color,border-left,border-left-width,border-left-style,border-left-color,border-radius,border-top-left-radius,border-top-right-radius,border-bottom-right-radius,border-bottom-left-radius,border-image,border-image-source,border-image-slice,border-image-width,border-image-outset,border-image-repeat,outline,outline-width,outline-style,outline-color,outline-offset,background,background-color,background-image,background-repeat,background-attachment,background-position,background-position-x,background-position-y,background-clip,background-origin,background-size,box-decoration-break,box-shadow,text-shadow +ij_scss_space_after_colon = true +ij_scss_space_before_opening_brace = true +ij_scss_use_double_quotes = true +ij_scss_value_alignment = 0 + +[*.vue] +indent_size = 2 +tab_width = 2 +ij_continuation_indent_size = 4 +ij_vue_indent_children_of_top_level = template +ij_vue_interpolation_new_line_after_start_delimiter = true +ij_vue_interpolation_new_line_before_end_delimiter = true +ij_vue_interpolation_wrap = off +ij_vue_keep_indents_on_empty_lines = false +ij_vue_spaces_within_interpolation_expressions = true + +[.editorconfig] +ij_editorconfig_align_group_field_declarations = false +ij_editorconfig_space_after_colon = false +ij_editorconfig_space_after_comma = true +ij_editorconfig_space_before_colon = false +ij_editorconfig_space_before_comma = false +ij_editorconfig_spaces_around_assignment_operators = true + +[{*.ant,*.fxml,*.jhm,*.jnlp,*.jrxml,*.pom,*.rng,*.tld,*.wadl,*.wsdl,*.xml,*.xsd,*.xsl,*.xslt,*.xul}] +ij_xml_align_attributes = true +ij_xml_align_text = false +ij_xml_attribute_wrap = normal +ij_xml_block_comment_add_space = false +ij_xml_block_comment_at_first_column = true +ij_xml_keep_blank_lines = 2 +ij_xml_keep_indents_on_empty_lines = false +ij_xml_keep_line_breaks = true +ij_xml_keep_line_breaks_in_text = true +ij_xml_keep_whitespaces = false +ij_xml_keep_whitespaces_around_cdata = preserve +ij_xml_keep_whitespaces_inside_cdata = false +ij_xml_line_comment_at_first_column = true +ij_xml_space_after_tag_name = false +ij_xml_space_around_equals_in_attribute = false +ij_xml_space_inside_empty_tag = false +ij_xml_text_wrap = normal +ij_xml_use_custom_settings = false + +[{*.ats,*.cts,*.mts,*.ts}] +ij_continuation_indent_size = 4 +ij_typescript_align_imports = false +ij_typescript_align_multiline_array_initializer_expression = false +ij_typescript_align_multiline_binary_operation = false +ij_typescript_align_multiline_chained_methods = false +ij_typescript_align_multiline_extends_list = false +ij_typescript_align_multiline_for = true +ij_typescript_align_multiline_parameters = true +ij_typescript_align_multiline_parameters_in_calls = false +ij_typescript_align_multiline_ternary_operation = false +ij_typescript_align_object_properties = 0 +ij_typescript_align_union_types = false +ij_typescript_align_var_statements = 0 +ij_typescript_array_initializer_new_line_after_left_brace = false +ij_typescript_array_initializer_right_brace_on_new_line = false +ij_typescript_array_initializer_wrap = off +ij_typescript_assignment_wrap = off +ij_typescript_binary_operation_sign_on_next_line = false +ij_typescript_binary_operation_wrap = off +ij_typescript_blacklist_imports = rxjs/Rx,node_modules/**,**/node_modules/**,@angular/material,@angular/material/typings/** +ij_typescript_blank_lines_after_imports = 1 +ij_typescript_blank_lines_around_class = 1 +ij_typescript_blank_lines_around_field = 0 +ij_typescript_blank_lines_around_field_in_interface = 0 +ij_typescript_blank_lines_around_function = 1 +ij_typescript_blank_lines_around_method = 1 +ij_typescript_blank_lines_around_method_in_interface = 1 +ij_typescript_block_brace_style = end_of_line +ij_typescript_block_comment_add_space = false +ij_typescript_block_comment_at_first_column = true +ij_typescript_call_parameters_new_line_after_left_paren = false +ij_typescript_call_parameters_right_paren_on_new_line = false +ij_typescript_call_parameters_wrap = off +ij_typescript_catch_on_new_line = false +ij_typescript_chained_call_dot_on_new_line = true +ij_typescript_class_brace_style = end_of_line +ij_typescript_comma_on_new_line = false +ij_typescript_do_while_brace_force = never +ij_typescript_else_on_new_line = false +ij_typescript_enforce_trailing_comma = keep +ij_typescript_enum_constants_wrap = on_every_item +ij_typescript_extends_keyword_wrap = off +ij_typescript_extends_list_wrap = off +ij_typescript_field_prefix = _ +ij_typescript_file_name_style = relaxed +ij_typescript_finally_on_new_line = false +ij_typescript_for_brace_force = never +ij_typescript_for_statement_new_line_after_left_paren = false +ij_typescript_for_statement_right_paren_on_new_line = false +ij_typescript_for_statement_wrap = off +ij_typescript_force_quote_style = false +ij_typescript_force_semicolon_style = false +ij_typescript_function_expression_brace_style = end_of_line +ij_typescript_if_brace_force = never +ij_typescript_import_merge_members = global +ij_typescript_import_prefer_absolute_path = global +ij_typescript_import_sort_members = true +ij_typescript_import_sort_module_name = false +ij_typescript_import_use_node_resolution = true +ij_typescript_imports_wrap = on_every_item +ij_typescript_indent_case_from_switch = true +ij_typescript_indent_chained_calls = true +ij_typescript_indent_package_children = 0 +ij_typescript_jsdoc_include_types = false +ij_typescript_jsx_attribute_value = braces +ij_typescript_keep_blank_lines_in_code = 2 +ij_typescript_keep_first_column_comment = true +ij_typescript_keep_indents_on_empty_lines = false +ij_typescript_keep_line_breaks = true +ij_typescript_keep_simple_blocks_in_one_line = false +ij_typescript_keep_simple_methods_in_one_line = false +ij_typescript_line_comment_add_space = true +ij_typescript_line_comment_at_first_column = false +ij_typescript_method_brace_style = end_of_line +ij_typescript_method_call_chain_wrap = off +ij_typescript_method_parameters_new_line_after_left_paren = false +ij_typescript_method_parameters_right_paren_on_new_line = false +ij_typescript_method_parameters_wrap = off +ij_typescript_object_literal_wrap = on_every_item +ij_typescript_object_types_wrap = on_every_item +ij_typescript_parentheses_expression_new_line_after_left_paren = false +ij_typescript_parentheses_expression_right_paren_on_new_line = false +ij_typescript_place_assignment_sign_on_next_line = false +ij_typescript_prefer_as_type_cast = false +ij_typescript_prefer_explicit_types_function_expression_returns = false +ij_typescript_prefer_explicit_types_function_returns = false +ij_typescript_prefer_explicit_types_vars_fields = false +ij_typescript_prefer_parameters_wrap = false +ij_typescript_property_prefix = +ij_typescript_reformat_c_style_comments = false +ij_typescript_space_after_colon = true +ij_typescript_space_after_comma = true +ij_typescript_space_after_dots_in_rest_parameter = false +ij_typescript_space_after_generator_mult = true +ij_typescript_space_after_property_colon = true +ij_typescript_space_after_quest = true +ij_typescript_space_after_type_colon = true +ij_typescript_space_after_unary_not = false +ij_typescript_space_before_async_arrow_lparen = true +ij_typescript_space_before_catch_keyword = true +ij_typescript_space_before_catch_left_brace = true +ij_typescript_space_before_catch_parentheses = true +ij_typescript_space_before_class_lbrace = true +ij_typescript_space_before_class_left_brace = true +ij_typescript_space_before_colon = true +ij_typescript_space_before_comma = false +ij_typescript_space_before_do_left_brace = true +ij_typescript_space_before_else_keyword = true +ij_typescript_space_before_else_left_brace = true +ij_typescript_space_before_finally_keyword = true +ij_typescript_space_before_finally_left_brace = true +ij_typescript_space_before_for_left_brace = true +ij_typescript_space_before_for_parentheses = true +ij_typescript_space_before_for_semicolon = false +ij_typescript_space_before_function_left_parenth = true +ij_typescript_space_before_generator_mult = false +ij_typescript_space_before_if_left_brace = true +ij_typescript_space_before_if_parentheses = true +ij_typescript_space_before_method_call_parentheses = false +ij_typescript_space_before_method_left_brace = true +ij_typescript_space_before_method_parentheses = false +ij_typescript_space_before_property_colon = false +ij_typescript_space_before_quest = true +ij_typescript_space_before_switch_left_brace = true +ij_typescript_space_before_switch_parentheses = true +ij_typescript_space_before_try_left_brace = true +ij_typescript_space_before_type_colon = false +ij_typescript_space_before_unary_not = false +ij_typescript_space_before_while_keyword = true +ij_typescript_space_before_while_left_brace = true +ij_typescript_space_before_while_parentheses = true +ij_typescript_spaces_around_additive_operators = true +ij_typescript_spaces_around_arrow_function_operator = true +ij_typescript_spaces_around_assignment_operators = true +ij_typescript_spaces_around_bitwise_operators = true +ij_typescript_spaces_around_equality_operators = true +ij_typescript_spaces_around_logical_operators = true +ij_typescript_spaces_around_multiplicative_operators = true +ij_typescript_spaces_around_relational_operators = true +ij_typescript_spaces_around_shift_operators = true +ij_typescript_spaces_around_unary_operator = false +ij_typescript_spaces_within_array_initializer_brackets = false +ij_typescript_spaces_within_brackets = false +ij_typescript_spaces_within_catch_parentheses = false +ij_typescript_spaces_within_for_parentheses = false +ij_typescript_spaces_within_if_parentheses = false +ij_typescript_spaces_within_imports = false +ij_typescript_spaces_within_interpolation_expressions = false +ij_typescript_spaces_within_method_call_parentheses = false +ij_typescript_spaces_within_method_parentheses = false +ij_typescript_spaces_within_object_literal_braces = false +ij_typescript_spaces_within_object_type_braces = true +ij_typescript_spaces_within_parentheses = false +ij_typescript_spaces_within_switch_parentheses = false +ij_typescript_spaces_within_type_assertion = false +ij_typescript_spaces_within_union_types = true +ij_typescript_spaces_within_while_parentheses = false +ij_typescript_special_else_if_treatment = true +ij_typescript_ternary_operation_signs_on_next_line = false +ij_typescript_ternary_operation_wrap = off +ij_typescript_union_types_wrap = on_every_item +ij_typescript_use_chained_calls_group_indents = false +ij_typescript_use_double_quotes = true +ij_typescript_use_explicit_js_extension = auto +ij_typescript_use_path_mapping = always +ij_typescript_use_public_modifier = false +ij_typescript_use_semicolon_after_statement = true +ij_typescript_var_declaration_wrap = normal +ij_typescript_while_brace_force = never +ij_typescript_while_on_new_line = false +ij_typescript_wrap_comments = false + +[{*.bash,*.sh,*.zsh}] +indent_size = 2 +tab_width = 2 +ij_shell_binary_ops_start_line = false +ij_shell_keep_column_alignment_padding = false +ij_shell_minify_program = false +ij_shell_redirect_followed_by_space = false +ij_shell_switch_cases_indented = false +ij_shell_use_unix_line_separator = true + +[{*.cjs,*.js}] +ij_continuation_indent_size = 4 +ij_javascript_align_imports = false +ij_javascript_align_multiline_array_initializer_expression = false +ij_javascript_align_multiline_binary_operation = false +ij_javascript_align_multiline_chained_methods = false +ij_javascript_align_multiline_extends_list = false +ij_javascript_align_multiline_for = true +ij_javascript_align_multiline_parameters = true +ij_javascript_align_multiline_parameters_in_calls = false +ij_javascript_align_multiline_ternary_operation = false +ij_javascript_align_object_properties = 0 +ij_javascript_align_union_types = false +ij_javascript_align_var_statements = 0 +ij_javascript_array_initializer_new_line_after_left_brace = false +ij_javascript_array_initializer_right_brace_on_new_line = false +ij_javascript_array_initializer_wrap = off +ij_javascript_assignment_wrap = off +ij_javascript_binary_operation_sign_on_next_line = false +ij_javascript_binary_operation_wrap = off +ij_javascript_blacklist_imports = rxjs/Rx,node_modules/**,**/node_modules/**,@angular/material,@angular/material/typings/** +ij_javascript_blank_lines_after_imports = 1 +ij_javascript_blank_lines_around_class = 1 +ij_javascript_blank_lines_around_field = 0 +ij_javascript_blank_lines_around_function = 1 +ij_javascript_blank_lines_around_method = 1 +ij_javascript_block_brace_style = end_of_line +ij_javascript_block_comment_add_space = false +ij_javascript_block_comment_at_first_column = true +ij_javascript_call_parameters_new_line_after_left_paren = false +ij_javascript_call_parameters_right_paren_on_new_line = false +ij_javascript_call_parameters_wrap = off +ij_javascript_catch_on_new_line = false +ij_javascript_chained_call_dot_on_new_line = true +ij_javascript_class_brace_style = end_of_line +ij_javascript_comma_on_new_line = false +ij_javascript_do_while_brace_force = never +ij_javascript_else_on_new_line = false +ij_javascript_enforce_trailing_comma = keep +ij_javascript_extends_keyword_wrap = off +ij_javascript_extends_list_wrap = off +ij_javascript_field_prefix = _ +ij_javascript_file_name_style = relaxed +ij_javascript_finally_on_new_line = false +ij_javascript_for_brace_force = never +ij_javascript_for_statement_new_line_after_left_paren = false +ij_javascript_for_statement_right_paren_on_new_line = false +ij_javascript_for_statement_wrap = off +ij_javascript_force_quote_style = false +ij_javascript_force_semicolon_style = false +ij_javascript_function_expression_brace_style = end_of_line +ij_javascript_if_brace_force = never +ij_javascript_import_merge_members = global +ij_javascript_import_prefer_absolute_path = global +ij_javascript_import_sort_members = true +ij_javascript_import_sort_module_name = false +ij_javascript_import_use_node_resolution = true +ij_javascript_imports_wrap = on_every_item +ij_javascript_indent_case_from_switch = true +ij_javascript_indent_chained_calls = true +ij_javascript_indent_package_children = 0 +ij_javascript_jsx_attribute_value = braces +ij_javascript_keep_blank_lines_in_code = 2 +ij_javascript_keep_first_column_comment = true +ij_javascript_keep_indents_on_empty_lines = false +ij_javascript_keep_line_breaks = true +ij_javascript_keep_simple_blocks_in_one_line = false +ij_javascript_keep_simple_methods_in_one_line = false +ij_javascript_line_comment_add_space = true +ij_javascript_line_comment_at_first_column = false +ij_javascript_method_brace_style = end_of_line +ij_javascript_method_call_chain_wrap = off +ij_javascript_method_parameters_new_line_after_left_paren = false +ij_javascript_method_parameters_right_paren_on_new_line = false +ij_javascript_method_parameters_wrap = off +ij_javascript_object_literal_wrap = on_every_item +ij_javascript_object_types_wrap = on_every_item +ij_javascript_parentheses_expression_new_line_after_left_paren = false +ij_javascript_parentheses_expression_right_paren_on_new_line = false +ij_javascript_place_assignment_sign_on_next_line = false +ij_javascript_prefer_as_type_cast = false +ij_javascript_prefer_explicit_types_function_expression_returns = false +ij_javascript_prefer_explicit_types_function_returns = false +ij_javascript_prefer_explicit_types_vars_fields = false +ij_javascript_prefer_parameters_wrap = false +ij_javascript_property_prefix = +ij_javascript_reformat_c_style_comments = false +ij_javascript_space_after_colon = true +ij_javascript_space_after_comma = true +ij_javascript_space_after_dots_in_rest_parameter = false +ij_javascript_space_after_generator_mult = true +ij_javascript_space_after_property_colon = true +ij_javascript_space_after_quest = true +ij_javascript_space_after_type_colon = true +ij_javascript_space_after_unary_not = false +ij_javascript_space_before_async_arrow_lparen = true +ij_javascript_space_before_catch_keyword = true +ij_javascript_space_before_catch_left_brace = true +ij_javascript_space_before_catch_parentheses = true +ij_javascript_space_before_class_lbrace = true +ij_javascript_space_before_class_left_brace = true +ij_javascript_space_before_colon = true +ij_javascript_space_before_comma = false +ij_javascript_space_before_do_left_brace = true +ij_javascript_space_before_else_keyword = true +ij_javascript_space_before_else_left_brace = true +ij_javascript_space_before_finally_keyword = true +ij_javascript_space_before_finally_left_brace = true +ij_javascript_space_before_for_left_brace = true +ij_javascript_space_before_for_parentheses = true +ij_javascript_space_before_for_semicolon = false +ij_javascript_space_before_function_left_parenth = true +ij_javascript_space_before_generator_mult = false +ij_javascript_space_before_if_left_brace = true +ij_javascript_space_before_if_parentheses = true +ij_javascript_space_before_method_call_parentheses = false +ij_javascript_space_before_method_left_brace = true +ij_javascript_space_before_method_parentheses = false +ij_javascript_space_before_property_colon = false +ij_javascript_space_before_quest = true +ij_javascript_space_before_switch_left_brace = true +ij_javascript_space_before_switch_parentheses = true +ij_javascript_space_before_try_left_brace = true +ij_javascript_space_before_type_colon = false +ij_javascript_space_before_unary_not = false +ij_javascript_space_before_while_keyword = true +ij_javascript_space_before_while_left_brace = true +ij_javascript_space_before_while_parentheses = true +ij_javascript_spaces_around_additive_operators = true +ij_javascript_spaces_around_arrow_function_operator = true +ij_javascript_spaces_around_assignment_operators = true +ij_javascript_spaces_around_bitwise_operators = true +ij_javascript_spaces_around_equality_operators = true +ij_javascript_spaces_around_logical_operators = true +ij_javascript_spaces_around_multiplicative_operators = true +ij_javascript_spaces_around_relational_operators = true +ij_javascript_spaces_around_shift_operators = true +ij_javascript_spaces_around_unary_operator = false +ij_javascript_spaces_within_array_initializer_brackets = false +ij_javascript_spaces_within_brackets = false +ij_javascript_spaces_within_catch_parentheses = false +ij_javascript_spaces_within_for_parentheses = false +ij_javascript_spaces_within_if_parentheses = false +ij_javascript_spaces_within_imports = false +ij_javascript_spaces_within_interpolation_expressions = false +ij_javascript_spaces_within_method_call_parentheses = false +ij_javascript_spaces_within_method_parentheses = false +ij_javascript_spaces_within_object_literal_braces = false +ij_javascript_spaces_within_object_type_braces = true +ij_javascript_spaces_within_parentheses = false +ij_javascript_spaces_within_switch_parentheses = false +ij_javascript_spaces_within_type_assertion = false +ij_javascript_spaces_within_union_types = true +ij_javascript_spaces_within_while_parentheses = false +ij_javascript_special_else_if_treatment = true +ij_javascript_ternary_operation_signs_on_next_line = false +ij_javascript_ternary_operation_wrap = off +ij_javascript_union_types_wrap = on_every_item +ij_javascript_use_chained_calls_group_indents = false +ij_javascript_use_double_quotes = true +ij_javascript_use_explicit_js_extension = auto +ij_javascript_use_path_mapping = always +ij_javascript_use_public_modifier = false +ij_javascript_use_semicolon_after_statement = true +ij_javascript_var_declaration_wrap = normal +ij_javascript_while_brace_force = never +ij_javascript_while_on_new_line = false +ij_javascript_wrap_comments = false + +[{*.ft,*.vm,*.vsl}] +ij_vtl_keep_indents_on_empty_lines = false + +[{*.gant,*.groovy,*.gy}] +ij_groovy_align_group_field_declarations = false +ij_groovy_align_multiline_array_initializer_expression = false +ij_groovy_align_multiline_assignment = false +ij_groovy_align_multiline_binary_operation = false +ij_groovy_align_multiline_chained_methods = false +ij_groovy_align_multiline_extends_list = false +ij_groovy_align_multiline_for = true +ij_groovy_align_multiline_list_or_map = true +ij_groovy_align_multiline_method_parentheses = false +ij_groovy_align_multiline_parameters = true +ij_groovy_align_multiline_parameters_in_calls = false +ij_groovy_align_multiline_resources = true +ij_groovy_align_multiline_ternary_operation = false +ij_groovy_align_multiline_throws_list = false +ij_groovy_align_named_args_in_map = true +ij_groovy_align_throws_keyword = false +ij_groovy_array_initializer_new_line_after_left_brace = false +ij_groovy_array_initializer_right_brace_on_new_line = false +ij_groovy_array_initializer_wrap = off +ij_groovy_assert_statement_wrap = off +ij_groovy_assignment_wrap = off +ij_groovy_binary_operation_wrap = off +ij_groovy_blank_lines_after_class_header = 0 +ij_groovy_blank_lines_after_imports = 1 +ij_groovy_blank_lines_after_package = 1 +ij_groovy_blank_lines_around_class = 1 +ij_groovy_blank_lines_around_field = 0 +ij_groovy_blank_lines_around_field_in_interface = 0 +ij_groovy_blank_lines_around_method = 1 +ij_groovy_blank_lines_around_method_in_interface = 1 +ij_groovy_blank_lines_before_imports = 1 +ij_groovy_blank_lines_before_method_body = 0 +ij_groovy_blank_lines_before_package = 0 +ij_groovy_block_brace_style = end_of_line +ij_groovy_block_comment_add_space = false +ij_groovy_block_comment_at_first_column = true +ij_groovy_call_parameters_new_line_after_left_paren = false +ij_groovy_call_parameters_right_paren_on_new_line = false +ij_groovy_call_parameters_wrap = off +ij_groovy_catch_on_new_line = false +ij_groovy_class_annotation_wrap = split_into_lines +ij_groovy_class_brace_style = end_of_line +ij_groovy_class_count_to_use_import_on_demand = 5 +ij_groovy_do_while_brace_force = never +ij_groovy_else_on_new_line = false +ij_groovy_enable_groovydoc_formatting = true +ij_groovy_enum_constants_wrap = off +ij_groovy_extends_keyword_wrap = off +ij_groovy_extends_list_wrap = off +ij_groovy_field_annotation_wrap = split_into_lines +ij_groovy_finally_on_new_line = false +ij_groovy_for_brace_force = never +ij_groovy_for_statement_new_line_after_left_paren = false +ij_groovy_for_statement_right_paren_on_new_line = false +ij_groovy_for_statement_wrap = off +ij_groovy_ginq_general_clause_wrap_policy = 2 +ij_groovy_ginq_having_wrap_policy = 1 +ij_groovy_ginq_indent_having_clause = true +ij_groovy_ginq_indent_on_clause = true +ij_groovy_ginq_on_wrap_policy = 1 +ij_groovy_ginq_space_after_keyword = true +ij_groovy_if_brace_force = never +ij_groovy_import_annotation_wrap = 2 +ij_groovy_imports_layout = *,|,javax.**,java.**,|,$* +ij_groovy_indent_case_from_switch = true +ij_groovy_indent_label_blocks = true +ij_groovy_insert_inner_class_imports = false +ij_groovy_keep_blank_lines_before_right_brace = 2 +ij_groovy_keep_blank_lines_in_code = 2 +ij_groovy_keep_blank_lines_in_declarations = 2 +ij_groovy_keep_control_statement_in_one_line = true +ij_groovy_keep_first_column_comment = true +ij_groovy_keep_indents_on_empty_lines = false +ij_groovy_keep_line_breaks = true +ij_groovy_keep_multiple_expressions_in_one_line = false +ij_groovy_keep_simple_blocks_in_one_line = false +ij_groovy_keep_simple_classes_in_one_line = true +ij_groovy_keep_simple_lambdas_in_one_line = true +ij_groovy_keep_simple_methods_in_one_line = true +ij_groovy_label_indent_absolute = false +ij_groovy_label_indent_size = 0 +ij_groovy_lambda_brace_style = end_of_line +ij_groovy_layout_static_imports_separately = true +ij_groovy_line_comment_add_space = false +ij_groovy_line_comment_add_space_on_reformat = false +ij_groovy_line_comment_at_first_column = true +ij_groovy_method_annotation_wrap = split_into_lines +ij_groovy_method_brace_style = end_of_line +ij_groovy_method_call_chain_wrap = off +ij_groovy_method_parameters_new_line_after_left_paren = false +ij_groovy_method_parameters_right_paren_on_new_line = false +ij_groovy_method_parameters_wrap = off +ij_groovy_modifier_list_wrap = false +ij_groovy_names_count_to_use_import_on_demand = 3 +ij_groovy_packages_to_use_import_on_demand = java.awt.*,javax.swing.* +ij_groovy_parameter_annotation_wrap = off +ij_groovy_parentheses_expression_new_line_after_left_paren = false +ij_groovy_parentheses_expression_right_paren_on_new_line = false +ij_groovy_prefer_parameters_wrap = false +ij_groovy_resource_list_new_line_after_left_paren = false +ij_groovy_resource_list_right_paren_on_new_line = false +ij_groovy_resource_list_wrap = off +ij_groovy_space_after_assert_separator = true +ij_groovy_space_after_colon = true +ij_groovy_space_after_comma = true +ij_groovy_space_after_comma_in_type_arguments = true +ij_groovy_space_after_for_semicolon = true +ij_groovy_space_after_quest = true +ij_groovy_space_after_type_cast = true +ij_groovy_space_before_annotation_parameter_list = false +ij_groovy_space_before_array_initializer_left_brace = false +ij_groovy_space_before_assert_separator = false +ij_groovy_space_before_catch_keyword = true +ij_groovy_space_before_catch_left_brace = true +ij_groovy_space_before_catch_parentheses = true +ij_groovy_space_before_class_left_brace = true +ij_groovy_space_before_closure_left_brace = true +ij_groovy_space_before_colon = true +ij_groovy_space_before_comma = false +ij_groovy_space_before_do_left_brace = true +ij_groovy_space_before_else_keyword = true +ij_groovy_space_before_else_left_brace = true +ij_groovy_space_before_finally_keyword = true +ij_groovy_space_before_finally_left_brace = true +ij_groovy_space_before_for_left_brace = true +ij_groovy_space_before_for_parentheses = true +ij_groovy_space_before_for_semicolon = false +ij_groovy_space_before_if_left_brace = true +ij_groovy_space_before_if_parentheses = true +ij_groovy_space_before_method_call_parentheses = false +ij_groovy_space_before_method_left_brace = true +ij_groovy_space_before_method_parentheses = false +ij_groovy_space_before_quest = true +ij_groovy_space_before_record_parentheses = false +ij_groovy_space_before_switch_left_brace = true +ij_groovy_space_before_switch_parentheses = true +ij_groovy_space_before_synchronized_left_brace = true +ij_groovy_space_before_synchronized_parentheses = true +ij_groovy_space_before_try_left_brace = true +ij_groovy_space_before_try_parentheses = true +ij_groovy_space_before_while_keyword = true +ij_groovy_space_before_while_left_brace = true +ij_groovy_space_before_while_parentheses = true +ij_groovy_space_in_named_argument = true +ij_groovy_space_in_named_argument_before_colon = false +ij_groovy_space_within_empty_array_initializer_braces = false +ij_groovy_space_within_empty_method_call_parentheses = false +ij_groovy_spaces_around_additive_operators = true +ij_groovy_spaces_around_assignment_operators = true +ij_groovy_spaces_around_bitwise_operators = true +ij_groovy_spaces_around_equality_operators = true +ij_groovy_spaces_around_lambda_arrow = true +ij_groovy_spaces_around_logical_operators = true +ij_groovy_spaces_around_multiplicative_operators = true +ij_groovy_spaces_around_regex_operators = true +ij_groovy_spaces_around_relational_operators = true +ij_groovy_spaces_around_shift_operators = true +ij_groovy_spaces_within_annotation_parentheses = false +ij_groovy_spaces_within_array_initializer_braces = false +ij_groovy_spaces_within_braces = true +ij_groovy_spaces_within_brackets = false +ij_groovy_spaces_within_cast_parentheses = false +ij_groovy_spaces_within_catch_parentheses = false +ij_groovy_spaces_within_for_parentheses = false +ij_groovy_spaces_within_gstring_injection_braces = false +ij_groovy_spaces_within_if_parentheses = false +ij_groovy_spaces_within_list_or_map = false +ij_groovy_spaces_within_method_call_parentheses = false +ij_groovy_spaces_within_method_parentheses = false +ij_groovy_spaces_within_parentheses = false +ij_groovy_spaces_within_switch_parentheses = false +ij_groovy_spaces_within_synchronized_parentheses = false +ij_groovy_spaces_within_try_parentheses = false +ij_groovy_spaces_within_tuple_expression = false +ij_groovy_spaces_within_while_parentheses = false +ij_groovy_special_else_if_treatment = true +ij_groovy_ternary_operation_wrap = off +ij_groovy_throws_keyword_wrap = off +ij_groovy_throws_list_wrap = off +ij_groovy_use_flying_geese_braces = false +ij_groovy_use_fq_class_names = false +ij_groovy_use_fq_class_names_in_javadoc = true +ij_groovy_use_relative_indents = false +ij_groovy_use_single_class_imports = true +ij_groovy_variable_annotation_wrap = off +ij_groovy_while_brace_force = never +ij_groovy_while_on_new_line = false +ij_groovy_wrap_chain_calls_after_dot = false +ij_groovy_wrap_long_lines = false + +[{*.har,*.jsb2,*.jsb3,*.json,.babelrc,.eslintrc,.prettierrc,.stylelintrc,bowerrc,jest.config}] +indent_size = 2 +ij_json_array_wrapping = split_into_lines +ij_json_keep_blank_lines_in_code = 0 +ij_json_keep_indents_on_empty_lines = false +ij_json_keep_line_breaks = true +ij_json_keep_trailing_comma = false +ij_json_object_wrapping = split_into_lines +ij_json_property_alignment = do_not_align +ij_json_space_after_colon = true +ij_json_space_after_comma = true +ij_json_space_before_colon = false +ij_json_space_before_comma = false +ij_json_spaces_within_braces = false +ij_json_spaces_within_brackets = false +ij_json_wrap_long_lines = false + +[{*.htm,*.html,*.ng,*.sht,*.shtm,*.shtml}] +ij_html_add_new_line_before_tags = body,div,p,form,h1,h2,h3 +ij_html_align_attributes = true +ij_html_align_text = false +ij_html_attribute_wrap = normal +ij_html_block_comment_add_space = false +ij_html_block_comment_at_first_column = true +ij_html_do_not_align_children_of_min_lines = 0 +ij_html_do_not_break_if_inline_tags = title,h1,h2,h3,h4,h5,h6,p +ij_html_do_not_indent_children_of_tags = html,body,thead,tbody,tfoot +ij_html_enforce_quotes = false +ij_html_inline_tags = a,abbr,acronym,b,basefont,bdo,big,br,cite,cite,code,dfn,em,font,i,img,input,kbd,label,q,s,samp,select,small,span,strike,strong,sub,sup,textarea,tt,u,var +ij_html_keep_blank_lines = 2 +ij_html_keep_indents_on_empty_lines = false +ij_html_keep_line_breaks = true +ij_html_keep_line_breaks_in_text = true +ij_html_keep_whitespaces = false +ij_html_keep_whitespaces_inside = span,pre,textarea +ij_html_line_comment_at_first_column = true +ij_html_new_line_after_last_attribute = never +ij_html_new_line_before_first_attribute = never +ij_html_quote_style = double +ij_html_remove_new_line_before_tags = br +ij_html_space_after_tag_name = false +ij_html_space_around_equality_in_attribute = false +ij_html_space_inside_empty_tag = false +ij_html_text_wrap = normal + +[{*.http,*.rest}] +indent_size = 0 +ij_continuation_indent_size = 4 +ij_http-request_call_parameters_wrap = normal +ij_http-request_method_parameters_wrap = split_into_lines +ij_http-request_space_before_comma = true +ij_http-request_spaces_around_assignment_operators = true + +[{*.jsf,*.jsp,*.jspf,*.tag,*.tagf,*.xjsp}] +ij_jsp_jsp_prefer_comma_separated_import_list = false +ij_jsp_keep_indents_on_empty_lines = false + +[{*.jspx,*.tagx}] +ij_jspx_keep_indents_on_empty_lines = false + +[{*.kt,*.kts}] +ij_kotlin_align_in_columns_case_branch = false +ij_kotlin_align_multiline_binary_operation = false +ij_kotlin_align_multiline_extends_list = false +ij_kotlin_align_multiline_method_parentheses = false +ij_kotlin_align_multiline_parameters = true +ij_kotlin_align_multiline_parameters_in_calls = false +ij_kotlin_allow_trailing_comma = false +ij_kotlin_allow_trailing_comma_on_call_site = false +ij_kotlin_assignment_wrap = normal +ij_kotlin_blank_lines_after_class_header = 0 +ij_kotlin_blank_lines_around_block_when_branches = 0 +ij_kotlin_blank_lines_before_declaration_with_comment_or_annotation_on_separate_line = 1 +ij_kotlin_block_comment_add_space = false +ij_kotlin_block_comment_at_first_column = true +ij_kotlin_call_parameters_new_line_after_left_paren = true +ij_kotlin_call_parameters_right_paren_on_new_line = true +ij_kotlin_call_parameters_wrap = on_every_item +ij_kotlin_catch_on_new_line = false +ij_kotlin_class_annotation_wrap = split_into_lines +ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL +ij_kotlin_continuation_indent_for_chained_calls = false +ij_kotlin_continuation_indent_for_expression_bodies = false +ij_kotlin_continuation_indent_in_argument_lists = false +ij_kotlin_continuation_indent_in_elvis = false +ij_kotlin_continuation_indent_in_if_conditions = false +ij_kotlin_continuation_indent_in_parameter_lists = false +ij_kotlin_continuation_indent_in_supertype_lists = false +ij_kotlin_else_on_new_line = false +ij_kotlin_enum_constants_wrap = off +ij_kotlin_extends_list_wrap = normal +ij_kotlin_field_annotation_wrap = split_into_lines +ij_kotlin_finally_on_new_line = false +ij_kotlin_if_rparen_on_new_line = true +ij_kotlin_import_nested_classes = false +ij_kotlin_imports_layout = *,java.**,javax.**,kotlin.**,^ +ij_kotlin_insert_whitespaces_in_simple_one_line_method = true +ij_kotlin_keep_blank_lines_before_right_brace = 2 +ij_kotlin_keep_blank_lines_in_code = 2 +ij_kotlin_keep_blank_lines_in_declarations = 2 +ij_kotlin_keep_first_column_comment = true +ij_kotlin_keep_indents_on_empty_lines = false +ij_kotlin_keep_line_breaks = true +ij_kotlin_lbrace_on_next_line = false +ij_kotlin_line_break_after_multiline_when_entry = true +ij_kotlin_line_comment_add_space = false +ij_kotlin_line_comment_add_space_on_reformat = false +ij_kotlin_line_comment_at_first_column = true +ij_kotlin_method_annotation_wrap = split_into_lines +ij_kotlin_method_call_chain_wrap = normal +ij_kotlin_method_parameters_new_line_after_left_paren = true +ij_kotlin_method_parameters_right_paren_on_new_line = true +ij_kotlin_method_parameters_wrap = on_every_item +ij_kotlin_name_count_to_use_star_import = 5 +ij_kotlin_name_count_to_use_star_import_for_members = 3 +ij_kotlin_packages_to_use_import_on_demand = java.util.*,kotlinx.android.synthetic.**,io.ktor.** +ij_kotlin_parameter_annotation_wrap = off +ij_kotlin_space_after_comma = true +ij_kotlin_space_after_extend_colon = true +ij_kotlin_space_after_type_colon = true +ij_kotlin_space_before_catch_parentheses = true +ij_kotlin_space_before_comma = false +ij_kotlin_space_before_extend_colon = true +ij_kotlin_space_before_for_parentheses = true +ij_kotlin_space_before_if_parentheses = true +ij_kotlin_space_before_lambda_arrow = true +ij_kotlin_space_before_type_colon = false +ij_kotlin_space_before_when_parentheses = true +ij_kotlin_space_before_while_parentheses = true +ij_kotlin_spaces_around_additive_operators = true +ij_kotlin_spaces_around_assignment_operators = true +ij_kotlin_spaces_around_equality_operators = true +ij_kotlin_spaces_around_function_type_arrow = true +ij_kotlin_spaces_around_logical_operators = true +ij_kotlin_spaces_around_multiplicative_operators = true +ij_kotlin_spaces_around_range = false +ij_kotlin_spaces_around_relational_operators = true +ij_kotlin_spaces_around_unary_operator = false +ij_kotlin_spaces_around_when_arrow = true +ij_kotlin_variable_annotation_wrap = off +ij_kotlin_while_on_new_line = false +ij_kotlin_wrap_elvis_expressions = 1 +ij_kotlin_wrap_expression_body_functions = 1 +ij_kotlin_wrap_first_method_in_call_chain = false + +[{*.markdown,*.md}] +ij_markdown_force_one_space_after_blockquote_symbol = true +ij_markdown_force_one_space_after_header_symbol = true +ij_markdown_force_one_space_after_list_bullet = true +ij_markdown_force_one_space_between_words = true +ij_markdown_format_tables = true +ij_markdown_insert_quote_arrows_on_wrap = true +ij_markdown_keep_indents_on_empty_lines = false +ij_markdown_keep_line_breaks_inside_text_blocks = true +ij_markdown_max_lines_around_block_elements = 1 +ij_markdown_max_lines_around_header = 1 +ij_markdown_max_lines_between_paragraphs = 1 +ij_markdown_min_lines_around_block_elements = 1 +ij_markdown_min_lines_around_header = 1 +ij_markdown_min_lines_between_paragraphs = 1 +ij_markdown_wrap_text_if_long = true +ij_markdown_wrap_text_inside_blockquotes = true + +[{*.pb,*.textproto}] +indent_size = 2 +tab_width = 2 +ij_continuation_indent_size = 4 +ij_prototext_keep_blank_lines_in_code = 2 +ij_prototext_keep_indents_on_empty_lines = false +ij_prototext_keep_line_breaks = true +ij_prototext_space_after_colon = true +ij_prototext_space_after_comma = true +ij_prototext_space_before_colon = false +ij_prototext_space_before_comma = false +ij_prototext_spaces_within_braces = true +ij_prototext_spaces_within_brackets = false + +[{*.properties,spring.handlers,spring.schemas}] +ij_properties_align_group_field_declarations = false +ij_properties_keep_blank_lines = false +ij_properties_key_value_delimiter = equals +ij_properties_spaces_around_key_value_delimiter = false + +[{*.qute.htm,*.qute.html,*.qute.json,*.qute.txt,*.qute.yaml,*.qute.yml}] +ij_qute_keep_indents_on_empty_lines = false + +[{*.toml,Cargo.lock,Cargo.toml.orig,Gopkg.lock,Pipfile,poetry.lock}] +ij_toml_keep_indents_on_empty_lines = false + +[{*.yaml,*.yml}] +indent_size = 2 +ij_yaml_align_values_properties = do_not_align +ij_yaml_autoinsert_sequence_marker = true +ij_yaml_block_mapping_on_new_line = false +ij_yaml_indent_sequence_value = true +ij_yaml_keep_indents_on_empty_lines = false +ij_yaml_keep_line_breaks = true +ij_yaml_sequence_on_new_line = false +ij_yaml_space_before_colon = false +ij_yaml_spaces_within_braces = true +ij_yaml_spaces_within_brackets = true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000000..8ea166ba49 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,81 @@ +# +# The master version catalog for the XDK. +# +# The settings plugin makes sure we only need to declare it in one location (here), +# for all included builds and and the aggregator will make sure all build scripts can use it. +# +# To avoid version skew, we also access this version catalog from settings files and settings +# plugins, using a more explicit hard-coded approach. The code for this lookup is in the +# "org.xtclang.build.gradle" convention plugin, in the function "resolveVersion". +# +# For anything post the settings stage, we can refer to entries in the version catalog +# in a typesafe way, e.g. alias(libs.tasktree) for a dependency or plugin. Or +# libs.versions.xdkVersion version number. +# +[versions] +xdk = "unspecified" +xtc-plugin = "unspecified" +group-xdk = "unspecified" +group-xtc-plugin = "unspecified" + +download = "5.5.0" +versions = "0.50.0" +tasktree = "2.1.1" +gradle-portal-publish = "1.2.1" +gradle-nexus-publish = "1.3.0" +gradle-release = "3.0.2" +kohttp = "0.12.0" +jakarta = "2.3.2" + +# TODO: There is nothing like "group-ref". Might want to just generate this from a template? +[plugins] +xtc = { id = "org.xtclang.xtc-plugin", version.ref = "xtc-plugin" } +xdk-build-aggregator = { id = "org.xtclang.build.aggregator", version.ref = "xdk" } +xdk-build-java = { id = "org.xtclang.build.java", version.ref = "xdk" } +xdk-build-publish = { id = "org.xtclang.build.publish", version.ref = "xdk" } +xdk-build-versioning = { id = "org.xtclang.build.xdk.versioning", version.ref = "xdk" } + +download = { id = "de.undercouch.download", version.ref = "download" } +versions = { id = "com.github.ben-manes.versions", version.ref = "versions" } +tasktree = { id = "com.dorongold.task-tree", version.ref = "tasktree" } +gradle-portal-publish = { id = "com.gradle.plugin-publish", version.ref = "gradle-portal-publish" } +# TODO: Enable mavenCentral publication through this plugin: https://github.com/gradle-nexus/publish-plugin +gradle-nexus-publish = { id = "io.github.gradle-nexus.publish-plugin", version.ref = "gradle-nexus-publish" } +# TODO: Enable GitHub releases, and this seems to be both the most popular plugin and the one closest to the right version / process semantics. +gradle-release = { id = "net.researchgate.gradle-release", version.ref = "gradle-release" } + +[libraries] +xdk = { group = "org.xtclang", name = "xdk", version.ref = "xdk" } +xdk-ecstasy = { group = "org.xtclang", name = "lib-ecstasy", version.ref = "xdk" } +xdk-aggregate = { group = "org.xtclang", name = "lib-aggregate", version.ref = "xdk" } +xdk-collections = { group = "org.xtclang", name = "lib-collections", version.ref = "xdk" } +xdk-crypto = { group = "org.xtclang", name = "lib-crypto", version.ref = "xdk" } +xdk-json = { group = "org.xtclang", name = "lib-json", version.ref = "xdk" } +xdk-jsondb = { group = "org.xtclang", name = "lib-jsondb", version.ref = "xdk" } +xdk-net = { group = "org.xtclang", name = "lib-net", version.ref = "xdk" } +xdk-oodb = { group = "org.xtclang", name = "lib-oodb", version.ref = "xdk" } +xdk-web = { group = "org.xtclang", name = "lib-web", version.ref = "xdk" } +xdk-webauth = { group = "org.xtclang", name = "lib-webauth", version.ref = "xdk" } +xdk-xenia = { group = "org.xtclang", name = "lib-xenia", version.ref = "xdk" } + +javatools = { group = "org.xtclang", name = "javatools", version.ref = "xdk" } +javatools-unicode = { group = "org.xtclang", name = "javatools-unicode", version.ref = "xdk" } +javatools-utils = { group = "org.xtclang", name = "javatools-utils", version.ref = "xdk" } +javatools-turtle = { group = "org.xtclang", name = "javatools-turtle", version.ref = "xdk" } +javatools-bridge = { group = "org.xtclang", name = "javatools-bridge", version.ref = "xdk" } + +kohttp = { module = "io.github.rybalkinsd:kohttp", version.ref = "kohttp" } +kohttp-jackson = { module = "io.github.rybalkinsd:kohttp-jackson", version.ref = "kohttp" } +javax-activation = { module = "com.sun.activation:javax.activation", version = "1.2.0" } +jakarta-xml-bind-api = { module = "jakarta.xml.bind:jakarta.xml.bind-api", version.ref = "jakarta" } +jaxb-runtime = { module = "org.glassfish.jaxb:jaxb-runtime", version.ref = "jakarta" } + +[bundles] +unicode = ["javax-activation", "jakarta-xml-bind-api", "jaxb-runtime"] + +# TODO: Future libraries that may make it into the build pipeline, one way or another. +#spotless = { id = "com.diffplug.spotless", version = "6.22.0" } +#shadow = { id = "com.github.johnrengelman.shadow", version = "8.1.1" } +#nsis = { id = "io.github.androxyde.gradlensis", version = "0.2.0" } +#maven-plugin-development = { id = "de.benediktritter.maven-plugin-development", version = "0.4.2" } +#version-catalog-update = { id = "nl.littlerobots.version-catalog-update", version = "0.8.1" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 7f93135c49b765f8051ef9d0a6055ff8e46073d8..d64cd4917707c1f8861d8cb53dd15194d4248596 100644 GIT binary patch literal 43462 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 literal 63721 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鯊%1Et zwk~hMkhq;MOw^Q5`7oC{CUUyTw9x>^%*FHx^qJw(LB+E0WBX@{Ghw;)6aA-KyYg8p z7XDveQOpEr;B4je@2~usI5BlFadedX^ma{b{ypd|RNYqo#~d*mj&y`^iojR}s%~vF z(H!u`yx68D1Tj(3(m;Q+Ma}s2n#;O~bcB1`lYk%Irx60&-nWIUBr2x&@}@76+*zJ5 ze&4?q8?m%L9c6h=J$WBzbiTf1Z-0Eb5$IZs>lvm$>1n_Mezp*qw_pr8<8$6f)5f<@ zyV#tzMCs51nTv_5ca`x`yfE5YA^*%O_H?;tWYdM_kHPubA%vy47i=9>Bq) zRQ&0UwLQHeswmB1yP)+BiR;S+Vc-5TX84KUA;8VY9}yEj0eESSO`7HQ4lO z4(CyA8y1G7_C;6kd4U3K-aNOK!sHE}KL_-^EDl(vB42P$2Km7$WGqNy=%fqB+ zSLdrlcbEH=T@W8V4(TgoXZ*G1_aq$K^@ek=TVhoKRjw;HyI&coln|uRr5mMOy2GXP zwr*F^Y|!Sjr2YQXX(Fp^*`Wk905K%$bd03R4(igl0&7IIm*#f`A!DCarW9$h$z`kYk9MjjqN&5-DsH@8xh63!fTNPxWsFQhNv z#|3RjnP$Thdb#Ys7M+v|>AHm0BVTw)EH}>x@_f4zca&3tXJhTZ8pO}aN?(dHo)44Z z_5j+YP=jMlFqwvf3lq!57-SAuRV2_gJ*wsR_!Y4Z(trO}0wmB9%f#jNDHPdQGHFR; zZXzS-$`;7DQ5vF~oSgP3bNV$6Z(rwo6W(U07b1n3UHqml>{=6&-4PALATsH@Bh^W? z)ob%oAPaiw{?9HfMzpGb)@Kys^J$CN{uf*HX?)z=g`J(uK1YO^8~s1(ZIbG%Et(|q z$D@_QqltVZu9Py4R0Ld8!U|#`5~^M=b>fnHthzKBRr=i+w@0Vr^l|W;=zFT#PJ?*a zbC}G#It}rQP^Ait^W&aa6B;+0gNvz4cWUMzpv(1gvfw-X4xJ2Sv;mt;zb2Tsn|kSS zo*U9N?I{=-;a-OybL4r;PolCfiaL=y@o9{%`>+&FI#D^uy#>)R@b^1ue&AKKwuI*` zx%+6r48EIX6nF4o;>)zhV_8(IEX})NGU6Vs(yslrx{5fII}o3SMHW7wGtK9oIO4OM&@@ECtXSICLcPXoS|{;=_yj>hh*%hP27yZwOmj4&Lh z*Nd@OMkd!aKReoqNOkp5cW*lC)&C$P?+H3*%8)6HcpBg&IhGP^77XPZpc%WKYLX$T zsSQ$|ntaVVOoRat$6lvZO(G-QM5s#N4j*|N_;8cc2v_k4n6zx9c1L4JL*83F-C1Cn zaJhd;>rHXB%%ZN=3_o3&Qd2YOxrK~&?1=UuN9QhL$~OY-Qyg&})#ez*8NpQW_*a&kD&ANjedxT0Ar z<6r{eaVz3`d~+N~vkMaV8{F?RBVemN(jD@S8qO~L{rUw#=2a$V(7rLE+kGUZ<%pdr z?$DP|Vg#gZ9S}w((O2NbxzQ^zTot=89!0^~hE{|c9q1hVzv0?YC5s42Yx($;hAp*E zyoGuRyphQY{Q2ee0Xx`1&lv(l-SeC$NEyS~8iil3_aNlnqF_G|;zt#F%1;J)jnPT& z@iU0S;wHJ2$f!juqEzPZeZkjcQ+Pa@eERSLKsWf=`{R@yv7AuRh&ALRTAy z8=g&nxsSJCe!QLchJ=}6|LshnXIK)SNd zRkJNiqHwKK{SO;N5m5wdL&qK`v|d?5<4!(FAsDxR>Ky#0#t$8XCMptvNo?|SY?d8b z`*8dVBlXTUanlh6n)!EHf2&PDG8sXNAt6~u-_1EjPI1|<=33T8 zEnA00E!`4Ave0d&VVh0e>)Dc}=FfAFxpsC1u9ATfQ`-Cu;mhc8Z>2;uyXtqpLb7(P zd2F9<3cXS} znMg?{&8_YFTGRQZEPU-XPq55%51}RJpw@LO_|)CFAt62-_!u_Uq$csc+7|3+TV_!h z+2a7Yh^5AA{q^m|=KSJL+w-EWDBc&I_I1vOr^}P8i?cKMhGy$CP0XKrQzCheG$}G# zuglf8*PAFO8%xop7KSwI8||liTaQ9NCAFarr~psQt)g*pC@9bORZ>m`_GA`_K@~&% zijH0z;T$fd;-Liw8%EKZas>BH8nYTqsK7F;>>@YsE=Rqo?_8}UO-S#|6~CAW0Oz1} z3F(1=+#wrBJh4H)9jTQ_$~@#9|Bc1Pd3rAIA_&vOpvvbgDJOM(yNPhJJq2%PCcMaI zrbe~toYzvkZYQ{ea(Wiyu#4WB#RRN%bMe=SOk!CbJZv^m?Flo5p{W8|0i3`hI3Np# zvCZqY%o258CI=SGb+A3yJe~JH^i{uU`#U#fvSC~rWTq+K`E%J@ zasU07&pB6A4w3b?d?q}2=0rA#SA7D`X+zg@&zm^iA*HVi z009#PUH<%lk4z~p^l0S{lCJk1Uxi=F4e_DwlfHA`X`rv(|JqWKAA5nH+u4Da+E_p+ zVmH@lg^n4ixs~*@gm_dgQ&eDmE1mnw5wBz9Yg?QdZwF|an67Xd*x!He)Gc8&2!urh z4_uXzbYz-aX)X1>&iUjGp;P1u8&7TID0bTH-jCL&Xk8b&;;6p2op_=y^m@Nq*0{#o!!A;wNAFG@0%Z9rHo zcJs?Th>Ny6+hI`+1XoU*ED$Yf@9f91m9Y=#N(HJP^Y@ZEYR6I?oM{>&Wq4|v0IB(p zqX#Z<_3X(&{H+{3Tr|sFy}~=bv+l=P;|sBz$wk-n^R`G3p0(p>p=5ahpaD7>r|>pm zv;V`_IR@tvZreIuv2EM7ZQHhO+qUgw#kOs%*ekY^n|=1#x9&c;Ro&I~{rG-#_3ZB1 z?|9}IFdbP}^DneP*T-JaoYHt~r@EfvnPE5EKUwIxjPbsr$% zfWW83pgWST7*B(o=kmo)74$8UU)v0{@4DI+ci&%=#90}!CZz|rnH+Mz=HN~97G3~@ z;v5(9_2%eca(9iu@J@aqaMS6*$TMw!S>H(b z4(*B!|H|8&EuB%mITr~O?vVEf%(Gr)6E=>H~1VR z&1YOXluJSG1!?TnT)_*YmJ*o_Q@om~(GdrhI{$Fsx_zrkupc#y{DK1WOUR>tk>ZE) ziOLoBkhZZ?0Uf}cm>GsA>Rd6V8@JF)J*EQlQ<=JD@m<)hyElXR0`pTku*3MU`HJn| zIf7$)RlK^pW-$87U;431;Ye4Ie+l~_B3*bH1>*yKzn23cH0u(i5pXV! z4K?{3oF7ZavmmtTq((wtml)m6i)8X6ot_mrE-QJCW}Yn!(3~aUHYG=^fA<^~`e3yc z-NWTb{gR;DOUcK#zPbN^D*e=2eR^_!(!RKkiwMW@@yYtEoOp4XjOGgzi`;=8 zi3`Ccw1%L*y(FDj=C7Ro-V?q)-%p?Ob2ZElu`eZ99n14-ZkEV#y5C+{Pq87Gu3&>g zFy~Wk7^6v*)4pF3@F@rE__k3ikx(hzN3@e*^0=KNA6|jC^B5nf(XaoQaZN?Xi}Rn3 z$8&m*KmWvPaUQ(V<#J+S&zO|8P-#!f%7G+n_%sXp9=J%Z4&9OkWXeuZN}ssgQ#Tcj z8p6ErJQJWZ+fXLCco=RN8D{W%+*kko*2-LEb))xcHwNl~Xmir>kmAxW?eW50Osw3# zki8Fl$#fvw*7rqd?%E?}ZX4`c5-R&w!Y0#EBbelVXSng+kUfeUiqofPehl}$ormli zg%r)}?%=?_pHb9`Cq9Z|B`L8b>(!+8HSX?`5+5mm81AFXfnAt1*R3F z%b2RPIacKAddx%JfQ8l{3U|vK@W7KB$CdLqn@wP^?azRks@x8z59#$Q*7q!KilY-P zHUbs(IFYRGG1{~@RF;Lqyho$~7^hNC`NL3kn^Td%A7dRgr_&`2k=t+}D-o9&C!y^? z6MsQ=tc3g0xkK(O%DzR9nbNB(r@L;1zQrs8mzx&4dz}?3KNYozOW5;=w18U6$G4U2 z#2^qRLT*Mo4bV1Oeo1PKQ2WQS2Y-hv&S|C7`xh6=Pj7MNLC5K-zokZ67S)C;(F0Dd zloDK2_o1$Fmza>EMj3X9je7e%Q`$39Dk~GoOj89-6q9|_WJlSl!!+*{R=tGp z8u|MuSwm^t7K^nUe+^0G3dkGZr3@(X+TL5eah)K^Tn zXEtHmR9UIaEYgD5Nhh(s*fcG_lh-mfy5iUF3xxpRZ0q3nZ=1qAtUa?(LnT9I&~uxX z`pV?+=|-Gl(kz?w!zIieXT}o}7@`QO>;u$Z!QB${a08_bW0_o@&9cjJUXzVyNGCm8 zm=W+$H!;_Kzp6WQqxUI;JlPY&`V}9C$8HZ^m?NvI*JT@~BM=()T()Ii#+*$y@lTZBkmMMda>7s#O(1YZR+zTG@&}!EXFG{ zEWPSDI5bFi;NT>Yj*FjH((=oe%t%xYmE~AGaOc4#9K_XsVpl<4SP@E!TgC0qpe1oi zNpxU2b0(lEMcoibQ-G^cxO?ySVW26HoBNa;n0}CWL*{k)oBu1>F18X061$SP{Gu67 z-v-Fa=Fl^u3lnGY^o5v)Bux}bNZ~ z5pL+7F_Esoun8^5>z8NFoIdb$sNS&xT8_|`GTe8zSXQzs4r^g0kZjg(b0bJvz`g<70u9Z3fQILX1Lj@;@+##bP|FAOl)U^9U>0rx zGi)M1(Hce)LAvQO-pW!MN$;#ZMX?VE(22lTlJrk#pB0FJNqVwC+*%${Gt#r_tH9I_ z;+#)#8cWAl?d@R+O+}@1A^hAR1s3UcW{G+>;X4utD2d9X(jF555}!TVN-hByV6t+A zdFR^aE@GNNgSxxixS2p=on4(+*+f<8xrwAObC)D5)4!z7)}mTpb7&ofF3u&9&wPS< zB62WHLGMhmrmOAgmJ+|c>qEWTD#jd~lHNgT0?t-p{T=~#EMcB| z=AoDKOL+qXCfk~F)-Rv**V}}gWFl>liXOl7Uec_8v)(S#av99PX1sQIVZ9eNLkhq$ zt|qu0b?GW_uo}TbU8!jYn8iJeIP)r@;!Ze_7mj{AUV$GEz6bDSDO=D!&C9!M@*S2! zfGyA|EPlXGMjkH6x7OMF?gKL7{GvGfED=Jte^p=91FpCu)#{whAMw`vSLa`K#atdN zThnL+7!ZNmP{rc=Z>%$meH;Qi1=m1E3Lq2D_O1-X5C;!I0L>zur@tPAC9*7Jeh)`;eec}1`nkRP(%iv-`N zZ@ip-g|7l6Hz%j%gcAM}6-nrC8oA$BkOTz^?dakvX?`^=ZkYh%vUE z9+&)K1UTK=ahYiaNn&G5nHUY5niLGus@p5E2@RwZufRvF{@$hW{;{3QhjvEHMvduO z#Wf-@oYU4ht?#uP{N3utVzV49mEc9>*TV_W2TVC`6+oI)zAjy$KJrr=*q##&kobiQ z1vNbya&OVjK`2pdRrM?LuK6BgrLN7H_3m z!qpNKg~87XgCwb#I=Q&0rI*l$wM!qTkXrx1ko5q-f;=R2fImRMwt5Qs{P*p^z@9ex z`2#v(qE&F%MXlHpdO#QEZyZftn4f05ab^f2vjxuFaat2}jke{j?5GrF=WYBR?gS(^ z9SBiNi}anzBDBRc+QqizTTQuJrzm^bNA~A{j%ugXP7McZqJ}65l10({wk++$=e8O{ zxWjG!Qp#5OmI#XRQQM?n6?1ztl6^D40hDJr?4$Wc&O_{*OfMfxe)V0=e{|N?J#fgE>j9jAajze$iN!*yeF%jJU#G1c@@rm zolGW!j?W6Q8pP=lkctNFdfgUMg92wlM4E$aks1??M$~WQfzzzXtS)wKrr2sJeCN4X zY(X^H_c^PzfcO8Bq(Q*p4c_v@F$Y8cHLrH$`pJ2}=#*8%JYdqsqnGqEdBQMpl!Ot04tUGSXTQdsX&GDtjbWD=prcCT9(+ z&UM%lW%Q3yrl1yiYs;LxzIy>2G}EPY6|sBhL&X&RAQrSAV4Tlh2nITR?{6xO9ujGu zr*)^E`>o!c=gT*_@6S&>0POxcXYNQd&HMw6<|#{eSute2C3{&h?Ah|cw56-AP^f8l zT^kvZY$YiH8j)sk7_=;gx)vx-PW`hbSBXJGCTkpt;ap(}G2GY=2bbjABU5)ty%G#x zAi07{Bjhv}>OD#5zh#$0w;-vvC@^}F! z#X$@)zIs1L^E;2xDAwEjaXhTBw2<{&JkF*`;c3<1U@A4MaLPe{M5DGGkL}#{cHL%* zYMG+-Fm0#qzPL#V)TvQVI|?_M>=zVJr9>(6ib*#z8q@mYKXDP`k&A4A};xMK0h=yrMp~JW{L?mE~ph&1Y1a#4%SO)@{ zK2juwynUOC)U*hVlJU17%llUxAJFuKZh3K0gU`aP)pc~bE~mM!i1mi!~LTf>1Wp< zuG+ahp^gH8g8-M$u{HUWh0m^9Rg@cQ{&DAO{PTMudV6c?ka7+AO& z746QylZ&Oj`1aqfu?l&zGtJnpEQOt;OAFq19MXTcI~`ZcoZmyMrIKDFRIDi`FH)w; z8+*8tdevMDv*VtQi|e}CnB_JWs>fhLOH-+Os2Lh!&)Oh2utl{*AwR)QVLS49iTp{6 z;|172Jl!Ml17unF+pd+Ff@jIE-{Oxv)5|pOm@CkHW?{l}b@1>Pe!l}VccX#xp@xgJ zyE<&ep$=*vT=}7vtvif0B?9xw_3Gej7mN*dOHdQPtW5kA5_zGD zpA4tV2*0E^OUimSsV#?Tg#oiQ>%4D@1F5@AHwT8Kgen$bSMHD3sXCkq8^(uo7CWk`mT zuslYq`6Yz;L%wJh$3l1%SZv#QnG3=NZ=BK4yzk#HAPbqXa92;3K5?0kn4TQ`%E%X} z&>Lbt!!QclYKd6+J7Nl@xv!uD%)*bY-;p`y^ZCC<%LEHUi$l5biu!sT3TGGSTPA21 zT8@B&a0lJHVn1I$I3I1I{W9fJAYc+8 zVj8>HvD}&O`TqU2AAb={?eT;0hyL(R{|h23=4fDSZKC32;wWxsVj`P z3J3{M$PwdH!ro*Cn!D&=jnFR>BNGR<<|I8CI@+@658Dy(lhqbhXfPTVecY@L8%`3Q z1Fux2w?2C3th60jI~%OC9BtpNF$QPqcG+Pz96qZJ71_`0o0w_q7|h&O>`6U+^BA&5 zXd5Zp1Xkw~>M%RixTm&OqpNl8Q+ue=92Op_>T~_9UON?ZM2c0aGm=^A4ejrXj3dV9 zhh_bCt-b9`uOX#cFLj!vhZ#lS8Tc47OH>*)y#{O9?AT~KR9LntM|#l#Dlm^8{nZdk zjMl#>ZM%#^nK2TPzLcKxqx24P7R1FPlBy7LSBrRvx>fE$9AJ;7{PQm~^LBX^k#6Zq zw*Z(zJC|`!6_)EFR}8|n8&&Rbj8y028~P~sFXBFRt+tmqH-S3<%N;C&WGH!f3{7cm zy_fCAb9@HqaXa1Y5vFbxWf%#zg6SI$C+Uz5=CTO}e|2fjWkZ;Dx|84Ow~bkI=LW+U zuq;KSv9VMboRvs9)}2PAO|b(JCEC_A0wq{uEj|3x@}*=bOd zwr{TgeCGG>HT<@Zeq8y}vTpwDg#UBvD)BEs@1KP$^3$sh&_joQPn{hjBXmLPJ{tC) z*HS`*2+VtJO{|e$mM^|qv1R*8i(m1`%)}g=SU#T#0KlTM2RSvYUc1fP+va|4;5}Bfz98UvDCpq7}+SMV&;nX zQw~N6qOX{P55{#LQkrZk(e5YGzr|(B;Q;ju;2a`q+S9bsEH@i1{_Y0;hWYn1-79jl z5c&bytD*k)GqrVcHn6t-7kinadiD>B{Tl`ZY@`g|b~pvHh5!gKP4({rp?D0aFd_cN zhHRo4dd5^S6ViN(>(28qZT6E>??aRhc($kP`>@<+lIKS5HdhjVU;>f7<4))E*5|g{ z&d1}D|vpuV^eRj5j|xx9nwaCxXFG?Qbjn~_WSy=N}P0W>MP zG-F%70lX5Xr$a)2i6?i|iMyM|;Jtf*hO?=Jxj12oz&>P=1#h~lf%#fc73M2_(SUM- zf&qnjS80|_Y0lDgl&I?*eMumUklLe_=Td!9G@eR*tcPOgIShJipp3{A10u(4eT~DY zHezEj8V+7m!knn7)W!-5QI3=IvC^as5+TW1@Ern@yX| z7Nn~xVx&fGSr+L%4iohtS3w^{-H1A_5=r&x8}R!YZvp<2T^YFvj8G_vm}5q;^UOJf ztl=X3iL;;^^a#`t{Ae-%5Oq{?M#s6Npj+L(n-*LMI-yMR{)qki!~{5z{&`-iL}lgW zxo+tnvICK=lImjV$Z|O_cYj_PlEYCzu-XBz&XC-JVxUh9;6*z4fuBG+H{voCC;`~GYV|hj%j_&I zDZCj>Q_0RCwFauYoVMiUSB+*Mx`tg)bWmM^SwMA+?lBg12QUF_x2b)b?qb88K-YUd z0dO}3k#QirBV<5%jL$#wlf!60dizu;tsp(7XLdI=eQs?P`tOZYMjVq&jE)qK*6B^$ zBe>VvH5TO>s>izhwJJ$<`a8fakTL!yM^Zfr2hV9`f}}VVUXK39p@G|xYRz{fTI+Yq z20d=)iwjuG9RB$%$^&8#(c0_j0t_C~^|n+c`Apu|x7~;#cS-s=X1|C*YxX3ailhg_|0`g!E&GZJEr?bh#Tpb8siR=JxWKc{#w7g zWznLwi;zLFmM1g8V5-P#RsM@iX>TK$xsWuujcsVR^7TQ@!+vCD<>Bk9tdCo7Mzgq5 zv8d>dK9x8C@Qoh01u@3h0X_`SZluTb@5o;{4{{eF!-4405x8X7hewZWpz z2qEi4UTiXTvsa(0X7kQH{3VMF>W|6;6iTrrYD2fMggFA&-CBEfSqPlQDxqsa>{e2M z(R5PJ7uOooFc|9GU0ELA%m4&4Ja#cQpNw8i8ACAoK6?-px+oBl_yKmenZut#Xumjz zk8p^OV2KY&?5MUwGrBOo?ki`Sxo#?-Q4gw*Sh0k`@ zFTaYK2;}%Zk-68`#5DXU$2#=%YL#S&MTN8bF+!J2VT6x^XBci6O)Q#JfW{YMz) zOBM>t2rSj)n#0a3cjvu}r|k3od6W(SN}V-cL?bi*Iz-8uOcCcsX0L>ZXjLqk zZu2uHq5B|Kt>e+=pPKu=1P@1r9WLgYFq_TNV1p9pu0erHGd!+bBp!qGi+~4A(RsYN@CyXNrC&hxGmW)u5m35OmWwX`I+0yByglO`}HC4nGE^_HUs^&A(uaM zKPj^=qI{&ayOq#z=p&pnx@@k&I1JI>cttJcu@Ihljt?6p^6{|ds`0MoQwp+I{3l6` zB<9S((RpLG^>=Kic`1LnhpW2=Gu!x`m~=y;A`Qk!-w`IN;S8S930#vBVMv2vCKi}u z6<-VPrU0AnE&vzwV(CFC0gnZYcpa-l5T0ZS$P6(?9AM;`Aj~XDvt;Jua=jIgF=Fm? zdp=M$>`phx%+Gu};;-&7T|B1AcC#L4@mW5SV_^1BRbo6;2PWe$r+npRV`yc;T1mo& z+~_?7rA+(Um&o@Tddl zL_hxvWk~a)yY}%j`Y+200D%9$bWHy&;(yj{jpi?Rtz{J66ANw)UyPOm;t6FzY3$hx zcn)Ir79nhFvNa7^a{SHN7XH*|Vlsx`CddPnA&Qvh8aNhEA;mPVv;Ah=k<*u!Zq^7 z<=xs*iQTQOMMcg|(NA_auh@x`3#_LFt=)}%SQppP{E>mu_LgquAWvh<>L7tf9+~rO znwUDS52u)OtY<~!d$;m9+87aO+&`#2ICl@Y>&F{jI=H(K+@3M1$rr=*H^dye#~TyD z!){#Pyfn+|ugUu}G;a~!&&0aqQ59U@UT3|_JuBlYUpT$2+11;}JBJ`{+lQN9T@QFY z5+`t;6(TS0F?OlBTE!@7D`8#URDNqx2t6`GZ{ZgXeS@v%-eJzZOHz18aS|svxII$a zZeFjrJ*$IwX$f-Rzr_G>xbu@euGl)B7pC&S+CmDJBg$BoV~jxSO#>y z33`bupN#LDoW0feZe0%q8un0rYN|eRAnwDHQ6e_)xBTbtoZtTA=Fvk){q}9Os~6mQ zKB80VI_&6iSq`LnK7*kfHZoeX6?WE}8yjuDn=2#JG$+;-TOA1%^=DnXx%w{b=w}tS zQbU3XxtOI8E(!%`64r2`zog;5<0b4i)xBmGP^jiDZ2%HNSxIf3@wKs~uk4%3Mxz;~ zts_S~E4>W+YwI<-*-$U8*^HKDEa8oLbmqGg?3vewnaNg%Mm)W=)lcC_J+1ov^u*N3 zXJ?!BrH-+wGYziJq2Y#vyry6Z>NPgkEk+Ke`^DvNRdb>Q2Nlr#v%O@<5hbflI6EKE z9dWc0-ORk^T}jP!nkJ1imyjdVX@GrjOs%cpgA8-c&FH&$(4od#x6Y&=LiJZPINVyW z0snY$8JW@>tc2}DlrD3StQmA0Twck~@>8dSix9CyQOALcREdxoM$Sw*l!}bXKq9&r zysMWR@%OY24@e`?+#xV2bk{T^C_xSo8v2ZI=lBI*l{RciPwuE>L5@uhz@{!l)rtVlWC>)6(G)1~n=Q|S!{E9~6*fdpa*n z!()-8EpTdj=zr_Lswi;#{TxbtH$8*G=UM`I+icz7sr_SdnHXrv=?iEOF1UL+*6O;% zPw>t^kbW9X@oEXx<97%lBm-9?O_7L!DeD)Me#rwE54t~UBu9VZ zl_I1tBB~>jm@bw0Aljz8! zXBB6ATG6iByKIxs!qr%pz%wgqbg(l{65DP4#v(vqhhL{0b#0C8mq`bnqZ1OwFV z7mlZZJFMACm>h9v^2J9+^_zc1=JjL#qM5ZHaThH&n zXPTsR8(+)cj&>Un{6v*z?@VTLr{TmZ@-fY%*o2G}*G}#!bmqpoo*Ay@U!JI^Q@7gj;Kg-HIrLj4}#ec4~D2~X6vo;ghep-@&yOivYP zC19L0D`jjKy1Yi-SGPAn94(768Tcf$urAf{)1)9W58P`6MA{YG%O?|07!g9(b`8PXG1B1Sh0?HQmeJtP0M$O$hI z{5G`&9XzYhh|y@qsF1GnHN|~^ru~HVf#)lOTSrv=S@DyR$UKQk zjdEPFDz{uHM&UM;=mG!xKvp;xAGHOBo~>_=WFTmh$chpC7c`~7?36h)7$fF~Ii}8q zF|YXxH-Z?d+Q+27Rs3X9S&K3N+)OBxMHn1u(vlrUC6ckBY@@jl+mgr#KQUKo#VeFm zFwNYgv0<%~Wn}KeLeD9e1$S>jhOq&(e*I@L<=I5b(?G(zpqI*WBqf|Zge0&aoDUsC zngMRA_Kt0>La+Erl=Uv_J^p(z=!?XHpenzn$%EA`JIq#yYF?JLDMYiPfM(&Csr#f{ zdd+LJL1by?xz|D8+(fgzRs~(N1k9DSyK@LJygwaYX8dZl0W!I&c^K?7)z{2is;OkE zd$VK-(uH#AUaZrp=1z;O*n=b?QJkxu`Xsw&7yrX0?(CX=I-C#T;yi8a<{E~?vr3W> zQrpPqOW2M+AnZ&p{hqmHZU-;Q(7?- zP8L|Q0RM~sB0w1w53f&Kd*y}ofx@c z5Y6B8qGel+uT1JMot$nT1!Tim6{>oZzJXdyA+4euOLME?5Fd_85Uk%#E*ln%y{u8Q z$|?|R@Hpb~yTVK-Yr_S#%NUy7EBfYGAg>b({J|5b+j-PBpPy$Ns`PaJin4JdRfOaS zE|<HjH%NuJgsd2wOlv>~y=np%=2)$M9LS|>P)zJ+Fei5vYo_N~B0XCn+GM76 z)Xz3tg*FRVFgIl9zpESgdpWAavvVViGlU8|UFY{{gVJskg*I!ZjWyk~OW-Td4(mZ6 zB&SQreAAMqwp}rjy`HsG({l2&q5Y52<@AULVAu~rWI$UbFuZs>Sc*x+XI<+ez%$U)|a^unjpiW0l0 zj1!K0(b6$8LOjzRqQ~K&dfbMIE=TF}XFAi)$+h}5SD3lo z%%Qd>p9se=VtQG{kQ;N`sI)G^u|DN#7{aoEd zkksYP%_X$Rq08);-s6o>CGJ<}v`qs%eYf+J%DQ^2k68C%nvikRsN?$ap--f+vCS`K z#&~)f7!N^;sdUXu54gl3L=LN>FB^tuK=y2e#|hWiWUls__n@L|>xH{%8lIJTd5`w? zSwZbnS;W~DawT4OwSJVdAylbY+u5S+ZH{4hAi2&}Iv~W(UvHg(1GTZRPz`@{SOqzy z(8g&Dz=$PfRV=6FgxN~zo+G8OoPI&d-thcGVR*_^(R8COTM@bq?fDwY{}WhsQS1AK zF6R1t8!RdFmfocpJ6?9Yv~;WYi~XPgs(|>{5})j!AR!voO7y9&cMPo#80A(`za@t>cx<0;qxM@S*m(jYP)dMXr*?q0E`oL;12}VAep179uEr8c<=D zr5?A*C{eJ`z9Ee;E$8)MECqatHkbHH z&Y+ho0B$31MIB-xm&;xyaFCtg<{m~M-QDbY)fQ>Q*Xibb~8ytxZQ?QMf9!%cV zU0_X1@b4d+Pg#R!`OJ~DOrQz3@cpiGy~XSKjZQQ|^4J1puvwKeScrH8o{bscBsowomu z^f12kTvje`yEI3eEXDHJ6L+O{Jv$HVj%IKb|J{IvD*l6IG8WUgDJ*UGz z3!C%>?=dlfSJ>4U88)V+`U-!9r^@AxJBx8R;)J4Fn@`~k>8>v0M9xp90OJElWP&R5 zM#v*vtT}*Gm1^)Bv!s72T3PB0yVIjJW)H7a)ilkAvoaH?)jjb`MP>2z{%Y?}83 zUIwBKn`-MSg)=?R)1Q0z3b>dHE^)D8LFs}6ASG1|daDly_^lOSy&zIIhm*HXm1?VS=_iacG);_I9c zUQH1>i#*?oPIwBMJkzi_*>HoUe}_4o>2(SHWzqQ=;TyhAHS;Enr7!#8;sdlty&(>d zl%5cjri8`2X^Ds`jnw7>A`X|bl=U8n+3LKLy(1dAu8`g@9=5iw$R0qk)w8Vh_Dt^U zIglK}sn^)W7aB(Q>HvrX=rxB z+*L)3DiqpQ_%~|m=44LcD4-bxO3OO*LPjsh%p(k?&jvLp0py57oMH|*IMa(<|{m1(0S|x)?R-mqJ=I;_YUZA>J z62v*eSK;5w!h8J+6Z2~oyGdZ68waWfy09?4fU&m7%u~zi?YPHPgK6LDwphgaYu%0j zurtw)AYOpYKgHBrkX189mlJ`q)w-f|6>IER{5Lk97%P~a-JyCRFjejW@L>n4vt6#hq;!|m;hNE||LK3nw1{bJOy+eBJjK=QqNjI;Q6;Rp5 z&035pZDUZ#%Oa;&_7x0T<7!RW`#YBOj}F380Bq?MjjEhrvlCATPdkCTTl+2efTX$k zH&0zR1n^`C3ef~^sXzJK-)52(T}uTG%OF8yDhT76L~|^+hZ2hiSM*QA9*D5odI1>& z9kV9jC~twA5MwyOx(lsGD_ggYmztXPD`2=_V|ks_FOx!_J8!zM zTzh^cc+=VNZ&(OdN=y4Juw)@8-85lwf_#VMN!Ed(eQiRiLB2^2e`4dp286h@v@`O%_b)Y~A; zv}r6U?zs&@uD_+(_4bwoy7*uozNvp?bXFoB8?l8yG0qsm1JYzIvB_OH4_2G*IIOwT zVl%HX1562vLVcxM_RG*~w_`FbIc!(T=3>r528#%mwwMK}uEhJ()3MEby zQQjzqjWkwfI~;Fuj(Lj=Ug0y`>~C7`w&wzjK(rPw+Hpd~EvQ-ufQOiB4OMpyUKJhw zqEt~jle9d7S~LI~$6Z->J~QJ{Vdn3!c}g9}*KG^Kzr^(7VI5Gk(mHLL{itj_hG?&K4Ws0+T4gLfi3eu$N=`s36geNC?c zm!~}vG6lx9Uf^5M;bWntF<-{p^bruy~f?sk9 zcETAPQZLoJ8JzMMg<-=ju4keY@SY%Wo?u9Gx=j&dfa6LIAB|IrbORLV1-H==Z1zCM zeZcOYpm5>U2fU7V*h;%n`8 zN95QhfD994={1*<2vKLCNF)feKOGk`R#K~G=;rfq}|)s20&MCa65 zUM?xF5!&e0lF%|U!#rD@I{~OsS_?=;s_MQ_b_s=PuWdC)q|UQ&ea)DMRh5>fpQjXe z%9#*x=7{iRCtBKT#H>#v%>77|{4_slZ)XCY{s3j_r{tdpvb#|r|sbS^dU1x70$eJMU!h{Y7Kd{dl}9&vxQl6Jt1a` zHQZrWyY0?!vqf@u-fxU_@+}u(%Wm>0I#KP48tiAPYY!TdW(o|KtVI|EUB9V`CBBNaBLVih7+yMVF|GSoIQD0Jfb{ z!OXq;(>Z?O`1gap(L~bUcp>Lc@Jl-})^=6P%<~~9ywY=$iu8pJ0m*hOPzr~q`23eX zgbs;VOxxENe0UMVeN*>uCn9Gk!4siN-e>x)pIKAbQz!G)TcqIJ0`JBBaX>1-4_XO_-HCS^vr2vjv#7KltDZdyQ{tlWh4$Gm zB>|O1cBDC)yG(sbnc*@w6e%e}r*|IhpXckx&;sQCwGdKH+3oSG-2)Bf#x`@<4ETAr z0My%7RFh6ZLiZ_;X6Mu1YmXx7C$lSZ^}1h;j`EZd6@%JNUe=btBE z%s=Xmo1Ps?8G`}9+6>iaB8bgjUdXT?=trMu|4yLX^m0Dg{m7rpKNJey|EwHI+nN1e zL^>qN%5Fg)dGs4DO~uwIdXImN)QJ*Jhpj7$fq_^`{3fwpztL@WBB}OwQ#Epo-mqMO zsM$UgpFiG&d#)lzEQ{3Q;)&zTw;SzGOah-Dpm{!q7<8*)Ti_;xvV2TYXa}=faXZy? z3y?~GY@kl)>G&EvEijk9y1S`*=zBJSB1iet>0;x1Ai)*`^{pj0JMs)KAM=@UyOGtO z3y0BouW$N&TnwU6!%zS%nIrnANvZF&vB1~P5_d`x-giHuG zPJ;>XkVoghm#kZXRf>qxxEix;2;D1CC~NrbO6NBX!`&_$iXwP~P*c($EVV|669kDO zKoTLZNF4Cskh!Jz5ga9uZ`3o%7Pv`d^;a=cXI|>y;zC3rYPFLQkF*nv(r>SQvD*## z(Vo%^9g`%XwS0t#94zPq;mYGLKu4LU3;txF26?V~A0xZbU4Lmy`)>SoQX^m7fd^*E z+%{R4eN!rIk~K)M&UEzxp9dbY;_I^c} zOc{wlIrN_P(PPqi51k_$>Lt|X6A^|CGYgKAmoI#Li?;Wq%q~q*L7ehZkUrMxW67Jl zhsb~+U?33QS>eqyN{(odAkbopo=Q$Az?L+NZW>j;#~@wCDX?=L5SI|OxI~7!Pli;e zELMFcZtJY3!|=Gr2L4>z8yQ-{To>(f80*#;6`4IAiqUw`=Pg$%C?#1 z_g@hIGerILSU>=P>z{gM|DS91A4cT@PEIB^hSop!uhMo#2G;+tQSpDO_6nOnPWSLU zS;a9m^DFMXR4?*X=}d7l;nXuHk&0|m`NQn%d?8|Ab3A9l9Jh5s120ibWBdB z$5YwsK3;wvp!Kn@)Qae{ef`0#NwlRpQ}k^r>yos_Ne1;xyKLO?4)t_G4eK~wkUS2A&@_;)K0-03XGBzU+5f+uMDxC z(s8!8!RvdC#@`~fx$r)TKdLD6fWEVdEYtV#{ncT-ZMX~eI#UeQ-+H(Z43vVn%Yj9X zLdu9>o%wnWdvzA-#d6Z~vzj-}V3FQ5;axDIZ;i(95IIU=GQ4WuU{tl-{gk!5{l4_d zvvb&uE{%!iFwpymz{wh?bKr1*qzeZb5f6e6m_ozRF&zux2mlK=v_(_s^R6b5lu?_W4W3#<$zeG~Pd)^!4tzhs}-Sx$FJP>)ZGF(hVTH|C3(U zs0PO&*h_ zNA-&qZpTP$$LtIgfiCn07}XDbK#HIXdmv8zdz4TY;ifNIH-0jy(gMSByG2EF~Th#eb_TueZC` zE?3I>UTMpKQ})=C;6p!?G)M6w^u*A57bD?2X`m3X^6;&4%i_m(uGJ3Z5h`nwxM<)H z$I5m?wN>O~8`BGnZ=y^p6;0+%_0K}Dcg|K;+fEi|qoBqvHj(M&aHGqNF48~XqhtU? z^ogwBzRlOfpAJ+Rw7IED8lRbTdBdyEK$gPUpUG}j-M42xDj_&qEAQEtbs>D#dRd7Y z<&TpSZ(quQDHiCFn&0xsrz~4`4tz!CdL8m~HxZM_agu@IrBpyeL1Ft}V$HX_ZqDPm z-f89)pjuEzGdq-PRu`b1m+qBGY{zr_>{6Ss>F|xHZlJj9dt5HD$u`1*WZe)qEIuDSR)%z+|n zatVlhQ?$w#XRS7xUrFE;Y8vMGhQS5*T{ZnY=q1P?w5g$OKJ#M&e??tAmPWHMj3xhS ziGxapy?kn@$~2%ZY;M8Bc@%$pkl%Rvj!?o%agBvpQ-Q61n9kznC4ttrRNQ4%GFR5u zyv%Yo9~yxQJWJSfj z?#HY$y=O~F|2pZs22pu|_&Ajd+D(Mt!nPUG{|1nlvP`=R#kKH zO*s$r_%ss5h1YO7k0bHJ2CXN)Yd6CHn~W!R=SqkWe=&nAZu(Q1G!xgcUilM@YVei@2@a`8he z9@pM`)VB*=e7-MWgLlXlc)t;fF&-AwM{E-EX}pViFn0I0CNw2bNEnN2dj!^4(^zS3 zobUm1uQnpqk_4q{pl*n06=TfK_C>UgurKFjRXsK_LEn};=79`TB12tv6KzwSu*-C8 z;=~ohDLZylHQ|Mpx-?yql>|e=vI1Z!epyUpAcDCp4T|*RV&X`Q$0ogNwy6mFALo^@ z9=&(9txO8V@E!@6^(W0{*~CT>+-MA~vnJULBxCTUW>X5>r7*eXYUT0B6+w@lzw%n> z_VjJ<2qf|(d6jYq2(x$(ZDf!yVkfnbvNmb5c|hhZ^2TV_LBz`9w!e_V*W_(MiA7|= z&EeIIkw*+$Xd!)j8<@_<}A5;~A_>3JT*kX^@}cDoLd>Qj<`Se^wdUa(j0dp+Tl8EptwBm{9OGsdFEq zM`!pjf(Lm(`$e3FLOjqA5LnN5o!}z{ zNf}rJuZh@yUtq&ErjHeGzX4(!luV!jB&;FAP|!R_QHYw#^Z1LwTePAKJ6X&IDNO#; z)#I@Xnnzyij~C@UH~X51JCgQeF0&hTXnuoElz#m{heZRexWc0k4<>0+ClX7%0 zEBqCCld1tD9Zwkr4{?Nor19#E5-YKfB8d?qgR82-Ow2^AuNevly2*tHA|sK!ybYkX zm-sLQH72P&{vEAW6+z~O5d0qd=xW~rua~5a?ymYFSD@8&gV)E5@RNNBAj^C99+Z5Z zR@Pq55mbCQbz+Mn$d_CMW<-+?TU960agEk1J<>d>0K=pF19yN))a~4>m^G&tc*xR+yMD*S=yip-q=H zIlredHpsJV8H(32@Zxc@bX6a21dUV95Th--8pE6C&3F>pk=yv$yd6@Haw;$v4+Fcb zRwn{Qo@0`7aPa2LQOP}j9v>sjOo5Kqvn|`FLizX zB+@-u4Lw|jsvz{p^>n8Vo8H2peIqJJnMN}A)q6%$Tmig7eu^}K2 zrh$X?T|ZMsoh{6pdw1G$_T<`Ds-G=jc;qcGdK4{?dN2-XxjDNbb(7pk|3JUVCU4y; z)?LXR>f+AAu)JEiti_Zy#z5{RgsC}R(@jl%9YZ>zu~hKQ*AxbvhC378-I@{~#%Y`Z zy=a=9YpewPIC+gkEUUwtUL7|RU7=!^Aa}Mk^6uxOgRGA#JXjWLsjFUnix|Mau{hDT z7mn*z1m5g`vP(#tjT0Zy4eAY(br&!RiiXE=ZI!{sE1#^#%x^Z7t1U)b<;%Y}Q9=5v z;wpDCEZ@OE36TWT=|gxigT@VaW9BvHS05;_P(#s z8zI4XFQys}q)<`tkX$WnSarn{3e!s}4(J!=Yf>+Y>cP3f;vr63f2{|S^`_pWc)^5_!R z*(x-fuBxL51@xe!lnDBKi}Br$c$BMZ3%f2Sa6kLabiBS{pq*yj;q|k(86x`PiC{p6 z_bxCW{>Q2BA8~Ggz&0jkrcU+-$ANBsOop*ms>34K9lNYil@}jC;?cYP(m^P}nR6FV zk(M%48Z&%2Rx$A&FhOEirEhY0(dn;-k(qkTU)sFQ`+-ih+s@A8g?r8Pw+}2;35WYf zi}VO`jS`p(tc)$X$a>-#WXoW!phhatC*$}|rk>|wUU71eUJG^$c6_jwX?iSHM@6__ zvV|6%U*$sSXJu9SX?2%M^kK|}a2QJ8AhF{fuXrHZxXsI~O zGKX45!K7p*MCPEQ=gp?eu&#AW*pR{lhQR##P_*{c_DjMGL|3T3-bSJ(o$|M{ytU}> zAV>wq*uE*qFo9KvnA^@juy{x<-u*#2NvkV={Ly}ysKYB-k`K3@K#^S1Bb$8Y#0L0# z`6IkSG&|Z$ODy|VLS+y5pFJx&8tvPmMd8c9FhCyiU8~k6FwkakUd^(_ml8`rnl>JS zZV){9G*)xBqPz^LDqRwyS6w86#D^~xP4($150M)SOZRe9sn=>V#aG0Iy(_^YcPpIz8QYM-#s+n% z@Jd?xQq?Xk6=<3xSY7XYP$$yd&Spu{A#uafiIfy8gRC`o0nk{ezEDjb=q_qRAlR1d zFq^*9Gn)yTG4b}R{!+3hWQ+u3GT~8nwl2S1lpw`s0X_qpxv)g+JIkVKl${sYf_nV~B>Em>M;RlqGb5WVil(89 zs=ld@|#;dq1*vQGz=7--Br-|l) zZ%Xh@v8>B7P?~}?Cg$q9_={59l%m~O&*a6TKsCMAzG&vD>k2WDzJ6!tc!V)+oxF;h zJH;apM=wO?r_+*#;ulohuP=E>^zon}a$NnlcQ{1$SO*i=jnGVcQa^>QOILc)e6;eNTI>os=eaJ{*^DE+~jc zS}TYeOykDmJ=6O%>m`i*>&pO_S;qMySJIyP=}4E&J%#1zju$RpVAkZbEl+p%?ZP^C z*$$2b4t%a(e+%>a>d_f_<JjxI#J1x;=hPd1zFPx=6T$;;X1TD*2(edZ3f46zaAoW>L53vS_J*N8TMB|n+;LD| zC=GkQPpyDY#Am4l49chDv*gojhRj_?63&&8#doW`INATAo(qY#{q}%nf@eTIXmtU< zdB<7YWfyCmBs|c)cK>1)v&M#!yNj#4d$~pVfDWQc_ke1?fw{T1Nce_b`v|Vp5ig(H zJvRD^+ps46^hLX;=e2!2e;w9y1D@!D$c@Jc&%%%IL=+xzw55&2?darw=9g~>P z9>?Kdc$r?6c$m%x2S$sdpPl>GQZ{rC9mPS63*qjCVa?OIBj!fW zm|g?>CVfGXNjOfcyqImXR_(tXS(F{FcoNzKvG5R$IgGaxC@)i(e+$ME}vPVIhd|mx2IIE+f zM?9opQHIVgBWu)^A|RzXw!^??S!x)SZOwZaJkGjc<_}2l^eSBm!eAJG9T>EC6I_sy z?bxzDIAn&K5*mX)$RQzDA?s)-no-XF(g*yl4%+GBf`##bDXJ==AQk*xmnatI;SsLp zP9XTHq5mmS=iWu~9ES>b%Q=1aMa|ya^vj$@qz9S!ih{T8_PD%Sf_QrNKwgrXw9ldm zHRVR98*{C?_XNpJn{abA!oix_mowRMu^2lV-LPi;0+?-F(>^5#OHX-fPED zCu^l7u3E%STI}c4{J2!)9SUlGP_@!d?5W^QJXOI-Ea`hFMKjR7TluLvzC-ozCPn1`Tpy z!vlv@_Z58ILX6>nDjTp-1LlFMx~-%GA`aJvG$?8*Ihn;mH37eK**rmOEwqegf-Ccx zrIX4;{c~RK>XuTXxYo5kMiWMy)!IC{*DHG@E$hx?RwP@+wuad(P1{@%tRkyJRqD)3 zMHHHZ4boqDn>-=DgR5VlhQTpfVy182Gk;A_S8A1-;U1RR>+$62>(MUx@Nox$vTjHq z%QR=j!6Gdyb5wu7y(YUktwMuW5<@jl?m4cv4BODiT5o8qVdC0MBqGr@-YBIwnpZAY znX9(_uQjP}JJ=!~Ve9#5I~rUnN|P_3D$LqZcvBnywYhjlMSFHm`;u9GPla{5QD7(7*6Tb3Svr8;(nuAd81q$*uq6HC_&~je*Ca7hP4sJp0av{M8480wF zxASi7Qv+~@2U%Nu1Ud;s-G4CTVWIPyx!sg&8ZG0Wq zG_}i3C(6_1>q3w!EH7$Kwq8uBp2F2N7}l65mk1p*9v0&+;th=_E-W)E;w}P(j⁢ zv5o9#E7!G0XmdzfsS{efPNi`1b44~SZ4Z8fuX!I}#8g+(wxzQwUT#Xb2(tbY1+EUhGKoT@KEU9Ktl>_0 z%bjDJg;#*gtJZv!-Zs`?^}v5eKmnbjqlvnSzE@_SP|LG_PJ6CYU+6zY6>92%E+ z=j@TZf-iW4(%U{lnYxQA;7Q!b;^brF8n0D>)`q5>|WDDXLrqYU_tKN2>=#@~OE7grMnNh?UOz-O~6 z6%rHy{#h9K0AT+lDC7q4{hw^|q6*Ry;;L%Q@)Ga}$60_q%D)rv(CtS$CQbpq9|y1e zRSrN4;$Jyl{m5bZw`$8TGvb}(LpY{-cQ)fcyJv7l3S52TLXVDsphtv&aPuDk1OzCA z4A^QtC(!11`IsNx_HnSy?>EKpHJWT^wmS~hc^p^zIIh@9f6U@I2 zC=Mve{j2^)mS#U$e{@Q?SO6%LDsXz@SY+=cK_QMmXBIU)j!$ajc-zLx3V60EXJ!qC zi<%2x8Q24YN+&8U@CIlN zrZkcT9yh%LrlGS9`G)KdP(@9Eo-AQz@8GEFWcb7U=a0H^ZVbLmz{+&M7W(nXJ4sN8 zJLR7eeK(K8`2-}j(T7JsO`L!+CvbueT%izanm-^A1Dn{`1Nw`9P?cq;7no+XfC`K(GO9?O^5zNIt4M+M8LM0=7Gz8UA@Z0N+lg+cX)NfazRu z5D)~HA^(u%w^cz+@2@_#S|u>GpB+j4KzQ^&Wcl9f z&hG#bCA(Yk0D&t&aJE^xME^&E-&xGHhXn%}psEIj641H+Nl-}boj;)Zt*t(4wZ5DN z@GXF$bL=&pBq-#vkTkh>7hl%K5|3 z{`Vn9b$iR-SoGENp}bn4;fR3>9sA%X2@1L3aE9yTra;Wb#_`xWwLSLdfu+PAu+o3| zGVnpzPr=ch{uuoHjtw7+_!L_2;knQ!DuDl0R`|%jr+}jFzXtrHIKc323?JO{l&;VF z*L1+}JU7%QJOg|5|Tc|D8fN zJORAg=_vsy{ak|o);@)Yh8Lkcg@$FG3k@ep36BRa^>~UmnRPziS>Z=`Jb2x*Q#`%A zU*i3&Vg?TluO@X0O;r2Jl6LKLUOVhSqg1*qOt^|8*c7 zo(298@+r$k_wQNGHv{|$tW(T8L+4_`FQ{kEW5Jgg{yf7ey4ss_(SNKfz(N9lx&a;< je(UuV8hP?p&}TPdm1I$XmG#(RzlD&B2izSj9sl%y5~4qc diff --git a/gradlew b/gradlew index 02c9a713f3..1aa94a4269 100755 --- a/gradlew +++ b/gradlew @@ -83,7 +83,8 @@ done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -144,7 +145,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac @@ -152,7 +153,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -199,13 +200,13 @@ fi # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-ea"' +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ diff --git a/gradlew.bat b/gradlew.bat index 9435c00236..93e3f59f13 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -34,7 +34,7 @@ set APP_HOME=%DIRNAME% for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-ea" +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome diff --git a/javatools/build.gradle.kts b/javatools/build.gradle.kts index 150efb12b6..e476f59125 100644 --- a/javatools/build.gradle.kts +++ b/javatools/build.gradle.kts @@ -1,77 +1,138 @@ -import org.gradle.language.base.plugins.LifecycleBasePlugin.BUILD_GROUP +import XdkBuildLogic.Companion.XDK_ARTIFACT_NAME_JAVATOOLS_FATJAR +import org.gradle.language.base.plugins.LifecycleBasePlugin.VERIFICATION_GROUP -/* +/** * Build file for the Java tools portion of the XDK. */ plugins { - java + alias(libs.plugins.xdk.build.java) + alias(libs.plugins.tasktree) } -dependencies { - implementation("org.xtclang.xvm:javatools_utils:") -} +val semanticVersion: SemanticVersion by extra -java { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 +val xdkJavaToolsUtils by configurations.registering { + description = "Consumer configuration of the classes for the XVM Java Tools Utils project (version $version)" + isCanBeResolved = true + isCanBeConsumed = false + attributes { + attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.LIBRARY)) + attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements.CLASSES)) + } } -testing { - suites { - val test by getting(JvmTestSuite::class) { - useJUnitJupiter() - } +// TODO: Move these to common-plugins, the XDK composite build does use them in some different places. +val xdkJavaToolsProvider by configurations.registering { + description = "Provider configuration of the The XVM Java Tools jar artifact: 'javatools-$version.jar'" + isCanBeResolved = false + isCanBeConsumed = true + outgoing.artifact(tasks.jar) + attributes { + attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.LIBRARY)) + attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(XDK_ARTIFACT_NAME_JAVATOOLS_FATJAR)) } } -tasks { - val copyImplicits by registering(Copy::class) { - group = BUILD_GROUP - description = "Copy the implicit.x from :lib_ecstasy project into the build directory." - from(file(project(":lib_ecstasy").property("implicit.x")!!)) - into(file("$buildDir/resources/main/")) - doLast { - logger.info("Finished task: copyImplicits") +dependencies { + @Suppress("UnstableApiUsage") + xdkJavaToolsUtils(libs.javatools.utils) + // Normal use case: The javatools-utils is included in the Javatools uber-jar, so we need it only as compile only. + // compileOnly(libs.javatools.utils) + // Debugging use case: If we want IDE debugging, executing the Compiler or Runner manually, we need javatools-utils as an implementation dependency. + implementation(libs.javatools.utils) + testImplementation(libs.javatools.utils) +} + +/** + * TODO: Someone please determine if this is something we should fix or not: + * + * We add the implicits.x file to the resource set, to both make it part of the javatools.jar + available. + * IntelliJ may warn that we have a "duplicate resource folder" if this is executed by a Run/Debug configuration. + * This is because lib_ecstasy is the place where these resources are originally placed. I'm not sure if + * they need to be there to be compiled into the ecstasy.xtc binary, in order for the launchers to work, or + * if they only need to be in the javatools.jar. If the latter is the case, we should move them. If the + * former is the case, they should be resolved from the the built ecstasy.xtc module on the module path + * anyway. + * + * We also need to refer to the "mack" module during runtime in the debugger. I suppose we can't access that + * due to a similar reason as above - IntelliJ needs a reference to it to be able to invoke it in + * the debug session, and it resides in a different module too (javatools_turtle). Seems weird that it + * doesn't get that from the ecstasy.xtc file that actually IS on the module path in the debug run. + */ +sourceSets { + main { + resources { + srcDir(file(xdkImplicitsFile.parent)) // May trigger a warning in your IDE, since we are referencing someone else's (javatools_turtle) main resource path. IDEs like to have one. } } +} - val copyUtils by registering(Copy::class) { - group = BUILD_GROUP - description = "Copy the classes from :javatools_utils project into the build directory." - dependsOn(project(":javatools_utils").tasks["classes"]) - from(file("${project(":javatools_utils").buildDir}/classes/java/main")) - include("**/*.class") - into(file("$buildDir/classes/java/main")) - doLast { - logger.info("Finished task: copyUtils") - } +val jar by tasks.existing(Jar::class) { + inputs.property("manifestSemanticVersion") { + semanticVersion.toString() + } + inputs.property("manifestVersion") { + version } + archiveBaseName = "javatools" - jar { - dependsOn(copyImplicits, copyUtils) - mustRunAfter(copyImplicits, copyUtils) - - val version = rootProject.version - manifest { - attributes["Manifest-Version"] = "1.0" - attributes["Sealed"] = "true" - attributes["Main-Class"] = "org.xvm.tool.Launcher" - attributes["Name"] = "/org/xvm/" - attributes["Specification-Title"] = "xvm" - attributes["Specification-Version"] = version - attributes["Specification-Vendor"] = "xtclang.org" - attributes["Implementation-Title"] = "xvm-prototype" - attributes["Implementation-Version"] = version - attributes["Implementation-Vendor"] = "xtclang.org" - } + // TODO: It may be fewer special cases if we just add to the source sets from these dependencies, but it's not + // apparent how to get that correct for includedBuilds. + from(xdkJavaToolsUtils) + + manifest { + attributes( + "Manifest-Version" to "1.0", + "Xdk-Version" to semanticVersion.toString(), + "Sealed" to "true", + "Main-Class" to "org.xvm.tool.Launcher", + "Name" to "/org/xvm/", + "Specification-Title" to "xvm", + "Specification-Version" to version, + "Specification-Vendor" to "xtclang.org", + "Implementation-Title" to "xvm-prototype", + "Implementation-Version" to version, + "Implementation-Vendor" to "xtclang.org" + ) } +} + +val assemble by tasks.existing { + dependsOn(sanityCheckJar) // "assemble" already depends on jar by default in the Java build life cycle +} - compileTestJava { - dependsOn(copyImplicits, copyUtils) +val sanityCheckJar by tasks.registering { + group = VERIFICATION_GROUP + description = + "If the properties are enabled, verify that the javatools.jar file is sane (contains expected packages and files), and optionally, that it has a certain number of entries." + + dependsOn(jar) + + val checkJar = getXdkPropertyBoolean("org.xtclang.javatools.sanityCheckJar") + val expectedEntryCount = getXdkPropertyInt("org.xtclang.javatools.verifyJar.expectedFileCount", -1) + inputs.properties("sanityCheckJarBoolean" to checkJar, "sanityCheckJarEntryCount" to expectedEntryCount) + inputs.file(jar) + + logger.info("$prefix Configuring sanityCheckJar task (enabled: $checkJar, expected entry count: $expectedEntryCount)") + + onlyIf { + checkJar } + doLast { + logger.info("$prefix Sanity checking integrity of generated jar file.") + + DebugBuild.verifyJarFileContents( + project, + listOfNotNull( + "implicit.x", // verify the implicits are in the jar + "org/xvm/tool/Compiler", // verify the javatools package is in there, including Compiler and Runner + "org/xvm/tool/Runner", + "org/xvm/util/Severity" // verify the + ), + expectedEntryCount + ) // Check for files in both javatools_utils and javatools + implicit.x - test { - maxHeapSize = "1G" + logger.info("$prefix Sanity check of javatools.jar completed successfully.") } } diff --git a/javatools/javatools.properties b/javatools/javatools.properties new file mode 100644 index 0000000000..79045b2ddf --- /dev/null +++ b/javatools/javatools.properties @@ -0,0 +1,12 @@ +# Should we verify the javatools jar file, i.e. check that it has both utils and tools in it and, +# optionally, including optionally counting the files. +org.xtclang.javatools.verifyJar=true + +# Remove this definition, or set the value to -1 to skip file count. If the javatools.jar contents +# frequently changes, it is a bit of a hassle to reset this property every time. However, it has +# remained stable for very long periods during the XTC project development. +# The last known file count for a valid javatools jar was "1259". +org.xtclang.javatools.verifyJar.expectedFileCount=-1 + +# TODO: TEMPORARILY DISABLE JAVAC LINTING / WARNINGS FOR THIS PROJECT, BECAUASE IT *SPEWS* THEM. +org.xtclang.java.lint=false diff --git a/javatools/settings.gradle.kts b/javatools/settings.gradle.kts new file mode 100644 index 0000000000..0c707ec7c6 --- /dev/null +++ b/javatools/settings.gradle.kts @@ -0,0 +1,10 @@ +pluginManagement { + includeBuild("../build-logic/settings-plugins") + includeBuild("../build-logic/common-plugins") +} + +plugins { + id("org.xtclang.build.common") +} + +rootProject.name = "javatools" diff --git a/javatools/src/main/java/org/xvm/asm/Constants.java b/javatools/src/main/java/org/xvm/asm/Constants.java index 7e5f315dc9..d2c826946b 100755 --- a/javatools/src/main/java/org/xvm/asm/Constants.java +++ b/javatools/src/main/java/org/xvm/asm/Constants.java @@ -579,5 +579,5 @@ public static Access valueOf(int i) /** * Compile-time debug flag. */ - boolean DEBUG = true; + boolean DEBUG = Boolean.parseBoolean(System.getProperty("xtc.debug", "true")); } \ No newline at end of file diff --git a/javatools/src/main/java/org/xvm/tool/Compiler.java b/javatools/src/main/java/org/xvm/tool/Compiler.java index 55edb01ace..d0404d0084 100644 --- a/javatools/src/main/java/org/xvm/tool/Compiler.java +++ b/javatools/src/main/java/org/xvm/tool/Compiler.java @@ -109,7 +109,7 @@ public static void main(String[] asArg) { try { - new Compiler(asArg).run(); + launch(asArg); } catch (LauncherException e) { @@ -117,6 +117,10 @@ public static void main(String[] asArg) } } + public static void launch(String[] asArg) throws LauncherException { + new Compiler(asArg).run(); + } + /** * Compiler constructor. * diff --git a/javatools/src/main/java/org/xvm/tool/Launcher.java b/javatools/src/main/java/org/xvm/tool/Launcher.java index 44c5388dc6..b36b1e4370 100644 --- a/javatools/src/main/java/org/xvm/tool/Launcher.java +++ b/javatools/src/main/java/org/xvm/tool/Launcher.java @@ -53,7 +53,7 @@ * */ public abstract class Launcher - implements ErrorListener + implements ErrorListener, Runnable { /** * Entry point from the OS. @@ -117,6 +117,7 @@ public Launcher(String[] asArgs, Console console) /** * Execute the Launcher tool. */ + @Override public void run() { Options opts = options(); @@ -1427,7 +1428,7 @@ public ModuleInfo ensureModuleInfo(File fileSpec, File[] resourceSpecs, File bin * * @param listPath */ - public void validateModulePath(List listPath) + public void validateModulePath(List listPath) throws LauncherException { for (File file : listPath) { @@ -1736,17 +1737,26 @@ static public class LauncherException /** * @param error true to abort with an error status */ - public LauncherException(boolean error) + public LauncherException(final boolean error) { - super(); + this(error, null); + } + public LauncherException(final boolean error, final String msg) + { + super(msg); this.error = error; } + @Override + public String toString() + { + return '[' + getClass().getSimpleName() + ": isError=" + error + ", msg=" + getMessage() + ']'; + } + public final boolean error; } - // ----- constants ----------------------------------------------------------------------------- /** diff --git a/javatools/src/main/java/org/xvm/tool/Runner.java b/javatools/src/main/java/org/xvm/tool/Runner.java index 594522f0d8..5c1b51e8f2 100644 --- a/javatools/src/main/java/org/xvm/tool/Runner.java +++ b/javatools/src/main/java/org/xvm/tool/Runner.java @@ -55,7 +55,7 @@ public static void main(String[] asArg) { try { - new Runner(asArg).run(); + launch(asArg); } catch (LauncherException e) { @@ -63,6 +63,10 @@ public static void main(String[] asArg) } } + public static void launch(String[] asArg) throws LauncherException { + new Runner(asArg).run(); + } + /** * Runner constructor. * diff --git a/javatools_bridge/build.gradle.kts b/javatools_bridge/build.gradle.kts index 9f1140093f..7859ccac9d 100644 --- a/javatools_bridge/build.gradle.kts +++ b/javatools_bridge/build.gradle.kts @@ -1,18 +1,26 @@ +import org.xtclang.plugin.tasks.XtcCompileTask + /* * Build file for the JavaTools "bridge" (aka "_native") module that is used to connect the Java * runtime to the Ecstasy type system. - * - * This project does NOT build the javatools_bridge.xtc file. (The :xdk project builds it.) */ -tasks.register("clean") { - group = "Build" - description = "Delete previous build results" - // this project does not build anything itself, so there is nothing to clean +plugins { + id("org.xtclang.build.xdk.versioning") + alias(libs.plugins.xtc) +} + +dependencies { + xdkJavaTools(libs.javatools) + xtcModule(libs.xdk.ecstasy) + xtcModule(libs.xdk.aggregate) + xtcModule(libs.xdk.collections) + xtcModule(libs.xdk.crypto) + xtcModule(libs.xdk.json) + xtcModule(libs.xdk.net) + xtcModule(libs.xdk.web) } -tasks.register("build") { - group = "Build" - description = "Build this project" - // this project does not build anything itself +val compileXtc by tasks.existing(XtcCompileTask::class) { + outputFilename("_native.xtc" to "javatools_bridge.xtc") } diff --git a/javatools_launcher/README.md b/javatools_launcher/README.md index c90575a3fb..ff0d27eedc 100644 --- a/javatools_launcher/README.md +++ b/javatools_launcher/README.md @@ -1,11 +1,6 @@ This sub-project is used to create native launchers that can be included in the XDK to execute various commands. -***WARNING: DO NOT DELETE THE BUILD DIRECTORY!*** _The project's `./build/` directory is not disposable; it is actually stored in `git`, with -the executable artifacts also stored in `git`, etc. More details can be found below._ - -***WARNING: THIS PROJECT DOES NOT ADHERE TO NORMAL GRADLE BUILD RULES!*** - The prototype runtime is implemented in Java, which makes it challenging for developers who are not used to Java command line execution. Our goal with the launcher was to simplify the use of the Java tools from the command line, by making them as simple to use as normal command-line tools are in any @@ -135,4 +130,5 @@ which will allow you to debug from IDEA: * In IDEA, use the "Select Run/Debug Configuration" drop-down in the toolbar to select the name previously configured; for example, `Debug Ecstasy Command`. Then press the "Debug" button - (usually `Shift-F9`). At this point, IDEA should connect to the command that you wish to debug. \ No newline at end of file + (usually `Shift-F9`). At this point, IDEA should connect to the command that you wish to debug. + \ No newline at end of file diff --git a/javatools_launcher/build.gradle.kts b/javatools_launcher/build.gradle.kts index 4a5292dd99..c1ae56fa35 100644 --- a/javatools_launcher/build.gradle.kts +++ b/javatools_launcher/build.gradle.kts @@ -2,5 +2,40 @@ * Build file for the javatools_launcher project. * * The launcher programs need to be built on different hardware/OS combinations, so this project is - * not currently automated. + * not currently automated. Currently, the pre-built executables for different platforms are placed + * in the reources of this module. + * + * TODO: We could add a mechanism to native compile this, but it would have to cross compile + * from all potential build platforms to native launchers for all supported platforms. The + * Gradle native plugins for c/cpp takes us almost all the way there, but misses some of + * the cross compilation abilities. */ + +plugins { + base +} + +val launcherExecutableDir = layout.projectDirectory.dir("src/main/resources/exe") + +val processResources by tasks.registering(Copy::class) { + from(files(launcherExecutableDir)) + exclude("**/README.md") + eachFile { + relativePath = RelativePath(true, name) + } + includeEmptyDirs = false + into(layout.buildDirectory.file("bin")) +} + +val assemble by tasks.existing { + dependsOn(processResources) + doLast { + logger.info("$prefix Finished assembling launcher resources into Gradle build.") + } +} + +val xtcLauncherBinaries by configurations.registering { + isCanBeResolved = false + isCanBeConsumed = true + outgoing.artifact(processResources) +} diff --git a/javatools_launcher/build/exe/linux_launcher b/javatools_launcher/src/main/resources/exe/linux_launcher similarity index 100% rename from javatools_launcher/build/exe/linux_launcher rename to javatools_launcher/src/main/resources/exe/linux_launcher diff --git a/javatools_launcher/build/exe/macos_launcher b/javatools_launcher/src/main/resources/exe/macos_launcher similarity index 100% rename from javatools_launcher/build/exe/macos_launcher rename to javatools_launcher/src/main/resources/exe/macos_launcher diff --git a/javatools_launcher/build/exe/windows_launcher.exe b/javatools_launcher/src/main/resources/exe/windows_launcher.exe similarity index 100% rename from javatools_launcher/build/exe/windows_launcher.exe rename to javatools_launcher/src/main/resources/exe/windows_launcher.exe diff --git a/javatools_launcher/build/javatools/README.md b/javatools_launcher/src/main/resources/javatools/README.md similarity index 100% rename from javatools_launcher/build/javatools/README.md rename to javatools_launcher/src/main/resources/javatools/README.md diff --git a/javatools_launcher/build/javatools/javatools.jar b/javatools_launcher/src/main/resources/javatools/javatools.jar similarity index 100% rename from javatools_launcher/build/javatools/javatools.jar rename to javatools_launcher/src/main/resources/javatools/javatools.jar diff --git a/javatools_turtle/build.gradle.kts b/javatools_turtle/build.gradle.kts new file mode 100644 index 0000000000..cfc6b89777 --- /dev/null +++ b/javatools_turtle/build.gradle.kts @@ -0,0 +1,23 @@ +import XdkBuildLogic.Companion.XDK_ARTIFACT_NAME_MACK_DIR +import org.gradle.api.attributes.Category.CATEGORY_ATTRIBUTE +import org.gradle.api.attributes.Category.LIBRARY +import org.gradle.api.attributes.LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE + +plugins { + id("org.xtclang.build.xdk.versioning") + alias(libs.plugins.xtc) +} + +val xdkTurtleProvider by configurations.registering { + isCanBeResolved = false + isCanBeConsumed = true + outgoing.artifact(tasks.processResources) + attributes { + attribute(CATEGORY_ATTRIBUTE, objects.named(LIBRARY)) + attribute(LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(XDK_ARTIFACT_NAME_MACK_DIR)) + } +} + +val compileXtc by tasks.existing { + enabled = false +} diff --git a/javatools_turtle/src/main/x/mack.x b/javatools_turtle/src/main/resources/mack.x similarity index 100% rename from javatools_turtle/src/main/x/mack.x rename to javatools_turtle/src/main/resources/mack.x diff --git a/javatools_unicode/build.gradle.kts b/javatools_unicode/build.gradle.kts index 969afd34c2..7d54e8fa63 100644 --- a/javatools_unicode/build.gradle.kts +++ b/javatools_unicode/build.gradle.kts @@ -3,27 +3,93 @@ * * Technically, this only needs to be built and run when new versions of the Unicode standard are * released, and when that occurs, the code in Char.x also has to be updated (to match the .dat file - * data) using the values in the *.txt files that are outputted by running this. + * data) using the values in the *.txt files that are output by running this. */ +import de.undercouch.gradle.tasks.download.Download +import org.gradle.language.base.plugins.LifecycleBasePlugin.BUILD_GROUP + plugins { - application + alias(libs.plugins.xdk.build.java) + alias(libs.plugins.download) + alias(libs.plugins.tasktree) } dependencies { - implementation("com.sun.activation:javax.activation:1.2.0") - implementation("jakarta.xml.bind:jakarta.xml.bind-api:2.3.2") - implementation("org.glassfish.jaxb:jaxb-runtime:2.3.2") - implementation("org.xtclang.xvm:javatools_utils:") + implementation(libs.bundles.unicode) + implementation(libs.javatools.utils) } -// TODO: All Java configuration will move to build-logic in the Gradle language support branch, -// but we leave this here for the moment. -java { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 +val unicodeUcdUrl = "https://unicode.org/Public/UCD/latest/ucdxml/ucd.all.flat.zip" +val processedResourcesDir = tasks.processResources.get().outputs.files.singleFile + +/** + * Type safe "jar" task accessor. + */ +val jar by tasks.existing(Jar::class) + +/** + * Download the ucd zip file from the unicode site, if it does not exist. + */ +val downloadUcdFlatZip by tasks.registering(Download::class) { + onlyIf { + System.err.println("Check rebuildunicode spec: should be executing: ${state.executing}") + assert(state.executing) + getXdkPropertyBoolean("org.xtclang.unicode.rebuild", false) // TODO inputproperty + } + src(unicodeUcdUrl) + overwrite(false) + onlyIfModified(true) + quiet(false) + dest(project.mkdir(project.layout.buildDirectory.dir("ucd"))) } -application { - mainClass.set("org.xvm.tool.BuildUnicodeTables") +/** + * Build unicode tables, and put them under the build directory. + * + * For a normal run, the unicode resources are already copied to the build directory + * by the processResources task, which as part of any default Java Plugin build lifecycle, + * will copy the src//resources directory to build/resources/ + * In that case, when resolveUnicodeTables is set to false, the only thing this task does + * is add the processResources outputs as its own outputs. If it's true, we will overwrite + * those resources to the build folder, and optionally, copy them to replace the source + * folder resources. + * + * We never execute this task explicitly, but we do declare a consumable coniguration that + * contains the output of this task, forcing it to run (and maybe rebuild unicode files) if + * anyone wants to resolve the config. The lib_ecstasy project adds this configuration to + * its incoming resources, which means that lib_ecstasy will include them in the ecstasy.xtc + * module. All we need to do is add the configuration as a resource for lib_ecstasy. + */ +val rebuildUnicodeTables by tasks.registering { + group = BUILD_GROUP + description = "If the unicode files should be regenerated, generate them from the build tool, and place them under the build resources." + + val rebuildUnicode = getXdkPropertyBoolean("org.xtclang.unicode.rebuild", false) + logger.info("$prefix Should rebuild unicode: $rebuildUnicode") + + dependsOn(jar) + outputs.dir(processedResourcesDir) + + if (rebuildUnicode) { + dependsOn(downloadUcdFlatZip) + doLast { + logger.lifecycle("$prefix Rebuilding unicode tables...") + val unicodeJar = jar.get().archiveFile + val localUcdZip = downloadUcdFlatZip.get().outputs.files.singleFile + logger.lifecycle("$prefix Downloaded unicode file: ${localUcdZip.absolutePath}") + javaexec { + mainClass = "org.xvm.tool.BuildUnicodeTables" + classpath(configurations.runtimeClasspath) + classpath(unicodeJar) + args(localUcdZip.absolutePath, File(processedResourcesDir, "ecstasy/text")) + } + } + } + + doLast { + printTaskInputs() + printTaskOutputs() + printTaskDependencies() + } } diff --git a/javatools_unicode/settings.gradle.kts b/javatools_unicode/settings.gradle.kts new file mode 100644 index 0000000000..1b24d00207 --- /dev/null +++ b/javatools_unicode/settings.gradle.kts @@ -0,0 +1,10 @@ +pluginManagement { + includeBuild("../build-logic/settings-plugins") + includeBuild("../build-logic/common-plugins") +} + +plugins { + id("org.xtclang.build.common") +} + +rootProject.name = "javatools-unicode" diff --git a/javatools_unicode/src/main/java/org/xvm/tool/BuildUnicodeTables.java b/javatools_unicode/src/main/java/org/xvm/tool/BuildUnicodeTables.java index 5129ce84cf..e8ef9801a7 100644 --- a/javatools_unicode/src/main/java/org/xvm/tool/BuildUnicodeTables.java +++ b/javatools_unicode/src/main/java/org/xvm/tool/BuildUnicodeTables.java @@ -1,6 +1,5 @@ package org.xvm.tool; - import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; @@ -12,7 +11,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.util.Map; import java.util.TreeMap; import java.util.zip.ZipEntry; @@ -32,340 +30,316 @@ import org.xvm.util.ConstOrdinalList; +import static java.util.Objects.requireNonNull; +import static java.util.Objects.requireNonNullElseGet; /** * Using the raw information from {@code ./resources/unicode/*.zip}, build the Unicode data tables * used by the Char class. */ -public class BuildUnicodeTables - { +public class BuildUnicodeTables { + public static final boolean TEST = false; + + private static final String UCD_ALL_FLAT_XML = "ucd.all.flat.xml"; + private static final File OUTPUT_DIR = new File("./build/resources/unicode/"); + + private final String[] asArgs; + /** * Entry point from the OS. * - * @param asArg command line arguments + * @param asArgs command line arguments */ - public static void main(String[] asArg) - { - BuildUnicodeTables tool = new BuildUnicodeTables(asArg); - tool.run(); - } + public static void main(final String[] asArgs) throws IOException, JAXBException { + new BuildUnicodeTables(asArgs).run(); + } /** - * @param asArgs the Launcher's command-line arguments + * @param asArgs the Launcher's command-line arguments */ - public BuildUnicodeTables(String[] asArgs) - { - } + public BuildUnicodeTables(final String[] asArgs) { + this.asArgs = asArgs; + } /** * Execute the Launcher tool. */ - public void run() - { + public void run() throws IOException, JAXBException { out("Locating Unicode raw data ..."); - List listRaw = loadData(); + final List listRaw = loadData(); int nHigh = -1; - for (CharData cd : listRaw) - { - int n = cd.lastIndex(); - if (n > nHigh) - { + for (final CharData cd : listRaw) { + final int n = cd.lastIndex(); + if (n > nHigh) { nHigh = n; - } } - int cAll = nHigh + 1; + } + final int cAll = nHigh + 1; out("Processing Unicode codepoints 0.." + nHigh); // various data collections - int [] cats = new int [cAll]; Arrays.fill(cats, new CharData().cat()); - // String[] labels = new String[cAll]; - int [] decs = new int [cAll]; Arrays.fill(decs, 10); // 10 is illegal; use as "null" - String[] nums = new String[cAll]; - int [] cccs = new int [cAll]; Arrays.fill(cccs, 255); // 255 is illegal; use as "null" - int [] lowers = new int [cAll]; - int [] uppers = new int [cAll]; - int [] titles = new int [cAll]; - String[] blocks = new String[cAll]; - - for (CharData cd : listRaw) - { - for (int codepoint = cd.firstIndex(), iLast = cd.lastIndex(); codepoint <= iLast; ++codepoint) - { - cats [codepoint] = cd.cat(); - // labels[codepoint] = cd.label(); - decs [codepoint] = cd.dec(); - nums [codepoint] = cd.num(); - cccs [codepoint] = cd.combo(); + final int[] cats = new int[cAll]; + Arrays.fill(cats, new CharData().cat()); + // String[] labels = new String[cAll]; + final int[] decs = new int[cAll]; + Arrays.fill(decs, 10); // 10 is illegal; use as "null" + final String[] nums = new String[cAll]; + final int[] cccs = new int[cAll]; + Arrays.fill(cccs, 255); // 255 is illegal; use as "null" + final int[] lowers = new int[cAll]; + final int[] uppers = new int[cAll]; + final int[] titles = new int[cAll]; + final String[] blocks = new String[cAll]; + + for (final CharData cd : listRaw) { + for (int codepoint = cd.firstIndex(), iLast = cd.lastIndex(); codepoint <= iLast; ++codepoint) { + cats[codepoint] = cd.cat(); + // labels[codepoint] = cd.label(); + decs[codepoint] = cd.dec(); + nums[codepoint] = cd.num(); + cccs[codepoint] = cd.combo(); lowers[codepoint] = cd.lower(); uppers[codepoint] = cd.upper(); titles[codepoint] = cd.title(); blocks[codepoint] = cd.block(); - - } } + } - writeResult("Cats" , cats); - // writeResult("Labels", labels); - writeResult("Decs" , decs); - writeResult("Nums" , nums); - writeResult("CCCs" , cccs); + writeResult("Cats", cats); + // writeResult("Labels", labels); + writeResult("Decs", decs); + writeResult("Nums", nums); + writeResult("CCCs", cccs); writeResult("Lowers", lowers); writeResult("Uppers", uppers); writeResult("Titles", titles); writeResult("Blocks", blocks); - } - - public List loadData() - { - try - { - String sXML; - if (TEST) - { - ClassLoader loader = BuildUnicodeTables.class.getClassLoader(); - if (loader == null) - { - loader = ClassLoader.getSystemClassLoader(); - } - String sFile = loader.getResource("test.xml").getFile(); - File file = new File(sFile); - assert file.exists(); - assert file.isFile(); - assert file.canRead(); - - long lRawLen = file.length(); - assert lRawLen < 2 * 1000 * 1000 * 1000; - - int cbRaw = (int) lRawLen; - byte[] abRaw = new byte[cbRaw]; - InputStream in = new FileInputStream(file); - int cbActual = in.readNBytes(abRaw, 0, cbRaw); - assert cbActual == cbRaw; - sXML = new String(abRaw); - } - else - { - String sZip = "ucd.all.flat.zip"; - File file = new File(sZip); - if (!(file.exists() && file.isFile() && file.canRead())) - { - ClassLoader loader = BuildUnicodeTables.class.getClassLoader(); - if (loader == null) - { - loader = ClassLoader.getSystemClassLoader(); - } - sZip = loader.getResource(sZip).getFile(); - } - - ZipFile zip = new ZipFile(sZip); - ZipEntry entryXML = zip.getEntry("ucd.all.flat.xml"); - long lRawLen = entryXML.getSize(); - assert lRawLen < 2 * 1000 * 1000 * 1000; - - int cbRaw = (int) lRawLen; - byte[] abRaw = new byte[cbRaw]; - InputStream in = zip.getInputStream(entryXML); - int cbActual = in.readNBytes(abRaw, 0, cbRaw); + } + + public List loadData() throws IOException, JAXBException { + final String sXML; + if (TEST) { + sXML = loadDataTest(); + } else { + final var zip = getZipFile(); + final ZipEntry entryXML = zip.getEntry(UCD_ALL_FLAT_XML); + final long lRawLen = entryXML.getSize(); + assert lRawLen < 2 * 1000 * 1000 * 1000; + + final int cbRaw = (int)lRawLen; + final byte[] abRaw = new byte[cbRaw]; + try (final InputStream in = zip.getInputStream(entryXML)) { + final int cbActual = in.readNBytes(abRaw, 0, cbRaw); assert cbActual == cbRaw; sXML = new String(abRaw); - } - - JAXBContext jaxbContext = JAXBContext.newInstance(UCDData.class); - Unmarshaller jaxbUnmarshaller = jaxbContext.createUnmarshaller(); - UCDData data = (UCDData) jaxbUnmarshaller.unmarshal(new StringReader(sXML)); - return data.repertoire; } - catch (IOException | JAXBException e) - { - throw new RuntimeException(e); + } + + final JAXBContext jaxbContext = JAXBContext.newInstance(UCDData.class); + final Unmarshaller jaxbUnmarshaller = jaxbContext.createUnmarshaller(); + final UCDData data = (UCDData)jaxbUnmarshaller.unmarshal(new StringReader(sXML)); + return data.repertoire; + } + + private static String loadDataTest() throws IOException { + final String sXML; + final ClassLoader loader = requireNonNullElseGet(BuildUnicodeTables.class.getClassLoader(), ClassLoader::getSystemClassLoader); + final String sFile = requireNonNull(loader.getResource("test.xml")).getFile(); + final File file = new File(sFile); + assert file.exists(); + assert file.isFile(); + assert file.canRead(); + + final long lRawLen = file.length(); + assert lRawLen < 2 * 1000 * 1000 * 1000; + + final int cbRaw = (int)lRawLen; + final byte[] abRaw = new byte[cbRaw]; + try (final InputStream in = new FileInputStream(file)) { + final int cbActual = in.readNBytes(abRaw, 0, cbRaw); + assert cbActual == cbRaw; + sXML = new String(abRaw); + } + return sXML; + } + + private File resolveArgumentAsFile() { + if (asArgs.length > 0) { + final File ucdZip = new File(asArgs[0]); + out(getClass().getSimpleName() + " UCD zip file: " + ucdZip.getAbsolutePath()); + return ucdZip; + } + return null; + } + + private File resolveArgumentAsDestinationDir() { + if (asArgs.length > 1) { + final File destDir = new File(asArgs[1]); + out(getClass().getSimpleName() + " destination directory: " + destDir.getAbsolutePath()); + return destDir; + } + return OUTPUT_DIR; + } + + private ZipFile getZipFile() throws IOException { + final var file = requireNonNullElseGet(resolveArgumentAsFile(), () -> new File(UCD_ALL_FLAT_XML)); + if (!(file.exists() && file.isFile() && file.canRead())) { + final ClassLoader loader = requireNonNullElseGet(BuildUnicodeTables.class.getClassLoader(), ClassLoader::getSystemClassLoader); + final var resource = loader.getResource(UCD_ALL_FLAT_XML); + if (resource == null) { + throw new IOException("Cannot find resources for unicode file: " + UCD_ALL_FLAT_XML); } + return new ZipFile(resource.getFile()); } + out("Reverting to zip file: " + file.getAbsolutePath()); + return new ZipFile(file); + } - void writeResult(String name, String[] array) - { + void writeResult(final String name, final String[] array) throws IOException { // collect and sort the values - TreeMap map = new TreeMap<>(); - int c = array.length; - for (int i = 0; i < c; ++i) - { - String s = array[i]; - if (s != null) - { + final var map = new TreeMap(); + final int c = array.length; + for (final var s : array) { + if (s != null) { assert !s.isEmpty(); - map.compute(s, (k, v) -> (v==null?0:v) + 1); - } + map.compute(s, (k, v) -> (v == null ? 0 : v) + 1); } + } - StringBuilder sb = new StringBuilder(); - sb.append(name) - .append(": [index] \"str\" (freq) \n--------------------"); + final var sb = new StringBuilder(); + sb.append(name).append(": [index] \"str\" (freq) \n--------------------"); int index = 0; - for (Map.Entry entry : map.entrySet()) - { - sb.append("\n[") - .append(index) - .append("] \"") - .append(entry.getKey()) - .append("\" (") - .append(entry.getValue()) - .append("x)"); - + for (final var entry : map.entrySet()) { + sb.append("\n[").append(index).append("] \"").append(entry.getKey()).append("\" (").append(entry.getValue()).append("x)"); entry.setValue(index++); - } + } - int indexNull = index; - sb.append("\n\ndefault=") - .append(indexNull); + final int indexNull = index; + sb.append("\n\ndefault=").append(indexNull); writeDetails(name, sb.toString()); // assign indexes to each - int[] an = new int[c]; - for (int i = 0; i < c; ++i) - { - String s = array[i]; + final int[] an = new int[c]; + for (int i = 0; i < c; ++i) { + final String s = array[i]; an[i] = s == null ? indexNull : map.get(s); - } - - writeResult(name, an); } - void writeResult(String name, int[] array) - { -// if (name.equals("Cats")) -// { -// out("cats:"); -// for (int i = 0; i < 128; ++i) -// { -// out("[" + i + "]=" + array[i]); -// } -// } + writeResult(name, an); + } + + void writeResult(final String name, final int[] array) throws IOException { + // if (name.equals("Cats")) + // { + // out("cats:"); + // for (int i = 0; i < 128; ++i) + // { + // out("[" + i + "]=" + array[i]); + // } + // } writeResult(name, ConstOrdinalList.compress(array, 256)); + } + + private File resolveOutput(final String name, final String extension) throws IOException { + final var filename = "Char" + name + '.' + extension; + final var dir = resolveArgumentAsDestinationDir(); + if (!dir.exists() && !dir.mkdirs()) { + throw new IOException("Could not access or create dir: '" + dir.getAbsolutePath() + '\''); } + assert dir.canWrite(); + return new File(dir, filename); + } - void writeResult(String name, byte[] data) - { - try - { - String filename = "Char" + name + ".dat"; - File dir = new File("./build/resources/"); - File file = dir.exists() && dir.isDirectory() && dir.canWrite() - ? new File(dir, filename) - : new File(filename); - FileOutputStream out = new FileOutputStream(file); + void writeResult(final String name, final byte[] data) throws IOException { + try (final var out = new FileOutputStream(resolveOutput(name, "dat"))) { out.write(data); - out.close(); - } - catch (IOException e) - { - throw new RuntimeException(e); - } } + } - void writeDetails(String name, String details) - { - try - { - String filename = "Char" + name + ".txt"; - File dir = new File("./build/resources/"); - File file = dir.exists() && dir.isDirectory() && dir.canWrite() - ? new File(dir, filename) - : new File(filename); - FileWriter writer = new FileWriter(file); - writer.write(details); - writer.close(); - } - catch (IOException e) - { - throw new RuntimeException(e); - } + void writeDetails(final String name, final String details) throws IOException { + try (final var out = new FileWriter(resolveOutput(name, "txt"))) { + out.write(details); } - + } // ----- helpers ------------------------------------------------------------------------------- + /** + * Print a blank line to the terminal. + */ + public static void out() { + out(""); + } + /** * Print the String value of some object to the terminal. */ - public static void out(Object o) - { + public static void out(final Object o) { System.out.println(o); - } + } + + /** + * Print a blank line to the terminal. + */ + @SuppressWarnings("unused") + public static void err() { + err(""); + } /** * Print the String value of some object to the terminal. */ - public static void err(Object o) - { + public static void err(final Object o) { System.err.println(o); - } - + } // ----- inner classes ------------------------------------------------------------------------- @XmlRootElement(name = "ucd") @XmlAccessorType(XmlAccessType.FIELD) - public static class UCDData - { + public static class UCDData { @XmlElement public String description; - @XmlElements({ - @XmlElement(name="char" ), - @XmlElement(name="noncharacter"), - @XmlElement(name="surrogate" ), -// @XmlElement(name="group" ), // note: none present in Unicode 13 data - @XmlElement(name="reserved" ) - }) + @XmlElements({@XmlElement(name = "char"), @XmlElement(name = "noncharacter"), @XmlElement(name = "surrogate"), + // @XmlElement(name="group" ), // note: none present in Unicode 13 data + @XmlElement(name = "reserved")}) @XmlElementWrapper public List repertoire = new ArrayList<>(); @Override - public String toString() - { - StringBuilder sb = new StringBuilder(); - sb.append("UCD description=") - .append(description) - .append(", repertoire=\n"); + public String toString() { + final var sb = new StringBuilder(); + sb.append("UCD description=").append(description).append(", repertoire=\n"); int c = 0; - for (CharData item : repertoire) - { - if (c > 200) - { + for (final var item : repertoire) { + if (c > 200) { sb.append(",\n..."); break; - } - else if (c++ > 0) - { + } else if (c++ > 0) { sb.append(",\n"); - } + } sb.append(item); - } - return sb.toString(); } + return sb.toString(); } + } @XmlAccessorType(XmlAccessType.FIELD) - public static class CharData - { - int firstIndex() - { - return codepoint == null || codepoint.isEmpty() - ? Integer.parseInt(codepointStart, 16) - : Integer.parseInt(codepoint, 16); - } + public static class CharData { + int firstIndex() { + return codepoint == null || codepoint.isEmpty() ? Integer.parseInt(codepointStart, 16) : Integer.parseInt(codepoint, 16); + } - int lastIndex() - { - return codepoint == null || codepoint.isEmpty() - ? Integer.parseInt(codepointEnd, 16) - : Integer.parseInt(codepoint, 16); - } + int lastIndex() { + return codepoint == null || codepoint.isEmpty() ? Integer.parseInt(codepointEnd, 16) : Integer.parseInt(codepoint, 16); + } @XmlAttribute(name = "cp") String codepoint; @@ -376,86 +350,74 @@ int lastIndex() @XmlAttribute(name = "last-cp") String codepointEnd; -// note: names in the XML file don't work the way they do in the Unicode .txt data file format -// String label() -// { -// return name != null && name.length() >= 2 && name.charAt(0) == '<' -// && name.charAt(name.length()-1) == '>' -// ? name.substring(1, name.length()-1) -// : null; -// } + // note: names in the XML file don't work the way they do in the Unicode .txt data file format + // String label() + // { + // return name != null && name.length() >= 2 && name.charAt(0) == '<' + // && name.charAt(name.length()-1) == '>' + // ? name.substring(1, name.length()-1) + // : null; + // } @XmlAttribute(name = "na") String name; - int cat() - { - if (gc == null) - { + int cat() { + if (gc == null) { return 29; - } - - switch (gc) - { - case "Lu": return 0; - case "Ll": return 1; - case "Lt": return 2; - case "Lm": return 3; - case "Lo": return 4; - case "Mn": return 5; - case "Mc": return 6; - case "Me": return 7; - case "Nd": return 8; - case "Nl": return 9; - case "No": return 10; - case "Pc": return 11; - case "Pd": return 12; - case "Ps": return 13; - case "Pe": return 14; - case "Pi": return 15; - case "Pf": return 16; - case "Po": return 17; - case "Sm": return 18; - case "Sc": return 19; - case "Sk": return 20; - case "So": return 21; - case "Zs": return 22; - case "Zl": return 23; - case "Zp": return 24; - case "Cc": return 25; - case "Cf": return 26; - case "Cs": return 27; - case "Co": return 28; - - case "Cn": - case "": - default: return 29; - } } + return switch (gc) { + case "Lu" -> 0; + case "Ll" -> 1; + case "Lt" -> 2; + case "Lm" -> 3; + case "Lo" -> 4; + case "Mn" -> 5; + case "Mc" -> 6; + case "Me" -> 7; + case "Nd" -> 8; + case "Nl" -> 9; + case "No" -> 10; + case "Pc" -> 11; + case "Pd" -> 12; + case "Ps" -> 13; + case "Pe" -> 14; + case "Pi" -> 15; + case "Pf" -> 16; + case "Po" -> 17; + case "Sm" -> 18; + case "Sc" -> 19; + case "Sk" -> 20; + case "So" -> 21; + case "Zs" -> 22; + case "Zl" -> 23; + case "Zp" -> 24; + case "Cc" -> 25; + case "Cf" -> 26; + case "Cs" -> 27; + case "Co" -> 28; + default -> 29; + }; + } + @XmlAttribute(name = "gc") String gc; - int dec() - { - if ("De".equals(nt)) - { + int dec() { + if ("De".equals(nt)) { assert nv != null; assert !nv.isEmpty(); assert !"NaN".equals(nv); return Integer.parseInt(nv); - } + } return 10; // illegal value - } + } - String num() - { - return nt == null || nt.isEmpty() || "None".equals(nt) || - nv == null || nv.isEmpty() || "NaN".equals(nv) - ? null - : nv; - } + String num() { + return nt == null || nt.isEmpty() || "None".equals(nt) || nv == null || nv.isEmpty() || "NaN".equals(nv) ? null : nv; + } @XmlAttribute(name = "nt") String nt; @@ -463,105 +425,69 @@ String num() @XmlAttribute(name = "nv") String nv; - int combo() - { - return ccc == null || ccc.isEmpty() - ? 255 - : Integer.parseInt(ccc); - } + int combo() { + return ccc == null || ccc.isEmpty() ? 255 : Integer.parseInt(ccc); + } @XmlAttribute(name = "ccc") String ccc; - int lower() - { - return slc == null || slc.isEmpty() || "#".equals(slc) - ? 0 - : Integer.parseInt(slc, 16); - } + int lower() { + return slc == null || slc.isEmpty() || "#".equals(slc) ? 0 : Integer.parseInt(slc, 16); + } @XmlAttribute(name = "slc") String slc; - int upper() - { - return suc == null || suc.isEmpty() || "#".equals(suc) - ? 0 - : Integer.parseInt(suc, 16); - } + int upper() { + return suc == null || suc.isEmpty() || "#".equals(suc) ? 0 : Integer.parseInt(suc, 16); + } @XmlAttribute(name = "suc") String suc; - int title() - { - return stc == null || stc.isEmpty() || "#".equals(stc) - ? 0 - : Integer.parseInt(stc, 16); - } + int title() { + return stc == null || stc.isEmpty() || "#".equals(stc) ? 0 : Integer.parseInt(stc, 16); + } @XmlAttribute(name = "stc") String stc; - String block() - { - return blk == null || blk.isEmpty() - ? null - : blk; - } + String block() { + return blk == null || blk.isEmpty() ? null : blk; + } @XmlAttribute(name = "blk") String blk; -// @XmlAttribute(name = "bc") -// String bidiClass; -// -// @XmlAttribute(name = "Bidi_M") -// String bidiMirrored; -// -// @XmlAttribute(name = "bmg") -// String bidiMirrorImage; -// -// @XmlAttribute(name = "Bidi_C") -// String bidiControl; -// -// @XmlAttribute(name = "bpt") -// String bidiPairedBracketType; -// -// @XmlAttribute(name = "bpb") -// String bidiPairedBracket; + // @XmlAttribute(name = "bc") + // String bidiClass; + // + // @XmlAttribute(name = "Bidi_M") + // String bidiMirrored; + // + // @XmlAttribute(name = "bmg") + // String bidiMirrorImage; + // + // @XmlAttribute(name = "Bidi_C") + // String bidiControl; + // + // @XmlAttribute(name = "bpt") + // String bidiPairedBracketType; + // + // @XmlAttribute(name = "bpb") + // String bidiPairedBracket; @Override - public String toString() - { - return getClass().getSimpleName().toLowerCase() - + " codepoint=" + (codepoint == null || codepoint.isEmpty() - ? codepointStart + ".." + codepointEnd - : codepoint) - + (name != null && !name.isEmpty() ? ", name=\"" + name + "\"" : "") - + ", gen-cat=" + gc - + (blk != null && !blk.isEmpty() ? ", block=\"" + blk + "\"" : "") - + (nt != null && !nt.isEmpty() && !"None".equals(nt) ? ", num-type=\"" + nt + "\"" : "") - + ( - nv != null && !nv.isEmpty() && !"NaN".equals(nv) ? ", num-val=\"" + nv + "\"" : "") - + (suc == null || suc.isEmpty() - || "#".equals(suc) ? "" : ", suc=" + suc) - + (slc == null || slc.isEmpty() - || "#".equals(slc) ? "" : ", slc=" + slc) - + (stc == null || stc.isEmpty() - || "#".equals(stc) ? "" : ", stc=" + stc) -// + (bidiClass != null && bidiClass.length() > 0 ? ", bidiClass=\"" + bidiClass + "\"" : "") -// + (bidiMirrored != null && bidiMirrored.equals("Y") ? ", bidiMirrored=\"" + bidiMirrored + "\"" : "") -// + (bidiMirrorImage != null && bidiMirrorImage.length() > 0 ? ", bidiMirrorImage=\"" + bidiMirrorImage + "\"" : "") -// + (bidiControl != null && bidiControl.equals("Y") ? ", bidiControl=\"" + bidiControl + "\"" : "") -// + (bidiPairedBracketType != null && bidiPairedBracketType.length() > 0 ? ", bidiPairedBracketType=\"" + bidiPairedBracketType + "\"" : "") -// + (bidiPairedBracket != null && bidiPairedBracket.length() > 0 ? ", bidiPairedBracket=\"" + bidiPairedBracket + "\"" : "") + public String toString() { + return getClass().getSimpleName().toLowerCase() + " codepoint=" + (codepoint == null || codepoint.isEmpty() ? codepointStart + ".." + codepointEnd : codepoint) + (name != null && !name.isEmpty() ? ", name=\"" + name + "\"" : "") + ", gen-cat=" + gc + (blk != null && !blk.isEmpty() ? ", block=\"" + blk + "\"" : "") + (nt != null && !nt.isEmpty() && !"None".equals(nt) ? ", num-type=\"" + nt + "\"" : "") + (nv != null && !nv.isEmpty() && !"NaN".equals(nv) ? ", num-val=\"" + nv + "\"" : "") + (suc == null || suc.isEmpty() || "#".equals(suc) ? "" : ", suc=" + suc) + (slc == null || slc.isEmpty() || "#".equals(slc) ? "" : ", slc=" + slc) + (stc == null || stc.isEmpty() || "#".equals(stc) ? "" : ", stc=" + stc) + // + (bidiClass != null && bidiClass.length() > 0 ? ", bidiClass=\"" + bidiClass + "\"" : "") + // + (bidiMirrored != null && bidiMirrored.equals("Y") ? ", bidiMirrored=\"" + bidiMirrored + "\"" : "") + // + (bidiMirrorImage != null && bidiMirrorImage.length() > 0 ? ", bidiMirrorImage=\"" + bidiMirrorImage + "\"" : "") + // + (bidiControl != null && bidiControl.equals("Y") ? ", bidiControl=\"" + bidiControl + "\"" : "") + // + (bidiPairedBracketType != null && bidiPairedBracketType.length() > 0 ? ", bidiPairedBracketType=\"" + bidiPairedBracketType + "\"" : "") + // + (bidiPairedBracket != null && bidiPairedBracket.length() > 0 ? ", bidiPairedBracket=\"" + bidiPairedBracket + "\"" : "") ; - } } - - - // ----- fields -------------------------------------------------------------------------------- - - public static final boolean TEST = false; - } \ No newline at end of file + } +} \ No newline at end of file diff --git a/javatools_utils/build.gradle.kts b/javatools_utils/build.gradle.kts index 10230707d3..f354247a5b 100644 --- a/javatools_utils/build.gradle.kts +++ b/javatools_utils/build.gradle.kts @@ -3,40 +3,16 @@ */ plugins { - java + alias(libs.plugins.xdk.build.java) + alias(libs.plugins.tasktree) } -java { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 -} - -testing { - suites { - val test by getting(JvmTestSuite::class) { - useJUnitJupiter() - } - } -} - -tasks { - jar { - val version = rootProject.version - - manifest { - attributes["Manifest-Version"] = "1.0" - attributes["Sealed"] = "true" - attributes["Name"] = "/org/xvm/util" - attributes["Specification-Title"] = "xvm" - attributes["Specification-Version"] = version - attributes["Specification-Vendor"] = "xtclang.org" - attributes["Implementation-Title"] = "xvm-javatools_utils" - attributes["Implementation-Version"] = version - attributes["Implementation-Vendor"] = "xtclang.org" - } - } - - test { - maxHeapSize = "1G" - } +val xdkJavaToolsUtilsProvider by configurations.registering { + description = "Provider configuration of the XVM javatools_utils classes." + isCanBeResolved = false + isCanBeConsumed = true +/* attributes { + attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.LIBRARY)) + attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements.CLASSES)) + }*/ } diff --git a/javatools_utils/javatools-utils.properties b/javatools_utils/javatools-utils.properties new file mode 100644 index 0000000000..ec81e06876 --- /dev/null +++ b/javatools_utils/javatools-utils.properties @@ -0,0 +1,2 @@ +# TODO: TEMPORARILY DISABLE JAVAC LINTING / WARNINGS FOR THIS PROJECT, BECAUASE IT *SPEWS* THEM. +org.xtclang.java.lint=false diff --git a/javatools_utils/settings.gradle.kts b/javatools_utils/settings.gradle.kts new file mode 100644 index 0000000000..d252adad76 --- /dev/null +++ b/javatools_utils/settings.gradle.kts @@ -0,0 +1,10 @@ +pluginManagement { + includeBuild("../build-logic/settings-plugins") + includeBuild("../build-logic/common-plugins") +} + +plugins { + id("org.xtclang.build.common") +} + +rootProject.name = "javatools-utils" diff --git a/javatools_utils/src/main/java/org/xvm/util/Handy.java b/javatools_utils/src/main/java/org/xvm/util/Handy.java index 43e466fc47..58779bf818 100755 --- a/javatools_utils/src/main/java/org/xvm/util/Handy.java +++ b/javatools_utils/src/main/java/org/xvm/util/Handy.java @@ -306,9 +306,7 @@ public static byte[] toByteArray(long l) * * @param l the long value * @param ab the byte array to copy into - * @param ab the byte array offset to write the long value at - * - * @return the byte array + * @param of the byte array offset to write the long value at */ public static void toByteArray(long l, byte[] ab, int of) { @@ -809,6 +807,7 @@ public static String quotedString(String s) * * @return date/time string in format "YYYY-MM-DD HH:MM:SS" format */ + @SuppressWarnings("unused") public static String dateString(long cMillis) { Date date = new Date(cMillis); @@ -1595,6 +1594,7 @@ public static char[] readFileChars(File file) * @throws IOException indicates a failure to read the character contents * of the specified file */ + @SuppressWarnings("fallthrough") public static char[] readFileChars(File file, String sEncoding) throws IOException { diff --git a/javatools_utils/src/test/java/org/xvm/util/HandyTest.java b/javatools_utils/src/test/java/org/xvm/util/HandyTest.java index f63be1a872..aa5f289e18 100644 --- a/javatools_utils/src/test/java/org/xvm/util/HandyTest.java +++ b/javatools_utils/src/test/java/org/xvm/util/HandyTest.java @@ -520,4 +520,4 @@ public void write(byte[] b, int off, int len) } }); } - } \ No newline at end of file + } diff --git a/lib_aggregate/build.gradle.kts b/lib_aggregate/build.gradle.kts new file mode 100644 index 0000000000..1873e6feef --- /dev/null +++ b/lib_aggregate/build.gradle.kts @@ -0,0 +1,9 @@ +plugins { + id("org.xtclang.build.xdk.versioning") + alias(libs.plugins.xtc) +} + +dependencies { + xdkJavaTools(libs.javatools) + xtcModule(libs.xdk.ecstasy) +} diff --git a/lib_collections/build.gradle.kts b/lib_collections/build.gradle.kts new file mode 100644 index 0000000000..1873e6feef --- /dev/null +++ b/lib_collections/build.gradle.kts @@ -0,0 +1,9 @@ +plugins { + id("org.xtclang.build.xdk.versioning") + alias(libs.plugins.xtc) +} + +dependencies { + xdkJavaTools(libs.javatools) + xtcModule(libs.xdk.ecstasy) +} diff --git a/lib_crypto/build.gradle.kts b/lib_crypto/build.gradle.kts new file mode 100644 index 0000000000..1873e6feef --- /dev/null +++ b/lib_crypto/build.gradle.kts @@ -0,0 +1,9 @@ +plugins { + id("org.xtclang.build.xdk.versioning") + alias(libs.plugins.xtc) +} + +dependencies { + xdkJavaTools(libs.javatools) + xtcModule(libs.xdk.ecstasy) +} diff --git a/lib_ecstasy/build.gradle.kts b/lib_ecstasy/build.gradle.kts index a66035e759..008e290e13 100644 --- a/lib_ecstasy/build.gradle.kts +++ b/lib_ecstasy/build.gradle.kts @@ -1,46 +1,59 @@ +import XdkBuildLogic.Companion.XDK_ARTIFACT_NAME_MACK_DIR +import org.gradle.api.attributes.Category.CATEGORY_ATTRIBUTE +import org.gradle.api.attributes.Category.LIBRARY +import org.gradle.api.attributes.LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE +import org.xtclang.plugin.tasks.XtcCompileTask + /* * Build file for the Ecstasy core library of the XDK. * - * This project does NOT build the ecstasy.xtc file. (The :xdk project builds it.) - * - * This project can update the Unicode data files, if a Unicode release has occurred and provided - * a new `ucd.all.flat.zip`; that is the only time that the Unicode data files have to be updated. + * This project builds the ecstasy.xtc anb mack.xtc core library files. */ -project.ext.set("implicit.x", "${projectDir}/src/main/resources/implicit.x") - -tasks.register("clean") { - group = "Build" - description = "Delete previous build results" - // the Ecstasy module project does not build anything itself, so there is nothing to clean +plugins { + id("org.xtclang.build.xdk.versioning") + alias(libs.plugins.xtc) } -tasks.register("importUnicodeFiles") { - group = "Build" - description = "Copy the various Unicode data files from :javatools_unicode to :lib_ecstasy project." - from(file("${project(":javatools_unicode").buildDir}/resources/")) - include("Char*.txt", "Char*.dat") - into(file("src/main/resources/text/")) - doLast { - println("Finished task: importUnicodeFiles") +val xdkTurtle by configurations.registering { + isCanBeResolved = true + isCanBeConsumed = false + attributes { + attribute(CATEGORY_ATTRIBUTE, objects.named(LIBRARY)) + attribute(LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(XDK_ARTIFACT_NAME_MACK_DIR)) } } -tasks.register("rebuildUnicodeFiles") { - group = "Build" - description = "Force the rebuild of the `./src/main/resources/text` data files by running the :javatools_unicode project and copying the results." - val make = project(":javatools_unicode").tasks["run"] - val copy = tasks["importUnicodeFiles"] - dependsOn(make) - dependsOn(copy) - copy.mustRunAfter(make) - doLast { - println("Finished task: rebuildUnicodeFiles") - } +dependencies { + // TODO: Find out why xdkJavaTools is not an unstable API, while xdkTurtle and xdkUnicode are. + xdkJavaTools(libs.javatools) + @Suppress("UnstableApiUsage") + xdkTurtle(libs.javatools.turtle) // A dependency declaration like this works equally well if we are working with an included build/project or with an artifact. This is exactly what we want. } -tasks.register("build") { - group = "Build" - description = "Build this project" - // the Ecstasy module project does not build anything itself +val compileXtc by tasks.existing(XtcCompileTask::class) { + outputFilename("mack.xtc" to "javatools_turtle.xtc") } + +/** + * Set up source sets. The XTC main source set needs the turtle module as part of the compile, i.e. "mack.x", as it + * cannot build standalone, for bootstrapping reasons. It would really just be simpler to move mack.x to live beside + * ecstasy.x, but right now we want to transition to the Gradle build logic without changing semantics form the old + * world. This shows the flexibility of being Source Set aware, through. + */ +sourceSets { + main { + xtc { + // mack.x is in a different project, and does not build on its own, hence we add it to the lib_ecstasy source set instead. + srcDir(xdkTurtle) + } + //resources { + // Skip the local unicode files if we are in "rebuild unicode" mode. + //if (xdkBuild.rebuildUnicode()) { + // exclude("**/ecstasy/text**") + //} + //} + } +} + +// TODO Add resource processing for unicode \ No newline at end of file diff --git a/lib_json/build.gradle.kts b/lib_json/build.gradle.kts new file mode 100644 index 0000000000..1873e6feef --- /dev/null +++ b/lib_json/build.gradle.kts @@ -0,0 +1,9 @@ +plugins { + id("org.xtclang.build.xdk.versioning") + alias(libs.plugins.xtc) +} + +dependencies { + xdkJavaTools(libs.javatools) + xtcModule(libs.xdk.ecstasy) +} diff --git a/lib_jsondb/build.gradle.kts b/lib_jsondb/build.gradle.kts new file mode 100644 index 0000000000..ed1eb8a75d --- /dev/null +++ b/lib_jsondb/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { + id("org.xtclang.build.xdk.versioning") + alias(libs.plugins.xtc) +} + +dependencies { + xdkJavaTools(libs.javatools) + xtcModule(libs.xdk.ecstasy) + xtcModule(libs.xdk.aggregate) + xtcModule(libs.xdk.collections) + xtcModule(libs.xdk.json) + xtcModule(libs.xdk.oodb) +} diff --git a/lib_net/build.gradle.kts b/lib_net/build.gradle.kts new file mode 100644 index 0000000000..e7c43a9068 --- /dev/null +++ b/lib_net/build.gradle.kts @@ -0,0 +1,10 @@ +plugins { + id("org.xtclang.build.xdk.versioning") + alias(libs.plugins.xtc) +} + +dependencies { + xdkJavaTools(libs.javatools) + xtcModule(libs.xdk.ecstasy) + xtcModule(libs.xdk.crypto) +} diff --git a/lib_oodb/build.gradle.kts b/lib_oodb/build.gradle.kts new file mode 100644 index 0000000000..1873e6feef --- /dev/null +++ b/lib_oodb/build.gradle.kts @@ -0,0 +1,9 @@ +plugins { + id("org.xtclang.build.xdk.versioning") + alias(libs.plugins.xtc) +} + +dependencies { + xdkJavaTools(libs.javatools) + xtcModule(libs.xdk.ecstasy) +} diff --git a/lib_web/build.gradle.kts b/lib_web/build.gradle.kts new file mode 100644 index 0000000000..b62f8a9097 --- /dev/null +++ b/lib_web/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + id("org.xtclang.build.xdk.versioning") + alias(libs.plugins.xtc) +} + +dependencies { + xdkJavaTools(libs.javatools) + xtcModule(libs.xdk.ecstasy) + xtcModule(libs.xdk.aggregate) + xtcModule(libs.xdk.collections) + xtcModule(libs.xdk.crypto) + xtcModule(libs.xdk.json) + xtcModule(libs.xdk.net) +} + diff --git a/lib_webauth/build.gradle.kts b/lib_webauth/build.gradle.kts new file mode 100644 index 0000000000..e6c165e4b9 --- /dev/null +++ b/lib_webauth/build.gradle.kts @@ -0,0 +1,16 @@ +plugins { + id("org.xtclang.build.xdk.versioning") + alias(libs.plugins.xtc) +} + +dependencies { + xdkJavaTools(libs.javatools) + xtcModule(libs.xdk.ecstasy) + xtcModule(libs.xdk.aggregate) + xtcModule(libs.xdk.collections) + xtcModule(libs.xdk.crypto) + xtcModule(libs.xdk.json) + xtcModule(libs.xdk.net) + xtcModule(libs.xdk.oodb) + xtcModule(libs.xdk.web) +} diff --git a/lib_xenia/build.gradle.kts b/lib_xenia/build.gradle.kts new file mode 100644 index 0000000000..95b6910871 --- /dev/null +++ b/lib_xenia/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + id("org.xtclang.build.xdk.versioning") + alias(libs.plugins.xtc) +} + +dependencies { + xdkJavaTools(libs.javatools) + xtcModule(libs.xdk.ecstasy) + xtcModule(libs.xdk.aggregate) + xtcModule(libs.xdk.collections) + xtcModule(libs.xdk.crypto) + xtcModule(libs.xdk.json) + xtcModule(libs.xdk.net) + xtcModule(libs.xdk.web) +} diff --git a/manualTests/build.gradle.kts b/manualTests/build.gradle.kts index efdb291534..c585b38c85 100644 --- a/manualTests/build.gradle.kts +++ b/manualTests/build.gradle.kts @@ -1,154 +1,200 @@ +import org.xtclang.plugin.tasks.XtcCompileTask + /* - * Test utilities. + * Test utilities. This is a standalone XTC project, which should only depend on the XDK. + * If we want to use it to debug the XDK, that is also fine, as it will do dependency + * substitution on the XDK and XTC Plugin (and Javatools and other dependencies) correctly, + * through included builds, anyway. + * + * This is compiled as part of the XDK build, in order to ensure that the build DSL work as + * expected, and that we can resolve modules only with external dependencies to repository + * artifacts for the XTC Gradle plugin and the XDK. */ -val xdk = project(":xdk"); -val javatools = project(":javatools") -val javatoolsJar = "${javatools.buildDir}/libs/javatools.jar" - -val tests = listOf( - "src/main/x/annos.x", - "src/main/x/array.x", - "src/main/x/collections.x", - "src/main/x/defasn.x", - "src/main/x/exceptions.x", - "src/main/x/generics.x", - "src/main/x/innerOuter.x", - "src/main/x/files.x", - "src/main/x/IO.x", - "src/main/x/lambda.x", - "src/main/x/literals.x", - "src/main/x/loop.x", - "src/main/x/nesting.x", - "src/main/x/numbers.x", - "src/main/x/prop.x", - "src/main/x/maps.x", - "src/main/x/misc.x", - "src/main/x/queues.x", - "src/main/x/services.x", - "src/main/x/reflect.x", - "src/main/x/regex.x", - "src/main/x/tuple.x") - -val testModules = listOf( - "TestAnnotations", - "TestArray", - "TestCollections", - "TestDefAsn", - "TestTry", - "TestGenerics", - "TestInnerOuter", - "TestFiles", - "TestIO", - "TestLambda", - "TestLiterals", - "TestLoops", - "TestNesting", - "TestNumbers", - "TestProps", - "TestMaps", - "TestMisc", - "TestQueues", - "TestServices", - "TestReflection", - "TestRegularExpressions", - "TestTuples") - -tasks.register("clean") { - group = "Build" - description = "Delete previous build results" - delete("$buildDir") +plugins { + alias(libs.plugins.xdk.build.versioning) + alias(libs.plugins.xtc) } -val compileAll = tasks.register("compileAll") { - group = "Build" - description = "Compile all tests" - - dependsOn(xdk.tasks["build"]) - - classpath(javatoolsJar) +val sanityCheckRuntime = getXdkPropertyBoolean("org.xtclang.build.sanityCheckRuntime", false) - val opts = listOf( - "-o", "$buildDir", - "-L", "${xdk.buildDir}/xdk/lib", - "-L", "${xdk.buildDir}/xdk/javatools/javatools_turtle.xtc", - "-L", "${xdk.buildDir}/xdk/javatools/javatools_bridge.xtc") - - args(opts + tests + "src/main/x/runner.x") - mainClass.set("org.xvm.tool.Compiler") +dependencies { + xdk(libs.xdk) } -tasks.register("runAll") { - group = "Test" - description = "Run all tests" - - dependsOn(xdk.tasks["build"]) - - // the first two paths contain classes that are present in the javatoolsJar, - // but gradle's classpath() doesn't allow combining a jar with a regular path - classpath( - "${javatools.buildDir}/classes/java/main", - "${javatools.buildDir}/resources/main", - "${javatools.buildDir}/classes/java/test") - - args(tests) - mainClass.set("org.xvm.runtime.TestConnector") +/** + * This configured a source set, which makes the compiler build all of the included modules. + * There are several negative "should fail" source files in this subproject as well. but + * these are filtered out through the standard Gradle source set mechanism. This repo + * is not really meant to be used as a unit test. It merely sits on top of everything to + * ensure that we don't accidentally break external dependencies to the XDK artifacts + * for the world outside the XDK repo, and that the build lifecycle works as it should, + * and we don't push any broken changes to XTC language support, that won't be discovered + * until several commits later, or worse, by a third party XTC developer, who has no + * interest in building their own XDK internals or modifying the plugin. + */ +sourceSets { + main { + xtc { + include( + "**/annos.x", + "**/array.x", + "**/collections.x", + "**/defasn.x", + "**/exceptions.x", + "**/FizzBuzz.x", + "**/generics.x", + "**/innerOuter.x", + "**/files.x", + "**/IO.x", + "**/lambda.x", + "**/literals.x", + "**/loop.x", + "**/nesting.x", + "**/numbers.x", + "**/prop.x", + "**/maps.x", + "**/misc.x", + "**/queues.x", + "**/services.x", + "**/reflect.x", + "**/regex.x", + "**/tuple.x" + ) + } + } } -tasks.register("runAllParallel") { - group = "Test" - description = "Run all tests" - - dependsOn(xdk.tasks["build"], compileAll) - - classpath(javatoolsJar) - - val opts = listOf( - "-L", "${xdk.buildDir}/xdk/lib", - "-L", "${xdk.buildDir}/xdk/javatools/javatools_turtle.xtc", - "-L", "${xdk.buildDir}/xdk/javatools/javatools_bridge.xtc", - "-L", "$buildDir", - "Runner") - - args(opts + testModules) - mainClass.set("org.xvm.tool.Runner") +/** + * It's important to understand what causes caching problems and unncessary rebuild in Gradle. + * One of these things is tasks dependning on System environment variables. + * To fix that particular issue, an input of the form inputs.property("langEnvironment") { System.getenv(ENV_VAR) } + * needs to be added to the task configuration or any plugin that uses is must be aware of it. + *

+ * Also Using doFirst and doLast from a build script on a cacheable task ties you to build script changes since + * the implementation of the closure comes from the build script. If possible, you should use separate tasks instead. + *

+ * @see Gradle User Guide on caching + */ +fun alwaysRebuild(): Boolean { + val rebuild = (System.getenv("ORG_XTCLANG_BUILD_SANITY_CHECK_RUNTIME_FORCE_REBUILD") ?: "false").toBoolean() + if (rebuild) { + logger.warn("$prefix manualTest compile configuration is set to force rebuild (forceRebuild: true)") + } + return rebuild } -val compileOne = tasks.register("compileOne") { - description = "Compile a \"testName\" test" - - dependsOn(xdk.tasks["build"]) - - val name = if (project.hasProperty("testName")) project.property("testName") else "TestSimple" - - classpath(javatoolsJar) - - args("-o", "$buildDir", - "-L", "${xdk.buildDir}/xdk/lib", - "-L", "${xdk.buildDir}/xdk/javatools/javatools_turtle.xtc", - "-L", "${xdk.buildDir}/xdk/javatools/javatools_bridge.xtc", - "-L", "$buildDir", - "src/main/x/$name.x") - mainClass.set("org.xvm.tool.Compiler") +xtcCompile { + /* + * Displays XTC runtime version (should be semanticVersion of this XDK), default is "false" + */ + showVersion = false + + /* + * Run the XTC command in its built-in verbose mode (default: false). + */ + verbose = false + + /* + * Compile in build process thread. Enables seamless IDE debugging in the Gradle build, with breakpoints + * in e.g. Javatools classes, but is brittle, and should not be used for production use, for example + * if the launched app does System.exit, this will kill the build job too. + * + * Javatools launchers should be debuggable through a standard Run/Debug Configuration (for example in IntelliJ) + * where the Javatools project is added as a Java Application (and not a Gradle job). + * + * Default is true. + */ + fork = true + + /* + * Should all compilations be forced to rerun every time this build is performed? This is NOT recommended, + * as it removes pretty much every advantage that Gradle with dynamic dependency management gives you. It + * should be used only for testing purposes, and never for anything else, in a typical build, distribution + * generation or execution of an XTC app. + */ + forceRebuild = alwaysRebuild() + + /* + * By default, a Gradle task swallows stdin, but it's possible to override standard input and + * output for any XTC launcher task. + * + * To redirect any I/O stream, for example if you want to input data to the XTC debugger or + * to the console, such as credentials/interactive prompts, the error, output and input streams + * can be redirected to a custom source. + * + * This should at least enable the "ugly" use case of breaking into the debugger on the + * console when an "assert:debug" statement is evaluated. + */ + // stdin = System.`in` } -tasks.register("runOne") { - group = "Test" - description = "Run a \"testName\" test" - - dependsOn(xdk.tasks["build"], compileOne) +xtcRun { + /* + * Equivalent to the "--version" flag for the launcher (default: false). + */ + showVersion = false - val name = if (project.hasProperty("testName")) project.property("testName") else "TestSimple" - - classpath(javatoolsJar) - - val opts = listOf( - "-v", - "-L", "${xdk.buildDir}/xdk/lib", - "-L", "${xdk.buildDir}/xdk/javatools/javatools_turtle.xtc", - "-L", "${xdk.buildDir}/xdk/javatools/javatools_bridge.xtc", - "-L", "$buildDir") + /* + * Run the XTC command in its built-in verbose mode (default: false). + */ + verbose = true + + /* + * If fork is "true", the runner will run in the build process thread. Enables seamless IDE debugging in the Gradle build, with breakpoints + * in e.g. Javatools classes, but is brittle, and should not be used for production use, for example + * if the launched app does System.exit, this will kill the build job too. + * + * Javatools lanchers should be debuggable through a standard Run/Debug Configuration (for example in IntelliJ) + * where the Javatools project is added as a Java Application (and not a Gradle job). + * + * Default is true. + */ + fork = true + + /* + * Use an XTC native launcher (requires a local XDK installation on the test machine.) + * The default is "false". + */ + useNativeLauncher = false + + /* + * Add a JVM argument to the defaults. Will be ignored if the launch does not spawn a forked JVM for its run. + */ + jvmArgs("-showversion") + + /* + * Execute TestFizzBuzz with the Hello World arguments. We support providers, as well + * as Strings, as per the common Gradle API conventions. For example, you can do + * moduleArgs() or moduleArg() for + * lazy evaluation, too. + * + * Currently, all module configurations in the xtcRun DSL will be executed in sequence, + * as their order declared. + * + * We suspect that the pre-generated run tasks from the XTC Plugin are not the most optimal + * and intuitive way of running XTC modules. It's probably cleaner for the user to modify + * the configuration on task level for any runtime task, and add many simple custom run tasks + * for clarity. However, we haven't ahd the cycles to support the standard overrides for + * modules to run (all other DSL can be overridden) at task level. It is easy and encouraged + * to contribute to the XTC Plugin build DSL, so that we get more functionality, clearer + * and shorter syntax, and new features that we feel we require. It's not clear what all + * of these are, but working with an XTC project that applies the XTC Plugin will likely + * discover most of the shortcomings quickly, so that we can file enhancement requests. + * + * TODO: Add parallelism, and a simpler way to work with this. + * TODO: Add a nicer DSL syntax with a nested modules section. + */ + module { + moduleName = "TestFizzBuzz" // Will add other ways to resolve modules too. + showVersion = true // Overrides env showVersion flag. + moduleArgs("Hello, ", "World!") + } +} - args(opts + "src/main/x/$name.x") - mainClass.set("org.xvm.tool.Runner") -} \ No newline at end of file +tasks.withType().configureEach { + enabled = sanityCheckRuntime + doLast { + logger.lifecycle("$prefix *** RECOMPILING: $name") + } +} diff --git a/manualTests/settings.gradle.kts b/manualTests/settings.gradle.kts new file mode 100644 index 0000000000..b9769c7c50 --- /dev/null +++ b/manualTests/settings.gradle.kts @@ -0,0 +1,13 @@ +pluginManagement { + includeBuild("../build-logic/settings-plugins") + includeBuild("../build-logic/common-plugins") + includeBuild("../plugin") +} + +plugins { + id("org.xtclang.build.common") +} + +includeBuild("../xdk") + +rootProject.name = "manualTests" diff --git a/manualTests/src/main/x/FizzBuzz.x b/manualTests/src/main/x/FizzBuzz.x new file mode 100644 index 0000000000..94b461bce0 --- /dev/null +++ b/manualTests/src/main/x/FizzBuzz.x @@ -0,0 +1,25 @@ +// TODO: Add unit tests for both broken and working code/runtime modules. +// TODO: Add a "test debugger" task to manualTests with the assert:debug enabled, or something. +module TestFizzBuzz { + void run(String[] args = []) { + @Inject Console console; + + loop: for (String arg : args) { + console.print($"TestFizzBuzzArgument: (args[{loop.count}] = {arg})"); + } + + //assert:debug; + + for (Int x : 1..100) { + console.print( + switch (x % 3, x % 5) { + case (0, 0): "FizzBuzz"; + case (0, _): "Fizz"; + case (_, 0): "Buzz"; + case (_, _): x.toString(); + }, True); + console.print(" ", True); + } + console.print(); + } +} diff --git a/plugin/build.gradle.kts b/plugin/build.gradle.kts new file mode 100644 index 0000000000..861bf248e4 --- /dev/null +++ b/plugin/build.gradle.kts @@ -0,0 +1,131 @@ +import XdkBuildLogic.Companion.XDK_ARTIFACT_NAME_JAVATOOLS_FATJAR + +plugins { + alias(libs.plugins.xdk.build.java) + alias(libs.plugins.xdk.build.publish) + alias(libs.plugins.gradle.portal.publish) + alias(libs.plugins.tasktree) +} + +val semanticVersion: SemanticVersion by extra + +private val pprefix = "org.xtclang" + +// Property for the Plugin ID (unique to a plugin) +private val pluginId = getXdkProperty("$pprefix.plugin.id") + +// Properties for the artifact +private val pluginName = project.name +private val pluginGroup = getXdkProperty("$pprefix.plugin.group", group.toString()) +private val pluginVersion = getXdkProperty("$pprefix.plugin.version", version.toString()) + +logger.info("$prefix Plugin (id: '$pluginId') artifact version identifier: '$pluginGroup:$pluginName:$pluginVersion'") + +private val shouldBundleJavaTools = getXdkPropertyBoolean("$pprefix.plugin.bundle.javatools") + +private val javaToolsContents = project.objects.fileCollection() + +val xdkJavaToolsJarConsumer by configurations.registering { + isCanBeResolved = true + isCanBeConsumed = false + attributes { + attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.LIBRARY)) + attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(XDK_ARTIFACT_NAME_JAVATOOLS_FATJAR)) + // attribute(CATEGORY_ATTRIBUTE, objects.named(Category.LIBRARY)) + // attribute(LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(XDK_ARTIFACT_NAME_JAVATOOLS_FATJAR)) + // attribute(Category.CATEGORY_ATTRIBUTE, objects.named(JAVA_RUNTIME)) + // attribute(LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(XDK_ARTIFACT_NAME_JAVATOOLS_FATJAR)) + } +} + +if (shouldBundleJavaTools) { + dependencies { + @Suppress("UnstableApiUsage") xdkJavaToolsJarConsumer(libs.javatools) + } +} + +publishing { + publications { + val xtcPlugin by registering(MavenPublication::class) { + groupId = pluginGroup + artifactId = pluginName + version = pluginVersion + artifact(tasks.jar) // we have two more jar artifacts with "javadoc" and "source" classifiers, respectively. Tell Gradle we do NOT want those to be part of the publication (i.e. don't use from(components["java"]) // TODO: Do not publish source or javadoc + logger.info("$prefix Publication '$groupId:$artifactId:$version' (name: '$name') configured.") + } + } +} + +// TODO: For pure maven plugin artifacts, we can also use "de.benediktritter.maven-plugin-development, mavenPlugin { }" +@Suppress("UnstableApiUsage") +gradlePlugin { + // The built-in pluginMaven publication can be disabled with "isAutomatedPublishing=false" + // However, this results in the Gradle version (with Gradle specific metadata) of the plugin not + // being published. To read it from at least a local repo, we need that artifact too, hence we + // get three artifacts. + isAutomatedPublishing = getXdkPropertyBoolean("$pprefix.plugin.isAutomatedPublishing", true) + + logger.info("$prefix Configuring Gradle Plugin; isAutomatedPublishing=$isAutomatedPublishing") + + vcsUrl = getXdkProperty("$pprefix.plugin.vcs.url") + website = getXdkProperty("$pprefix.plugin.website") + + plugins { + val xtc by registering { + version = pluginVersion + id = pluginId + implementationClass = getXdkProperty("$pprefix.plugin.implementation.class") + displayName = getXdkProperty("$pprefix.plugin.display.name") + description = getXdkProperty("$pprefix.plugin.description") + logger.info("$prefix Configuring gradlePlugin; pluginId=$pluginId, implementationClass=$implementationClass, displayName=$displayName, description=$description") + tags = listOfNotNull("xtc", "language", "ecstasy", "xdk") + } + } +} + +tasks.withType().configureEach { + enabled = false + // TODO: Write JavaDocs for plugin. + logger.info("$prefix Note: JavaDoc task is currently disabled, but certain publication methods, such as for the Gradle plugin portal will still generate and publish JavaDocs.") +} + +tasks.withType().matching { it.name.startsWith("publishPluginMaven") }.configureEach { + enabled = false + // TODO: Reuse the existing PluginMaven task instead, because that is the one gradlePluginPortal hardcodes. + logger.info("$prefix Disabled default publication task: '$name'. The task '${name.replace("PluginMaven", "XtcPlugin")}' should be equivalent." + ) +} + +tasks.withType().configureEach { + if (name == "jar") { + if (shouldBundleJavaTools) { + /* + * It's important that this is a provider/lazy, because xdkJavaToolsJarConsumer kickstarts an + * entire javatools fatjar build when you resolve it, and that's what we have to do if we want + * it in the plugin, even though we are just configuring here. We will lift out the manualTests + * "use the plugin as an external party" test from the build source line to make sure dependencies + * are preserved correctly, and also add a dry-run/vs real diff test to see that our build caching + * does not break. + */ + // TODO with the right categories we could just instead of grabbing the JAR ask for the classes as outgoing config for javatools + inputs.files(xdkJavaToolsJarConsumer) + val jarFiles = { zipTree(xdkJavaToolsJarConsumer.get().singleFile) } + from(jarFiles) + } + manifest { + attributes( + "Manifest-Version" to "1.0", + "Xdk-Version" to semanticVersion.toString(), + "Main-Class" to "$pprefix.plugin.Usage", + "Name" to "/org/xtclang/plugin/", + "Sealed" to "true", + "Specification-Title" to "XTC Gradle and Maven Plugin", + "Specification-Vendor" to "xtclang.org", + "Specification-Version" to pluginVersion, + "Implementation-Title" to "xtc-plugin", + "Implementation-Vendor" to "xtclang.org", + "Implementation-Version" to pluginVersion, + ) + } + } +} diff --git a/plugin/settings.gradle.kts b/plugin/settings.gradle.kts new file mode 100644 index 0000000000..6f24c0ebbf --- /dev/null +++ b/plugin/settings.gradle.kts @@ -0,0 +1,10 @@ +pluginManagement { + includeBuild("../build-logic/settings-plugins") + includeBuild("../build-logic/common-plugins") +} + +plugins { + id("org.xtclang.build.common") +} + +rootProject.name = "xtc-plugin" diff --git a/plugin/src/main/java/org/xtclang/plugin/ProjectDelegate.java b/plugin/src/main/java/org/xtclang/plugin/ProjectDelegate.java new file mode 100644 index 0000000000..ce9f8e3f0a --- /dev/null +++ b/plugin/src/main/java/org/xtclang/plugin/ProjectDelegate.java @@ -0,0 +1,212 @@ +package org.xtclang.plugin; + +import org.gradle.StartParameter; +import org.gradle.api.Project; +import org.gradle.api.Task; +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.artifacts.ConfigurationContainer; +import org.gradle.api.artifacts.VersionCatalogsExtension; +import org.gradle.api.component.AdhocComponentWithVariants; +import org.gradle.api.file.DirectoryProperty; +import org.gradle.api.file.FileCollection; +import org.gradle.api.file.ProjectLayout; +import org.gradle.api.invocation.Gradle; +import org.gradle.api.logging.LogLevel; +import org.gradle.api.logging.Logger; +import org.gradle.api.logging.configuration.ShowStacktrace; +import org.gradle.api.model.ObjectFactory; +import org.gradle.api.plugins.ExtensionContainer; +import org.gradle.api.plugins.ExtraPropertiesExtension; +import org.gradle.api.tasks.TaskContainer; + +import java.net.URL; +import java.util.Arrays; + +import static org.xtclang.plugin.XtcPluginConstants.XTC_PLUGIN_VERBOSE_PROPERTY; + +public abstract class ProjectDelegate { + protected final Project project; + protected final String projectName; + protected final String prefix; + protected final ObjectFactory objects; + protected final Logger logger; + protected final Gradle gradle; + protected final StartParameter startParameter; + protected final boolean overrideVerboseLogging; + protected final ConfigurationContainer configs; + protected final AdhocComponentWithVariants component; + protected final URL pluginUrl; + protected final TaskContainer tasks; + protected final ProjectLayout layout; + protected final DirectoryProperty buildDir; + protected final ExtraPropertiesExtension extra; + protected final ExtensionContainer extensions; + protected final VersionCatalogsExtension versionCatalogExtension; + + @SuppressWarnings("unused") + protected ProjectDelegate(final Project project) { + this(project, null); + } + + protected ProjectDelegate(final Project project, final String taskName) { + this(project, taskName, null); + } + + protected ProjectDelegate(final Project project, final String taskName, final AdhocComponentWithVariants component) { + this.project = project; + this.projectName = project.getName(); + this.prefix = prefix(taskName); + this.objects = project.getObjects(); + this.layout = project.getLayout(); + this.gradle = project.getGradle(); + this.startParameter = gradle.getStartParameter(); + this.configs = project.getConfigurations(); + this.logger = project.getLogger(); + // Even if we add tasks later, this refers to a task container, so it's fine to initialize it here, and it can be final + this.tasks = project.getTasks(); + this.extensions = project.getExtensions(); + this.buildDir = layout.getBuildDirectory(); + this.extra = extensions.getByType(ExtraPropertiesExtension.class); + this.versionCatalogExtension = extensions.findByType(VersionCatalogsExtension.class); + this.component = component; + this.pluginUrl = getClass().getProtectionDomain().getCodeSource().getLocation(); + + // add a property to the existing environment, project.setProperty assumes the property exists already + extra.set("logPrefix", prefix); + + // Used to print only key messages with an "always" semantic. Used to quickly switch on and off, + // or persist in the shell, a setting that is used for stuff like always printing launcher command + // lines, regardless of log level, but not doing it if the override is turned off (default). + this.overrideVerboseLogging = "true".equalsIgnoreCase(System.getenv(XTC_PLUGIN_VERBOSE_PROPERTY)); + if (overrideVerboseLogging) { + logger.info("{} ORG_XTCLANG_PLUGIN_VERBOSE=true; the XTC Plugin may log important 'info' level events at 'lifecycle' level instead.", prefix); + } + } + + @SuppressWarnings("UnusedReturnValue") + protected final R apply() { + return apply(null); + } + + public abstract R apply(T arg); + + public final XtcBuildRuntimeException buildException(final String msg, final Object... args) { + return buildException(null, msg, args); + } + + public final XtcBuildRuntimeException buildException(final Throwable t, final String msg, final Object... args) { + logger.error(msg, t); + return new XtcBuildRuntimeException(t, prefix + ": " + msg, args); + } + + /** + * We count everything with the log level "info" or finer as verbose logging. + * + * @return True of we are running with verbose logging enabled, false otherwise. + */ + public boolean hasVerboseLogging() { + return switch (getLogLevel()) { + case DEBUG, INFO -> true; + default -> overrideVerboseLogging; + }; + } + + @SuppressWarnings("unused") + public boolean showStackTraces() { + return startParameter.getShowStacktrace() != ShowStacktrace.INTERNAL_EXCEPTIONS; + } + + public final String prefix() { + return prefix(project); + } + + public final String prefix(final Task task) { + return prefix(project, task); + } + + public final String prefix(final String taskName) { + return prefix(projectName, taskName); + } + + public static String prefix(final Project project) { + return prefix(project, (String)null); + } + + public static String prefix(final Project project, final Task task) { + return prefix(project.getName(), task == null ? null : task.getName()); + } + + public static String prefix(final Project project, final String taskName) { + return prefix(project.getName(), taskName); + } + + public static String prefix(final String projectName, final String taskName) { + final var taskString = taskName == null ? "" : (':' + taskName); + return '[' + java.util.Objects.requireNonNull(projectName) + taskString + ']'; + } + + public Project getProject() { + return project; + } + + public String getProjectName() { + return projectName; + } + + public ObjectFactory getObjects() { + return objects; + } + + public Logger getLogger() { + return project.getLogger(); + } + + @SuppressWarnings("unused") + public LogLevel getLogLevel() { + return startParameter.getLogLevel(); + } + + /** + * Flag task as always needing to be re-run. Cached state will be ignored. + *

+ * Can be used to implement, e.g., forceRebuild, and other behaviors that require a task to run fresh every + * time. that absolutely need to be rerun every time. Note that it's easier to just flag a task implementation + * as @NonCacheable. This is intended for unit tested, extended existing tasks and finer granularity levels + * of dependencies. The implementation forbids the task to cache outputs, and it will never be reported as + * up to date. Be aware that this totally removes most of the benefits of Gradle. + * + * @param task Task to flag as perpetually not up to date. + */ + public void considerNeverUpToDate(final Task task) { + task.getOutputs().cacheIf(t -> false); + task.getOutputs().upToDateWhen(t -> false); + logger.warn("{} WARNING: '{}' is configured to always be treated as out of date, and will be run. Do not include this as a part of the normal build cycle!", prefix, task.getName()); + } + + public FileCollection filesFrom(final String... configNames) { + return filesFrom(false, configNames); + } + + public FileCollection filesFrom(final boolean shouldBeResolved, final String... configNames) { + logger.info("{} Resolving filesFrom config: {}", prefix, Arrays.asList(configNames)); + FileCollection fc = objects.fileCollection(); + for (final var name : configNames) { + final Configuration config = configs.getByName(name); + if (shouldBeResolved && config.getState() != Configuration.State.RESOLVED) { + throw buildException("Configuration '{}' is not resolved, which is a requirement from the task execution phase.", name); + } + final var files = project.files(config); + logger.info("{} Scanning file collection: filesFrom: {} {}, files: {}", prefix, name, config.getState(), files.getFiles()); + fc = fc.plus(files); + } + fc.getAsFileTree().forEach(it -> logger.info("{} Resolved fileTree '{}'", prefix, it.getAbsolutePath())); + return fc; + } + + protected E ensureExtension(final String name, final Class clazz) { + if (extensions.findByType(clazz) == null) { + return extensions.create(name, clazz, project); + } + return extensions.getByType(clazz); + } +} diff --git a/plugin/src/main/java/org/xtclang/plugin/Usage.java b/plugin/src/main/java/org/xtclang/plugin/Usage.java new file mode 100644 index 0000000000..0009ba4967 --- /dev/null +++ b/plugin/src/main/java/org/xtclang/plugin/Usage.java @@ -0,0 +1,10 @@ +package org.xtclang.plugin; + +public final class Usage { + private Usage() { + } + + public static void main(final String[] args) { + throw new UnsupportedOperationException("ERROR: The XTC Gradle plugin is not intended to be run as a standalone application."); + } +} diff --git a/plugin/src/main/java/org/xtclang/plugin/XtcBuildException.java b/plugin/src/main/java/org/xtclang/plugin/XtcBuildException.java new file mode 100644 index 0000000000..0b61bf8d5f --- /dev/null +++ b/plugin/src/main/java/org/xtclang/plugin/XtcBuildException.java @@ -0,0 +1,45 @@ +package org.xtclang.plugin; + +import java.io.Serial; +import java.util.Arrays; + +public class XtcBuildException extends Exception { + @Serial + private static final long serialVersionUID = 1L; + + @SuppressWarnings("unused") + XtcBuildException(final String msg) { + this(null, msg); + } + + @SuppressWarnings("unused") + XtcBuildException(final String msg, final Object... args) { + this(null, msg, args); + } + + XtcBuildException(final Throwable cause, final String msg) { + super(msg, cause); + } + + XtcBuildException(final Throwable cause, final String msg, final Object... args) { + this(cause, resolveEllipsis(msg, args)); + } + + static String resolveEllipsis(final String msg, final Object... args) { + final var template = msg.replace("{}", "#"); + final var list = Arrays.asList(args); + final var sb = new StringBuilder(); + for (int i = 0, pos = 0; i < template.length(); i++) { + final var c = template.charAt(i); + if (c == '#') { + if (pos >= list.size()) { + throw new IllegalArgumentException("More ellipses than tokens in expansion: " + pos + " != " + list.size()); + } + sb.append(list.get(pos++)); + } else { + sb.append(c); + } + } + return sb.toString(); + } +} diff --git a/plugin/src/main/java/org/xtclang/plugin/XtcBuildRuntimeException.java b/plugin/src/main/java/org/xtclang/plugin/XtcBuildRuntimeException.java new file mode 100644 index 0000000000..70a72290db --- /dev/null +++ b/plugin/src/main/java/org/xtclang/plugin/XtcBuildRuntimeException.java @@ -0,0 +1,28 @@ +package org.xtclang.plugin; + +import org.gradle.api.GradleException; + +import java.io.Serial; + +public class XtcBuildRuntimeException extends GradleException { + @Serial + private static final long serialVersionUID = 1L; + + @SuppressWarnings("unused") + XtcBuildRuntimeException(final String msg) { + this(null, msg); + } + + @SuppressWarnings("unused") + XtcBuildRuntimeException(final String msg, final Object... args) { + this(null, msg, args); + } + + XtcBuildRuntimeException(final Throwable cause, final String msg) { + super(msg, cause); + } + + XtcBuildRuntimeException(final Throwable cause, final String msg, final Object... args) { + this(cause, XtcBuildException.resolveEllipsis(msg, args)); + } +} diff --git a/plugin/src/main/java/org/xtclang/plugin/XtcCompilerExtension.java b/plugin/src/main/java/org/xtclang/plugin/XtcCompilerExtension.java new file mode 100644 index 0000000000..7f77b4cd37 --- /dev/null +++ b/plugin/src/main/java/org/xtclang/plugin/XtcCompilerExtension.java @@ -0,0 +1,17 @@ +package org.xtclang.plugin; + +import org.gradle.api.provider.Property; + +public interface XtcCompilerExtension extends XtcLauncherTaskExtension { + Property getDisableWarnings(); + + Property getStrict(); + + Property getQualifiedOutputName(); + + Property getVersionedOutputName(); + + Property getXtcVersion(); + + Property getForceRebuild(); +} diff --git a/plugin/src/main/java/org/xtclang/plugin/XtcExtension.java b/plugin/src/main/java/org/xtclang/plugin/XtcExtension.java new file mode 100644 index 0000000000..072e8586e9 --- /dev/null +++ b/plugin/src/main/java/org/xtclang/plugin/XtcExtension.java @@ -0,0 +1,4 @@ +package org.xtclang.plugin; + +public interface XtcExtension { +} diff --git a/plugin/src/main/java/org/xtclang/plugin/XtcLauncherTaskExtension.java b/plugin/src/main/java/org/xtclang/plugin/XtcLauncherTaskExtension.java new file mode 100644 index 0000000000..7bf5ff4a58 --- /dev/null +++ b/plugin/src/main/java/org/xtclang/plugin/XtcLauncherTaskExtension.java @@ -0,0 +1,43 @@ +package org.xtclang.plugin; + +import org.gradle.api.provider.ListProperty; +import org.gradle.api.provider.Property; +import org.gradle.api.provider.Provider; + +import java.io.InputStream; +import java.io.OutputStream; + +@SuppressWarnings("unused") +public interface XtcLauncherTaskExtension { + Property getFork(); + + Property getShowVersion(); + + Property getUseNativeLauncher(); + + Property getVerbose(); + + ListProperty getJvmArgs(); + + Property getStdin(); + + Property getStdout(); + + Property getStderr(); + + default void jvmArg(String arg) { + jvmArgs(arg); + } + + void jvmArg(Provider arg); + + void jvmArgs(String... args); + + void jvmArgs(Iterable args); + + void jvmArgs(Provider> provider); + + void setJvmArgs(Iterable elements); + + void setJvmArgs(Provider> provider); +} diff --git a/plugin/src/main/java/org/xtclang/plugin/XtcModulePath.java b/plugin/src/main/java/org/xtclang/plugin/XtcModulePath.java new file mode 100644 index 0000000000..6bff463e15 --- /dev/null +++ b/plugin/src/main/java/org/xtclang/plugin/XtcModulePath.java @@ -0,0 +1,12 @@ +package org.xtclang.plugin; + +/** + * Abstraction for the ModulePath for an XTC task. + *

+ * TODO: Implement me, and sort out the semi-messy logic in XtcProjectDelegate, where the current module + * resolution logic exists. + */ + +@SuppressWarnings("unused") +public class XtcModulePath { +} diff --git a/plugin/src/main/java/org/xtclang/plugin/XtcPlugin.java b/plugin/src/main/java/org/xtclang/plugin/XtcPlugin.java new file mode 100644 index 0000000000..ce002beed3 --- /dev/null +++ b/plugin/src/main/java/org/xtclang/plugin/XtcPlugin.java @@ -0,0 +1,47 @@ + +package org.xtclang.plugin; + +import org.gradle.api.Project; +import org.gradle.api.Plugin; +import org.gradle.api.component.AdhocComponentWithVariants; +import org.gradle.api.component.SoftwareComponentFactory; +import org.gradle.api.plugins.JavaBasePlugin; +import org.jetbrains.annotations.NotNull; + +import javax.inject.Inject; +import java.util.Set; + +@SuppressWarnings("unused") +public class XtcPlugin implements Plugin { + /** Software component for an XTC project, equivalent to components["java"], used e.g. for publishing */ + private static final Set> REQUIRED_PLUGINS = Set.of( + JavaBasePlugin.class, + XtcProjectPlugin.class + ); + + /** + * A project scoped XTC plugin. Delegates is state on apply to a project + * aware plugin delegate, where the main plugin logic exists. This is to + * maximize final state, and not have to request project information through + * the Gradle APIs, everywhere. + */ + static class XtcProjectPlugin implements Plugin { + private final AdhocComponentWithVariants xtcComponent; + + @Inject + XtcProjectPlugin(final SoftwareComponentFactory softwareComponentFactory) { + this.xtcComponent = softwareComponentFactory.adhoc(XtcPluginConstants.XTC_COMPONENT_NAME); + } + + @Override + public void apply(final @NotNull Project project) { + new XtcProjectDelegate(project, xtcComponent).apply(); + } + } + + @Override + public void apply(final Project project) { + final var plugins = project.getPluginManager(); + REQUIRED_PLUGINS.forEach(plugins::apply); + } +} diff --git a/plugin/src/main/java/org/xtclang/plugin/XtcPluginConstants.java b/plugin/src/main/java/org/xtclang/plugin/XtcPluginConstants.java new file mode 100644 index 0000000000..1219afc801 --- /dev/null +++ b/plugin/src/main/java/org/xtclang/plugin/XtcPluginConstants.java @@ -0,0 +1,75 @@ +package org.xtclang.plugin; + +import org.gradle.api.Project; + +import java.io.File; +import java.util.Collections; +import java.util.Set; + +public final class XtcPluginConstants { + // XTC Language and dependency constants: + public static final String XTC_COMPONENT_NAME = "xtcComponent"; + public static final String XTC_LANGUAGE_NAME = "xtc"; + public static final String XTC_MODULE_FILE_EXTENSION = XTC_LANGUAGE_NAME; + public static final String XTC_SOURCE_FILE_EXTENSION = "x"; + public static final String XTC_SOURCE_SET_DIRECTORY_ROOT_NAME = "x"; + public static final String XTC_CONFIG_NAME_INCOMING = "xtcModule"; + public static final String XTC_CONFIG_NAME_OUTGOING = XTC_CONFIG_NAME_INCOMING + "Provider"; + public static final String XTC_CONFIG_NAME_MODULE_DEPENDENCY = "xtcModuleDeps"; + public static final String XTC_CONFIG_NAME_INCOMING_TEST = XTC_CONFIG_NAME_INCOMING + "Test"; + public static final String XTC_CONFIG_NAME_OUTGOING_TEST = XTC_CONFIG_NAME_OUTGOING + "Test"; + + // XTC Compile time constants + public static final String XTC_EXTENSION_NAME_COMPILER = "xtcCompile"; + public static final String XTC_COMPILER_CLASS_NAME = "org.xvm.tool.Compiler"; + public static final String XTC_COMPILER_LAUNCHER_NAME = "xcc"; + @SuppressWarnings("unused") // TODO: This will be added to facilitate publication of single XTC project artifacts. + public static final String XTC_COMPONENT_VARIANT_COMPILE = "compile"; + + // XTC Runtime constants: + public static final String XTC_EXTENSION_NAME_RUNTIME = "xtcRun"; + public static final String XTC_DEFAULT_RUN_METHOD_NAME_PREFIX = "run"; + public static final String XTC_RUNNER_CLASS_NAME = "org.xvm.tool.Runner"; + public static final String XTC_RUNNER_LAUNCHER_NAME = "xec"; + @SuppressWarnings("unused") // TODO: This will be added to facilitate publication of single XTC project artifacts. + public static final String XTC_COMPONENT_VARIANT_RUNTIME = "runtime"; + + // XDK Distribution constants: + public static final String XDK_CONFIG_NAME_INCOMING = "xdk"; + public static final String XDK_CONFIG_NAME_INCOMING_ZIP = "xdkDistribution"; + public static final String XDK_CONFIG_NAME_CONTENTS = "xdkContents"; + public static final String XDK_LIBRARY_ELEMENT_TYPE_XDK_CONTENTS = "xdk-contents"; + public static final String XDK_LIBRARY_ELEMENT_TYPE = "xdk-distribution-archive"; + public static final String XDK_EXTRACT_TASK_NAME = "extractXdk"; + public static final String XDK_VERSION_FILE_TASK_NAME = "xtcVersionFile"; + public static final String XDK_VERSION_TASK_NAME = "xtcVersion"; + public static final String XDK_VERSION_GROUP_NAME = "version"; + public static final String XDK_VERSION_PATH = "VERSION"; + + // Library (mostly Java tools) constants: + public static final String XDK_JAVATOOLS_ARTIFACT_ID = "javatools"; + public static final String XDK_JAVATOOLS_ARTIFACT_SUFFIX = "jar"; + public static final String XDK_CONFIG_NAME_JAVATOOLS_INCOMING = "xdkJavaTools"; + public static final String XDK_CONFIG_NAME_JAVATOOLS_OUTGOING = XDK_CONFIG_NAME_JAVATOOLS_INCOMING + "Provider"; + + // Config artifacts from the XDK build: + public static final String XDK_CONFIG_NAME_ARTIFACT_JAVATOOLS_FATJAR = "javatools-fatjar"; + + // Debugging (for example, adding significant events to output without increasing the log level) + public static final String XTC_PLUGIN_VERBOSE_PROPERTY = "ORG_XTCLANG_PLUGIN_VERBOSE"; + + // Default "empty" values for collections and Gradle API classes. + public static final Set EMPTY_FILE_COLLECTION = Collections.emptySet(); + public static final String UNSPECIFIED = Project.DEFAULT_VERSION; + + // JavaTools (launcher native code) + public static final String JAR_MANIFEST_PATH = "META-INF/MANIFEST.MF"; + public static final String JAVATOOLS_JAR_NAME = "javatools.jar"; + + // XTC Magic Number, for future verification of XTC module binaries, and for parts of language server support. + @SuppressWarnings("unused") + public static final long XTC_MAGIC = 0xEC57_A5EEL; + + private XtcPluginConstants() { + } +} diff --git a/plugin/src/main/java/org/xtclang/plugin/XtcPluginUtils.java b/plugin/src/main/java/org/xtclang/plugin/XtcPluginUtils.java new file mode 100644 index 0000000000..901ce5cabb --- /dev/null +++ b/plugin/src/main/java/org/xtclang/plugin/XtcPluginUtils.java @@ -0,0 +1,135 @@ +package org.xtclang.plugin; + +import org.gradle.api.Project; +import org.gradle.api.provider.Provider; + +import java.io.DataInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.jar.Attributes; +import java.util.jar.JarFile; +import java.util.jar.Manifest; + +import static java.util.Objects.requireNonNull; +import static org.xtclang.plugin.XtcPluginConstants.XDK_JAVATOOLS_ARTIFACT_ID; +import static org.xtclang.plugin.XtcPluginConstants.XTC_MAGIC; +import static org.xtclang.plugin.XtcPluginConstants.XTC_MODULE_FILE_EXTENSION; + +/** + * XTC Plugin Helper methods in a utility class. + *

+ * TODO: Move the state independent/reentrant stuff from the ProjectDelegate and its subclasses to here. + */ +public final class XtcPluginUtils { + private XtcPluginUtils() { + } + + public static Provider> singleArgumentIterableProvider(final Project project, final Provider arg) { + return project.provider(() -> List.of(arg.get())); + } + + public static List argumentArrayToList(final String... args) { + return Arrays.stream(ensureNotNull(args)).map(String::valueOf).toList(); + } + + private static Object[] ensureNotNull(final String... array) { + Arrays.stream(array).forEach(e -> Objects.requireNonNull(e, "Arguments must never be null.")); + return array; + } + + public static String capitalize(final String string) { + return Character.toUpperCase(string.charAt(0)) + string.substring(1); + } + + public static final class FileUtils { + private FileUtils() { + } + + /** + * Check that a file is a valid JavaTools artifact. This means that the file should exist as a regular + * file, be readable, have a name with the format javatools[version-and-extension].jar, and that its + * manifest is a valid XTC/XDK manifest, including semantic version information for the XDK to which + * it belongs. + * + * @param file file to check + * @return true if the file is a valid JavaTools jar file, false otherwise. + */ + public static boolean isValidJavaToolsArtifact(final File file) { + return hasJarExtension(file) && file.getName().startsWith(XDK_JAVATOOLS_ARTIFACT_ID) && readXdkVersionFromJar(file) != null; + } + + /** + * Check that a file is a valid XTC Module. This means that it should exist, be readable, and + * contain the correct magic at the first bytes. We don't currently check if it's an XDK version, + * or if it's version has a particular format. + * + * @param file file to check + * @return true if the file seems to be a valid XTC module, false otherwise + */ + public static boolean isValidXtcModule(final File file) { + return isValidXtcModule(file, true); + } + + @SuppressWarnings("SameParameterValue") + private static boolean isValidXtcModule(final File file, final boolean checkMagic) { + if (!file.exists() || !file.isFile() || !hasFileExtension(file, XTC_MODULE_FILE_EXTENSION)) { + return false; + } + if (!checkMagic) { + return true; + } + try (final var in = new DataInputStream(new FileInputStream(file))) { + return (in.readInt() & 0xffff_ffffL) == XTC_MAGIC; + } catch (final IOException e) { + return false; + } + } + + /** + * Reads the XDK version from a jar manifest. + * + * @param file Jar file from which to read + * @return the XDK version string, as stored in the jar, or null if file not found, or entry could not be parsed. + */ + public static String readXdkVersionFromJar(final File file) { + if (file == null) { + return null; + } + final var path = file.getAbsolutePath(); + assert file.isFile(); + try (final var jarFile = new JarFile(file)) { + final Manifest m = jarFile.getManifest(); + final var implVersion = m.getMainAttributes().get(Attributes.Name.IMPLEMENTATION_VERSION); + if (implVersion == null) { + throw new XtcBuildRuntimeException("Invalid manifest entries found in '{}'", path); + } + return implVersion.toString(); + } catch (final IOException e) { + throw new XtcBuildRuntimeException(e, "Not a valid 'javatools.jar': '{}'", path); + } + } + + private static boolean hasJarExtension(final File file) { + return hasFileExtension(file, "jar"); + } + + public static boolean hasFileExtension(final File file, final String extension) { + return getFileExtension(file).equalsIgnoreCase(extension); + } + + public static String getFileExtension(final File file) { + final String name = file.getName(); + final int dot = name.lastIndexOf('.'); + return dot == -1 ? "" : name.substring(dot + 1); + } + + public static boolean areIdenticalFiles(final File f1, final File f2) throws IOException { + return Files.mismatch(requireNonNull(f1).toPath(), requireNonNull(f2).toPath()) == -1L; + } + } +} diff --git a/plugin/src/main/java/org/xtclang/plugin/XtcProjectDelegate.java b/plugin/src/main/java/org/xtclang/plugin/XtcProjectDelegate.java new file mode 100644 index 0000000000..d27e893d2d --- /dev/null +++ b/plugin/src/main/java/org/xtclang/plugin/XtcProjectDelegate.java @@ -0,0 +1,583 @@ +package org.xtclang.plugin; + +import org.gradle.api.Project; +import org.gradle.api.Task; +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.artifacts.ModuleVersionSelector; +import org.gradle.api.artifacts.type.ArtifactTypeDefinition; +import org.gradle.api.attributes.Category; +import org.gradle.api.attributes.LibraryElements; +import org.gradle.api.component.AdhocComponentWithVariants; +import org.gradle.api.file.Directory; +import org.gradle.api.file.FileCollection; +import org.gradle.api.internal.tasks.DefaultSourceSet; +import org.gradle.api.model.ObjectFactory; +import org.gradle.api.plugins.JavaPlugin; +import org.gradle.api.plugins.JavaPluginExtension; +import org.gradle.api.provider.Provider; +import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.SourceSetContainer; +import org.gradle.api.tasks.SourceSetOutput; +import org.gradle.api.tasks.TaskProvider; +import org.xtclang.plugin.internal.DefaultXtcCompilerExtension; +import org.xtclang.plugin.internal.DefaultXtcExtension; +import org.xtclang.plugin.internal.DefaultXtcRuntimeExtension; +import org.xtclang.plugin.internal.DefaultXtcSourceDirectorySet; +import org.xtclang.plugin.tasks.XtcCompileTask; +import org.xtclang.plugin.tasks.XtcExtractXdkTask; +import org.xtclang.plugin.tasks.XtcRunAllTask; +import org.xtclang.plugin.tasks.XtcRunTask; + +import java.io.IOException; +import java.net.URL; +import java.nio.file.Files; + +import static org.gradle.api.attributes.Category.CATEGORY_ATTRIBUTE; +import static org.gradle.api.attributes.Category.LIBRARY; +import static org.gradle.api.attributes.LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE; +import static org.gradle.api.plugins.ApplicationPlugin.APPLICATION_GROUP; +import static org.gradle.api.tasks.SourceSet.MAIN_SOURCE_SET_NAME; +import static org.gradle.language.base.plugins.LifecycleBasePlugin.BUILD_GROUP; +import static org.xtclang.plugin.XtcPluginConstants.UNSPECIFIED; +import static org.xtclang.plugin.XtcPluginConstants.XDK_CONFIG_NAME_ARTIFACT_JAVATOOLS_FATJAR; +import static org.xtclang.plugin.XtcPluginConstants.XDK_CONFIG_NAME_CONTENTS; +import static org.xtclang.plugin.XtcPluginConstants.XDK_CONFIG_NAME_INCOMING; +import static org.xtclang.plugin.XtcPluginConstants.XDK_CONFIG_NAME_INCOMING_ZIP; +import static org.xtclang.plugin.XtcPluginConstants.XDK_CONFIG_NAME_JAVATOOLS_INCOMING; +import static org.xtclang.plugin.XtcPluginConstants.XDK_CONFIG_NAME_JAVATOOLS_OUTGOING; +import static org.xtclang.plugin.XtcPluginConstants.XDK_EXTRACT_TASK_NAME; +import static org.xtclang.plugin.XtcPluginConstants.XDK_LIBRARY_ELEMENT_TYPE; +import static org.xtclang.plugin.XtcPluginConstants.XDK_LIBRARY_ELEMENT_TYPE_XDK_CONTENTS; +import static org.xtclang.plugin.XtcPluginConstants.XDK_VERSION_FILE_TASK_NAME; +import static org.xtclang.plugin.XtcPluginConstants.XDK_VERSION_GROUP_NAME; +import static org.xtclang.plugin.XtcPluginConstants.XDK_VERSION_PATH; +import static org.xtclang.plugin.XtcPluginConstants.XDK_VERSION_TASK_NAME; +import static org.xtclang.plugin.XtcPluginConstants.XTC_CONFIG_NAME_INCOMING; +import static org.xtclang.plugin.XtcPluginConstants.XTC_CONFIG_NAME_INCOMING_TEST; +import static org.xtclang.plugin.XtcPluginConstants.XTC_CONFIG_NAME_OUTGOING; +import static org.xtclang.plugin.XtcPluginConstants.XTC_CONFIG_NAME_OUTGOING_TEST; +import static org.xtclang.plugin.XtcPluginConstants.XTC_DEFAULT_RUN_METHOD_NAME_PREFIX; +import static org.xtclang.plugin.XtcPluginConstants.XTC_EXTENSION_NAME_COMPILER; +import static org.xtclang.plugin.XtcPluginConstants.XTC_EXTENSION_NAME_RUNTIME; +import static org.xtclang.plugin.XtcPluginConstants.XTC_LANGUAGE_NAME; +import static org.xtclang.plugin.XtcPluginConstants.XTC_SOURCE_FILE_EXTENSION; +import static org.xtclang.plugin.XtcPluginConstants.XTC_SOURCE_SET_DIRECTORY_ROOT_NAME; +import static org.xtclang.plugin.XtcPluginUtils.capitalize; + +/** + * Base class for the Gradle XTC Plugin in a project context. + */ +public class XtcProjectDelegate extends ProjectDelegate { + + @SuppressWarnings("unused") + public XtcProjectDelegate(final Project project) { + this(project, null); + } + + public XtcProjectDelegate(final Project project, final AdhocComponentWithVariants component) { + super(project, null, component); + // TODO: Fix the JavaTools resolution code, which is a bit hacky right now. + // Enable calling the Launcher from the plugin to e.g. verify if an .x file defines a module + // instead of relying on "top .x file level"-layout for module definitions. + } + + @SuppressWarnings("fallthrough") + private void hideTask(final Task task) { + switch (task.getName()) { + case "jar": + case "compileJava": + case "compileTestJava": + // TODO: Start showing this when we have figured out their semantics, as not to confuse the user atm + case "runAllXtc": + case "runAllTestXtc": + logger.info("{} Hiding internal task: '{}'.", prefix, task.getName()); + task.setGroup(null); + break; + default: + break; + } + } + + private void applyJavaPlugin() { + project.getPluginManager().apply(JavaPlugin.class); + // At the moment we piggyback on the extended build LifeCycle provided + // by the JavaPlugin, as well as the source sets, and other things that + // should really be language independent in Gradle, but arent (yet). + // However, tasks like "jar" and similar Java specific tasks, should + // be invisible to the user, as possible, in order to create less + // confusion. This is by no means a rare pattern in Gradle plugins, + // but we still don't have to like it. + // + // TODO: Given enough spare cycles, we will probably create our own + // our own, fully XTC native, life cycle, with source set, and minimal + // changes to semantics where applicable (for example, Java resources + // are not compiled into the class file, and are taken from the build + // director/processResources task outputs. Changing this little piece + // to semantically conform to what XTC does should not be hard, but we + // haven't had the cycles to figure out why the changed build graph + // from doing that isn't 100% compatible with our builds. + tasks.forEach(this::hideTask); + } + + /** + * Register a SoftwareComponent for XTC projects. We will use this like + * components["java"] is currently used for publishing Java artifacts. + */ + private void createXtcComponents() { + project.getComponents().add(component); + } + + /** + * This method, "apply", is a delegate target call for an XTC project delegating plugin + */ + @Override + public Void apply(final Void args) { + applyJavaPlugin(); + createXtcComponents(); + + // Add xtc extension. + // TODO: Later move any non-specific task flags, like "fork = " here, and it will be applied to all tasks. + resolveXtcExtension(); + + // Ensure extensions for configuring the xtc and xec exist. + resolveXtcCompileExtension(); + resolveXtcRuntimeExtension(); + + // This is all config phase. Warn if a project isn't versioned when the XTC plugin is applied, so that we + // are sure no skew/version conflicts exist for inter-module dependencies and cross publication. + checkProjectIsVersioned(); + createDefaultSourceSets(); + createXtcDependencyConfigs(); + + createJavaToolsConfig(); + createResolutionStrategy(); + createVersioningTasks(); + + if (hasVerboseLogging()) { // Only print this (but still at lifecycle level) if verbose logging or the ORG_XTCLANG_PLUGIN_VERBOSE variable is set. + logger.lifecycle("{} XTC Plugin successfully applied to project '{}:{}:{}' (delegate plugin class: {})", + prefix, project.getGroup(), projectName, project.getVersion(), getClass().getSimpleName()); + } + return null; + } + + @Override + public String toString() { + return getClass().getSimpleName() + " (plugin: " + getPluginUrl() + ')'; + } + + public URL getPluginUrl() { + project.getRepositories().add(project.getRepositories().mavenCentral()); + return pluginUrl; + } + + @SuppressWarnings("UnusedReturnValue") + public XtcExtension resolveXtcExtension() { + return ensureExtension(XTC_LANGUAGE_NAME, DefaultXtcExtension.class); + } + + public XtcCompilerExtension resolveXtcCompileExtension() { + return ensureExtension(XTC_EXTENSION_NAME_COMPILER, DefaultXtcCompilerExtension.class); + } + + public XtcRuntimeExtension resolveXtcRuntimeExtension() { + return ensureExtension(XTC_EXTENSION_NAME_RUNTIME, DefaultXtcRuntimeExtension.class); + } + + private String getXtcSourceDirectoryRootPath(final SourceSet sourceSet) { + return "src/" + sourceSet.getName() + '/' + XTC_SOURCE_SET_DIRECTORY_ROOT_NAME; + } + + @SuppressWarnings({"SameParameterValue", "unused"}) + static String locationFor(final Class clazz) { + return clazz.getProtectionDomain().getCodeSource().getLocation().toString(); + } + + public static String incomingXtcModuleDependencies(final SourceSet sourceSet) { + return sourceSet.getName().equals(MAIN_SOURCE_SET_NAME) ? XTC_CONFIG_NAME_INCOMING : XTC_CONFIG_NAME_INCOMING_TEST; + } + + @SuppressWarnings("unused") + static String outgoingXtcModules(final SourceSet sourceSet) { + return sourceSet.getName().equals(MAIN_SOURCE_SET_NAME) ? XTC_CONFIG_NAME_OUTGOING : XTC_CONFIG_NAME_OUTGOING_TEST; + } + + public static Provider getXdkContentsDir(final Project project) { + return project.getLayout().getBuildDirectory().dir("xtc/xdk/lib"); + } + + public Provider getXdkContentsDir() { + return getXdkContentsDir(project); + } + + public FileCollection getXtcCompilerOutputModules(final SourceSet sourceSet) { + return buildDir.files(XTC_LANGUAGE_NAME + '/' + sourceSet.getName() + "/lib"); + } + + public Provider getXtcCompilerOutputDirModules(final SourceSet sourceSet) { + return buildDir.dir(XTC_LANGUAGE_NAME + '/' + sourceSet.getName() + "/lib"); + } + + public Provider getXtcCompilerOutputResourceDir(final SourceSet sourceSet) { + return buildDir.dir(XTC_LANGUAGE_NAME + '/' + sourceSet.getName() + "/resources"); + } + + public static String getCompileTaskName(final SourceSet sourceSet) { + return sourceSet.getCompileTaskName(XTC_LANGUAGE_NAME); + } + + public static String getRunTaskName(final SourceSet sourceSet, final boolean isRunAllTask) { + final var sourceSetName = sourceSet.getName(); + final var isMain = isMainSourceSet(sourceSet); + final var sb = new StringBuilder(); + sb.append(XTC_DEFAULT_RUN_METHOD_NAME_PREFIX); + if (isRunAllTask) { + sb.append("All"); + } + if (!isMain) { + sb.append(capitalize(sourceSetName)); + } + sb.append(capitalize(XTC_LANGUAGE_NAME)); + return sb.toString(); + } + + private static String getClassesTaskName(final SourceSet sourceSet) { + return isMainSourceSet(sourceSet) ? "classes" : sourceSet.getName() + "Classes"; + } + + @SuppressWarnings("unused") + private static String getProcessResourcesTaskName(final SourceSet sourceSet) { + return isMainSourceSet(sourceSet) ? "processResources" : "process" + capitalize(sourceSet.getName()) + "Resources"; + } + + private static boolean isMainSourceSet(final SourceSet sourceSet) { + return MAIN_SOURCE_SET_NAME.equals(sourceSet.getName()); + } + + // TODO: Move to static factory in compile task? + private TaskProvider createCompileTask(final SourceSet sourceSet) { + final var compileTaskName = getCompileTaskName(sourceSet); + final var compileTask = tasks.register(compileTaskName, XtcCompileTask.class, this, compileTaskName, sourceSet); + final var forceRebuild = resolveXtcCompileExtension().getForceRebuild(); + //final var processResources = tasks.getByName(getProcessResourcesTaskName(sourceSet)); + + compileTask.configure(task -> { + task.setGroup(BUILD_GROUP); + task.setDescription("Compile an XTC source set, similar to the JavaCompile task for Java."); + task.dependsOn(XDK_EXTRACT_TASK_NAME); + // TODO Fix more exact processResources semantics so that we use the build as resource path, and not the src. This works for first merge. + if (forceRebuild.get()) { + logger.warn("{} WARNING: '{}' Force rebuild is true for this compile task. Task is flagged as always stale/non-cacheable.", prefix(projectName, compileTaskName), compileTaskName); + considerNeverUpToDate(task); + } + task.setSource(sourceSet.getExtensions().getByName(XTC_LANGUAGE_NAME)); + task.doLast(t -> { + // This happens during task execution, after the config phase. + logger.info("{} Finished. Outputs in: {}", prefix(projectName, compileTaskName), t.getOutputs().getFiles().getAsFileTree()); + sourceSet.getOutput().getAsFileTree().forEach(it -> logger.info("{} compileXtc sourceSet output: {}", prefix, it)); + }); + }); + + // Find the "classes" task in the Java build life cycle that we reuse, and set the dependency correctly. This should + // wire in process resources too, but for some reason it seems to work differently. Basically this goes to the + // "assemble" task, but we want to reuse some of the Java life cycle internally. + tasks.getByName(getClassesTaskName(sourceSet)).dependsOn(compileTask); + + logger.info("{} Mapping source set to compile task: {} -> {}", prefix, sourceSet.getName(), compileTaskName); + logger.info("{} Registered and configured compile task for sourceSet: {}", prefix, sourceSet.getName()); + + return compileTask; + } + + /** + * Create the XTC run task. If there are no explicit modules in the xtcRun config, we don't create it, + * or we log an error or something. The run task will depend on the compile task, and make sure an XTC + * module in the source set is compiled. + * + * @param sourceSet the source set (typically main or test), but can be customized through the standard + * mechanisms, of course. + * @return the task provider of the rn task. + */ + // TODO: Move to static factory in run task? + private TaskProvider createRunTask(final SourceSet sourceSet, final Class clazz) { + final var runTaskName = getRunTaskName(sourceSet, isRunAllTask(clazz)); + final var compileTaskName = sourceSet.getCompileTaskName(XTC_LANGUAGE_NAME); + final var runTask = tasks.register(runTaskName, clazz, this, runTaskName, sourceSet); + runTask.configure(task -> { + task.setGroup(APPLICATION_GROUP); + task.setDescription("Run an XTC program with a configuration supplying the module path(s)."); + task.dependsOn(XDK_EXTRACT_TASK_NAME); + task.dependsOn(compileTaskName); // It's important to remember to depend on compile. + logger.info("{} Configured, dependency to tasks: {} -> {}", prefix, XDK_EXTRACT_TASK_NAME, sourceSet.getCompileTaskName(XTC_LANGUAGE_NAME)); + }); + logger.info("{} Created task: '{}'", prefix, runTask.getName()); + return runTask; + } + + private static boolean isRunAllTask(final Class clazz) { + return clazz == XtcRunAllTask.class; + } + + private String getSemanticVersion() { + final var group = project.getGroup().toString(); + final var version = project.getVersion().toString(); + if (group.isEmpty() || Project.DEFAULT_VERSION.equals(version)) { + logger.error("{} Has not been properly versioned (group={}, version={})", prefix, group, version); + } + return group + ':' + projectName + ':' + version; + } + + private void createVersioningTasks() { + tasks.register(XDK_VERSION_TASK_NAME, task -> { + task.setGroup(XDK_VERSION_GROUP_NAME); + task.setDescription("Display XTC version for project, and sanity check its application."); + task.doLast(t -> logger.info("{} XTC (version '{}'); Semantic Version: '{}'", + prefix(projectName, XDK_VERSION_TASK_NAME), project.getVersion(), getSemanticVersion())); + }); + + tasks.register(XDK_VERSION_FILE_TASK_NAME, task -> { + task.setGroup(XDK_VERSION_GROUP_NAME); + task.setDescription("Generate a file containing the XDK/XTC version under the build tree."); + final var version = buildDir.file(XDK_VERSION_PATH); + task.getOutputs().file(version); + task.doLast(t -> { + final var semanticVersion = getSemanticVersion(); + final var file = version.get().getAsFile(); + logger.info("{} Writing version information: '{}' to '{}'", prefix(projectName, XDK_VERSION_FILE_TASK_NAME), semanticVersion, file.getAbsolutePath()); + try { + Files.writeString(file.toPath(), semanticVersion + System.lineSeparator()); + } catch (final IOException e) { + throw buildException(e, "I/O error when writing VERSION file: '{}'.", e.getMessage()); + } + }); + }); + } + + public SourceSetContainer getSourceSets() { + return getJavaExtensionContainer().getSourceSets(); + } + + private void createXtcDependencyConfigs() { + for (final SourceSet sourceSet : getJavaExtensionContainer().getSourceSets()) { + createXtcDependencyConfigs(sourceSet); + } + createXdkDependencyConfigs(); + } + + // Attributes for anything that consumes or produces xtc files to a source set output directory + private void addXtcModuleAttributes(final Configuration config) { + config.attributes(it -> { + it.attribute(CATEGORY_ATTRIBUTE, objects.named(Category.class, LIBRARY)); + it.attribute(LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements.class, xtcModuleLibraryElementName(config))); + }); + } + + // Attributes for anything that consumes or produces xtc files / javatools.jar from/to a directory + private void addXdkContentsAttributes(final Configuration config) { + config.attributes(it -> { + it.attribute(CATEGORY_ATTRIBUTE, objects.named(Category.class, LIBRARY)); + it.attribute(LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements.class, XDK_LIBRARY_ELEMENT_TYPE_XDK_CONTENTS)); + }); + } + + private void addJavaToolsContentsAttributes(final Configuration config) { + config.attributes(it -> { + it.attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.class, Category.LIBRARY)); + it.attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements.class, XDK_CONFIG_NAME_ARTIFACT_JAVATOOLS_FATJAR)); + // it.attribute(CATEGORY_ATTRIBUTE, objects.named(Category.class, JAVA_RUNTIME)); + // it.attribute(LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements.class, XDK_CONFIG_NAME_ARTIFACT_JAVATOOLS_FATJAR)); + }); + } + + private void createXtcDependencyConfigs(final SourceSet sourceSet) { + final var compileTask = createCompileTask(sourceSet); + final var runTask = createRunTask(sourceSet, XtcRunTask.class); + final var runAllTask = createRunTask(sourceSet, XtcRunAllTask.class); + + logger.info("{} Created compile and run tasks for sourceSet '{}' -> '{}', '{}' and '{}'.", prefix, sourceSet.getName(), compileTask.getName(), runTask.getName(), runAllTask.getName()); + + final var xtcModuleConsumerConfig = incomingXtcModuleDependencies(sourceSet); + final var xtcModuleProducerConfig = outgoingXtcModules(sourceSet); + + @SuppressWarnings("unused") final var xtcModule = configs.register(xtcModuleConsumerConfig, config -> { + config.setDescription("Configuration that contains location of the .xtc file created by other entities, so that they can be declared as dependencies."); + config.setCanBeResolved(true); + config.setCanBeConsumed(false); + addXtcModuleAttributes(config); + }); + + final var xtcModuleProvider = configs.register(xtcModuleProducerConfig, config -> { + config.setDescription("Configuration that contains location of the .xtc files produced by this project build."); + config.setCanBeResolved(false); + config.setCanBeConsumed(true); + config.setVisible(false); + addXtcModuleAttributes(config); + }); + + // Tell the system that the system may produce an artifact, which is the output directory for this sourceSet, + // that will contain all xtc files built by the compile task for this source set. This makes it possible for + // other entities to declare an xtcModule dependency, which will force the directory to be refreshed to + // up-to-date artifacts, i.e. xtc module files generated by a compileXtc/compileXtcTest task for someone else's + // source set. + project.artifacts(artifactHandler -> { + // This is already the output of the compile task. + // But we need to declare this artifact in the xdkModuleProvider if we want someone to use the xtcModule consumer + // and get the build directory (also forcing it to be built since it depends on the compileTask) + final var location = getXtcCompilerOutputDirModules(sourceSet); + artifactHandler.add(xtcModuleProvider.getName(), location, artifact -> { + logger.info("{} Adding outgoing artifact {}; builtBy {}.", prefix, location.get(), compileTask.getName()); + artifact.builtBy(compileTask); + artifact.setType(ArtifactTypeDefinition.DIRECTORY_TYPE); + }); + }); + + // Ensure that any produced XTC module files are publishable if we publish the xtcComponent. + // (symmetrical to e.g. jar files and the "java" component) + // List.of(XTC_COMPONENT_VARIANT_COMPILE, XTC_COMPONENT_VARIANT_RUNTIME).forEach(v -> component.addVariantsFromConfiguration(xtcModuleProvider.get(), new JavaConfigurationVariantMapping(v, true))); + } + + private void createXdkDependencyConfigs() { + final var extractTask = tasks.register(XDK_EXTRACT_TASK_NAME, XtcExtractXdkTask.class, this); + + configs.register(XDK_CONFIG_NAME_JAVATOOLS_OUTGOING, it -> { + it.setCanBeConsumed(false); + it.setCanBeResolved(true); + it.setDescription("The xdkJavaToolsProvider configuration is used to resolve the javatools.jar from the XDK."); + }); + + // Configuration for anyone needing a zipped artifact of the XDK, because apparently we can't have library elements. + configs.register(XDK_CONFIG_NAME_INCOMING_ZIP, config -> { + config.setDescription("Configuration specifying dependencies on a particular XDK distribution."); + config.setCanBeResolved(true); + config.setCanBeConsumed(false); + }); + + // Configurations for anyone needing a zipped artifact of the XDK in the includedBuild world, because apparently the library elements are needed. + configs.register(XDK_CONFIG_NAME_INCOMING, config -> { + config.setDescription("Configuration specifying dependencies on a particular XDK distribution."); + // TODO: can we keep the unpacked modules added here as well after unpack task has been run? + config.setCanBeResolved(true); + config.setCanBeConsumed(false); + config.attributes(it -> { + it.attribute(CATEGORY_ATTRIBUTE, objects.named(Category.class, LIBRARY)); + it.attribute(LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements.class, XDK_LIBRARY_ELEMENT_TYPE)); + }); + }); + + // This is the consumer side - a dependency for anyone needing the XDK for runtime or compile + // time stuff. This one wants a directory with the XDK in it. That directory is built by the + // extractXdk task, that needs to know where the zip file is. + // + // By making contents consume only, and adding a dependency to the extract task from run and + // config task, we should be able to guarantee that the configuration contains an extracted XDK. + // We hoped to self-resolve this. This can still possibly be done by adding a resolvable + // extension to it. + // + // "xdkContents" config (resolvable, someone else has created the consumable, likely the XDK build.) + configs.register(XDK_CONFIG_NAME_CONTENTS, config -> { + config.setDescription("Configuration that consumes the contents of an XDK, i.e. .xtc module files and javatools.jar"); + config.setCanBeResolved(false); // resolution forces someone to find and unzip an XDK for us. + config.setCanBeConsumed(true); + addXdkContentsAttributes(config); + }); + + project.artifacts(artifactHandler -> { + final var location = getXdkContentsDir(); + artifactHandler.add(XDK_CONFIG_NAME_CONTENTS, location, artifact -> { + logger.info("{} Adding outgoing XDK contents artifact to project {} ({}) builtBy {} (dir).", prefix, projectName, location.get(), extractTask.getName()); + artifact.builtBy(extractTask); + artifact.setType(ArtifactTypeDefinition.DIRECTORY_TYPE); + }); + }); + } + + private XtcSourceDirectorySet createXtcSourceDirectorySet(final String parentName, final String parentDisplayName) { + final String name = parentDisplayName + '.' + XTC_LANGUAGE_NAME; + final String displayName = parentDisplayName + ' ' + XTC_LANGUAGE_NAME + " source"; + logger.info("{} Creating XTC source directory set from (parentName: {} parentDisplayName: {}, name: {}, displayName: {})", prefix, parentName, parentDisplayName, name, displayName); + + final ObjectFactory objects = project.getObjects(); + final var xtcSourceDirectorySet = objects.newInstance(DefaultXtcSourceDirectorySet.class, objects.sourceDirectorySet(name, displayName)); + + xtcSourceDirectorySet.getFilter().include("**/*" + XTC_SOURCE_FILE_EXTENSION); + + return xtcSourceDirectorySet; + } + + private void createDefaultSourceSets() { + for (final SourceSet sourceSet : getSourceSets()) { + logger.info("{} Creating adding XTC source directory to inherited Java source set: {}", prefix, sourceSet.getName()); + // Create a source directory set named "xtc" for this existing source set. + final var sourceSetName = sourceSet.getName(); + // Create the xtcSourceDirectorySet + final var xtcSourceDirectorySet = createXtcSourceDirectorySet(sourceSet.getName(), ((DefaultSourceSet)sourceSet).getDisplayName()); + // Create the source set output, so that we can add processed resources and build source code (.xtc module) locations to it. + final SourceSetOutput output = sourceSet.getOutput(); + // Add the "xtc" source set. + sourceSet.getExtensions().add(XtcSourceDirectorySet.class, XTC_LANGUAGE_NAME, xtcSourceDirectorySet); + // Add the directory "src//x" to the source set (convention) + final var srcDir = getXtcSourceDirectoryRootPath(sourceSet); + xtcSourceDirectorySet.srcDir(srcDir); + // Add all sources from the xtc source directory to the sourceSet during resolution. + sourceSet.getAllSource().source(xtcSourceDirectorySet); + // Add output directories for modules (compileXtc output) and resources + // (sourceSet.output.resourcesDir) to the task, so that dependencies will work. + final var outputModules = getXtcCompilerOutputModules(sourceSet); + final var outputResources = getXtcCompilerOutputResourceDir(sourceSet); + logger.info("{} Configured sourceSets.{}.outputModules : {}", prefix, sourceSetName, outputModules); + logger.info("{} Configured sourceSets.{}.outputResources : {}", prefix, sourceSetName, outputResources.get()); + output.dir(outputResources); // TODO is this really correct? We have the resource dir as a special property in the sourceSetOutput already? + output.dir(outputModules); + output.setResourcesDir(outputResources); + } + } + + private void createResolutionStrategy() { + configs.all(config -> { + logger.info("{} Config '{}'; evaluating dependency resolutions", prefix, config.getName()); + config.getResolutionStrategy().eachDependency(dependency -> { + final var request = dependency.getRequested(); + logger.info("{} Config '{}' Requests dependency (artifact: {}, moduleId: {})", prefix, config.getName(), requestToNotation(request), request.getModule()); + }); + }); + } + + // TODO: Shouldn't be really just look in our xdkJavaTools (consumer) and the XDK? (And add the XDK to the javatools consumer config?) + private void createJavaToolsConfig() { + // TODO: The xdk should be an xdkJavaTools provider. Declare as such in ExtractXdkTask. This should remove a large amount of version handling code. + final var xdkJavaTools = configs.register(XDK_CONFIG_NAME_JAVATOOLS_INCOMING, config -> { + config.setDescription("Configuration that resolves and consumes Java bridge/tool dependencies"); + config.setCanBeResolved(true); + config.setCanBeConsumed(false); + addJavaToolsContentsAttributes(config); + }); + + logger.info("{} Created {} config and added dependencies.", prefix, xdkJavaTools.getName()); + } + + private static String xtcModuleLibraryElementName(final Configuration config) { + return XTC_LANGUAGE_NAME + (config.getName().contains("Test") ? "-test" : ""); + } + + private JavaPluginExtension getJavaExtensionContainer() { + /* + * The Java sourceSets and the application of the Java plugin modifies the life cycle. + * We may have to extend the compileClasspath and runtimeClasspath for the Java plugin with XTC + * stuff to get the compilation properly hooked up, but this seems to work right now: + */ + final var container = extensions.findByType(JavaPluginExtension.class); + if (container == null) { + throw buildException("Internal error; was expected to have a Java extension container."); + } + return container; + } + + private static String requestToNotation(final ModuleVersionSelector request) { + return String.format("%s:%s:%s", request.getGroup(), request.getName(), request.getVersion()); + } + + private void checkProjectIsVersioned() { + if (UNSPECIFIED.equalsIgnoreCase(project.getVersion().toString())) { + logger.lifecycle("WARNING: Project {} has unspecified version.", prefix); + } + } +} diff --git a/plugin/src/main/java/org/xtclang/plugin/XtcRunModule.java b/plugin/src/main/java/org/xtclang/plugin/XtcRunModule.java new file mode 100644 index 0000000000..8516dc6bea --- /dev/null +++ b/plugin/src/main/java/org/xtclang/plugin/XtcRunModule.java @@ -0,0 +1,35 @@ +package org.xtclang.plugin; + +import org.gradle.api.provider.ListProperty; +import org.gradle.api.provider.Property; +import org.gradle.api.provider.Provider; + +public interface XtcRunModule { + Property getModuleName(); + + Property getMethodName(); + + ListProperty getModuleArgs(); + + default void moduleArg(final String arg) { + moduleArgs(arg); + } + + void moduleArg(final Provider provider); + + void moduleArgs(String... args); + + void moduleArgs(Iterable args); + + void moduleArgs(Provider> provider); + + void setModuleArgs(Iterable args); + + void setModuleArgs(Provider> provider); + + boolean hasDefaultMethodName(); + + String getDefaultMethodName(); + + boolean validate(); +} diff --git a/plugin/src/main/java/org/xtclang/plugin/XtcRuntimeExtension.java b/plugin/src/main/java/org/xtclang/plugin/XtcRuntimeExtension.java new file mode 100644 index 0000000000..90b91f2a84 --- /dev/null +++ b/plugin/src/main/java/org/xtclang/plugin/XtcRuntimeExtension.java @@ -0,0 +1,28 @@ +package org.xtclang.plugin; + +import org.gradle.api.Action; +import org.gradle.api.provider.ListProperty; + +import java.util.List; + +@SuppressWarnings("unused") // TODO Implement and code coverage test all configurations. +public interface XtcRuntimeExtension extends XtcLauncherTaskExtension { + + XtcRunModule module(Action action); + + ListProperty getModules(); + + void moduleName(String name); + + void moduleNames(String... modules); + + void setModules(List modules); + + void setModules(XtcRunModule... modules); + + void setModuleNames(List moduleNames); + + void setModuleNames(String... moduleNames); + + boolean isEmpty(); +} diff --git a/plugin/src/main/java/org/xtclang/plugin/XtcSourceDirectorySet.java b/plugin/src/main/java/org/xtclang/plugin/XtcSourceDirectorySet.java new file mode 100644 index 0000000000..4ddbeb120e --- /dev/null +++ b/plugin/src/main/java/org/xtclang/plugin/XtcSourceDirectorySet.java @@ -0,0 +1,6 @@ +package org.xtclang.plugin; + +import org.gradle.api.file.SourceDirectorySet; + +public interface XtcSourceDirectorySet extends SourceDirectorySet { +} diff --git a/plugin/src/main/java/org/xtclang/plugin/internal/DefaultXtcCompilerExtension.java b/plugin/src/main/java/org/xtclang/plugin/internal/DefaultXtcCompilerExtension.java new file mode 100644 index 0000000000..9a69e16ce0 --- /dev/null +++ b/plugin/src/main/java/org/xtclang/plugin/internal/DefaultXtcCompilerExtension.java @@ -0,0 +1,61 @@ +package org.xtclang.plugin.internal; + +import org.gradle.api.Project; +import org.gradle.api.file.DirectoryProperty; +import org.gradle.api.provider.Property; +import org.xtclang.plugin.XtcCompilerExtension; + +import javax.inject.Inject; + +public class DefaultXtcCompilerExtension extends DefaultXtcLauncherTaskExtension implements XtcCompilerExtension { + // Even though we have getters, these need to be protected for subclasses to be able to use them in the build DSL + // We still have to have getters, or the plugin validation fails, and we might as well put the input properties on + // those for readability. + protected final Property disableWarnings; + protected final Property isStrict; + protected final Property hasQualifiedOutputName; + protected final Property hasVersionedOutputName; + protected final Property stamp; + protected final Property shouldForceRebuild; + + @Inject + public DefaultXtcCompilerExtension(final Project project) { + super(project); + this.disableWarnings = objects.property(Boolean.class).convention(false); + this.isStrict = objects.property(Boolean.class).convention(false); + this.hasQualifiedOutputName = objects.property(Boolean.class).convention(false); + this.hasVersionedOutputName = objects.property(Boolean.class).convention(false); + this.stamp = objects.property(String.class); + this.shouldForceRebuild = objects.property(Boolean.class).convention(false); + } + + @Override + public Property getDisableWarnings() { + return disableWarnings; + } + + @Override + public Property getStrict() { + return isStrict; + } + + @Override + public Property getQualifiedOutputName() { + return hasQualifiedOutputName; + } + + @Override + public Property getVersionedOutputName() { + return hasVersionedOutputName; + } + + @Override + public Property getXtcVersion() { + return stamp; + } + + @Override + public Property getForceRebuild() { + return shouldForceRebuild; + } +} diff --git a/plugin/src/main/java/org/xtclang/plugin/internal/DefaultXtcExtension.java b/plugin/src/main/java/org/xtclang/plugin/internal/DefaultXtcExtension.java new file mode 100644 index 0000000000..2abfa69c04 --- /dev/null +++ b/plugin/src/main/java/org/xtclang/plugin/internal/DefaultXtcExtension.java @@ -0,0 +1,18 @@ +package org.xtclang.plugin.internal; + +import org.gradle.api.Project; +import org.xtclang.plugin.XtcExtension; + +//TODO: We may want to add things extensions like xtcLangGitHub() here. +public class DefaultXtcExtension implements XtcExtension { + private final Project project; + + public DefaultXtcExtension(final Project project) { + this.project = project; + } + + @Override + public String toString() { + return project.getName() + " XTC extension"; + } +} diff --git a/plugin/src/main/java/org/xtclang/plugin/internal/DefaultXtcLauncherTaskExtension.java b/plugin/src/main/java/org/xtclang/plugin/internal/DefaultXtcLauncherTaskExtension.java new file mode 100644 index 0000000000..64fdcd29f4 --- /dev/null +++ b/plugin/src/main/java/org/xtclang/plugin/internal/DefaultXtcLauncherTaskExtension.java @@ -0,0 +1,123 @@ +package org.xtclang.plugin.internal; + +import org.gradle.api.Project; +import org.gradle.api.logging.Logger; +import org.gradle.api.model.ObjectFactory; +import org.gradle.api.provider.ListProperty; +import org.gradle.api.provider.Property; +import org.gradle.api.provider.Provider; +import org.xtclang.plugin.ProjectDelegate; +import org.xtclang.plugin.XtcLauncherTaskExtension; +import org.xtclang.plugin.XtcPluginUtils; + +import java.io.InputStream; +import java.io.OutputStream; +import java.util.List; + +public abstract class DefaultXtcLauncherTaskExtension implements XtcLauncherTaskExtension { + private static final List DEFAULT_JVM_ARGS = List.of("-ea"); + + protected final Project project; + protected final String prefix; + protected final ObjectFactory objects; + protected final Logger logger; + + protected final ListProperty jvmArgs; + protected final Property isVerbose; + protected final Property isFork; + protected final Property showVersion; + protected final Property useNativeLauncher; + protected final Property stdin; + protected final Property stdout; + protected final Property stderr; + + protected DefaultXtcLauncherTaskExtension(final Project project) { + this.project = project; + this.prefix = ProjectDelegate.prefix(project); + this.objects = project.getObjects(); + this.logger = project.getLogger(); + this.jvmArgs = objects.listProperty(String.class).convention(DEFAULT_JVM_ARGS); + this.isVerbose = objects.property(Boolean.class).convention(false); + this.isFork = objects.property(Boolean.class).convention(true); + this.showVersion = objects.property(Boolean.class).convention(false); + this.useNativeLauncher = objects.property(Boolean.class).convention(false); + this.stdin = objects.property(InputStream.class); + this.stdout = objects.property(OutputStream.class); + this.stderr = objects.property(OutputStream.class); + } + + // TODO: Sort public methods in alphabetical order for all these files, remove where just inheritance that has been added to the superclass already if any are left, and put public methods first. + @Override + public Property getStdin() { + return stdin; + } + + @Override + public Property getStdout() { + return stdout; + } + + @Override + public Property getStderr() { + return stderr; + } + + @Override + public Property getFork() { + return isFork; + } + + @Override + public Property getShowVersion() { + return showVersion; + } + + @Override + public Property getUseNativeLauncher() { + return useNativeLauncher; + } + + @Override + public Property getVerbose() { + return isVerbose; + } + + @Override + public ListProperty getJvmArgs() { + return jvmArgs; + } + + @Override + public void jvmArg(final Provider arg) { + jvmArgs(XtcPluginUtils.singleArgumentIterableProvider(project, arg)); + } + + @Override + public void jvmArgs(final String... args) { + jvmArgs(XtcPluginUtils.argumentArrayToList(args)); + } + + @Override + public void jvmArgs(final Iterable elements) { + jvmArgs.addAll(elements); + } + + @Override + public void jvmArgs(final Provider> provider) { + jvmArgs.addAll(provider); + } + + @Override + public void setJvmArgs(final Iterable elements) { + jvmArgs.set(elements); + } + + @Override + public void setJvmArgs(final Provider> provider) { + jvmArgs.set(provider); + } + + public static boolean hasModifiedJvmArgs(final List jvmArgs) { + return !DEFAULT_JVM_ARGS.equals(jvmArgs); + } +} diff --git a/plugin/src/main/java/org/xtclang/plugin/internal/DefaultXtcRunModule.java b/plugin/src/main/java/org/xtclang/plugin/internal/DefaultXtcRunModule.java new file mode 100644 index 0000000000..c8230ca549 --- /dev/null +++ b/plugin/src/main/java/org/xtclang/plugin/internal/DefaultXtcRunModule.java @@ -0,0 +1,116 @@ +package org.xtclang.plugin.internal; + +import org.gradle.api.Project; +import org.gradle.api.provider.ListProperty; +import org.gradle.api.provider.Property; +import org.gradle.api.provider.Provider; +import org.xtclang.plugin.XtcPluginUtils; +import org.xtclang.plugin.XtcRunModule; + +import javax.inject.Inject; +import java.util.ArrayList; +import java.util.List; + +import static java.util.Collections.emptyList; +import static java.util.Objects.requireNonNull; + +public class DefaultXtcRunModule implements XtcRunModule { + private static final String DEFAULT_METHOD_NAME = "run"; + + private final Project project; + + protected final Property moduleName; // mandatory + protected final Property methodName; // optional but always has a modifiable convention value + protected final ListProperty moduleArgs; // optional but always has a modifiable, initially empty, set of arguments + + @Inject + @SuppressWarnings("unused") + public DefaultXtcRunModule(final Project project) { + this(project, null); + } + + public DefaultXtcRunModule(final Project project, final String moduleName) { + this(project, moduleName, DEFAULT_METHOD_NAME, emptyList()); + } + + public DefaultXtcRunModule(final Project project, final String moduleName, final String moduleMethod, final List moduleArgs) { + this.project = project; + final var objects = project.getObjects(); + this.moduleName = objects.property(String.class); + this.methodName = objects.property(String.class).convention(requireNonNull(moduleMethod)); + this.moduleArgs = objects.listProperty(String.class).value(new ArrayList<>(moduleArgs)); + if (moduleName != null) { + this.moduleName.set(moduleName); + } + } + + @Deprecated //TODO: Figure out a better way to override/resolve dependencies for the run configurations and the tasks that inherit it. + static List getModuleInputs(final XtcRunModule module) { + return List.of(module.getModuleName(), module.getMethodName(), module.getModuleArgs()); + } + + @Override + public Property getModuleName() { + return moduleName; + } + + @Override + public Property getMethodName() { + return methodName; + } + + @Override + public ListProperty getModuleArgs() { + return moduleArgs; + } + + @Override + public void moduleArg(final Provider arg) { + moduleArgs(XtcPluginUtils.singleArgumentIterableProvider(project, arg)); + } + + @Override + public void moduleArgs(final String... args) { + moduleArgs(XtcPluginUtils.argumentArrayToList(args)); + } + + @Override + public void moduleArgs(final Iterable args) { + moduleArgs.addAll(args); + } + + @Override + public void moduleArgs(final Provider> provider) { + moduleArgs.addAll(provider); + } + + @Override + public void setModuleArgs(final Iterable args) { + moduleArgs.set(args); + } + + @Override + public void setModuleArgs(final Provider> provider) { + moduleArgs.set(provider); + } + + @Override + public boolean hasDefaultMethodName() { + return getDefaultMethodName().equals(getMethodName().get()); + } + + @Override + public String getDefaultMethodName() { + return DEFAULT_METHOD_NAME; + } + + @Override + public boolean validate() { + return moduleName.isPresent() && methodName.isPresent(); + } + + @Override + public String toString() { + return '[' + getClass().getSimpleName() + ": moduleName='" + (moduleName.isPresent() ? moduleName.get() : "NONE") + "', methodName='" + (methodName.isPresent() ? methodName.get() : "NONE") + "', moduleArgs='" + getModuleArgs() + "']"; + } +} diff --git a/plugin/src/main/java/org/xtclang/plugin/internal/DefaultXtcRuntimeExtension.java b/plugin/src/main/java/org/xtclang/plugin/internal/DefaultXtcRuntimeExtension.java new file mode 100644 index 0000000000..0c9145d834 --- /dev/null +++ b/plugin/src/main/java/org/xtclang/plugin/internal/DefaultXtcRuntimeExtension.java @@ -0,0 +1,117 @@ +package org.xtclang.plugin.internal; + +import org.gradle.api.Action; +import org.gradle.api.Project; +import org.gradle.api.provider.ListProperty; +import org.xtclang.plugin.XtcRunModule; +import org.xtclang.plugin.XtcRuntimeExtension; + +import javax.inject.Inject; +import java.util.Arrays; +import java.util.List; + +import static java.util.Collections.emptyList; + +public class DefaultXtcRuntimeExtension extends DefaultXtcLauncherTaskExtension implements XtcRuntimeExtension { + // TODO: Make it possible to add to the module path, both here and in the XTC compiler with explicit paths. + // well in the compiler, it just corresponds to the SourceSet and you can edit that already (except for + // maybe the XDK handling behind the scenes). Anyway I'll make it a common property for both environments. + // You never know. + + /** + * Should we enable the debugger for this tas+k? Note that the "classic" Java attach way of debugging + * runs into some complications with Gradle. Here is a good source of information for debugging + * a Gradle build with "classic Java style", and it may well involve reinvoking Gradle with + * --no-daemon and --no-build-cache. See e.g. + * "..." + * + *

+ * We are working on a more seamless approach, triggered on assert:debug in Ecstasy code, and + * controlling the executing process. While Gradle eats and throws away STDIN in lots of + * configurations, the "robust" way to make what we are trying to work, is to redirect + * stdin and use it to talk to a debugger over the console. This requires, e.g. changing + * the scope of a JavaExec so that it runs in the Gradle build process, with daemons enabled. + * To have a "fits all" solution to temporarily fall back on the existing XTC debugger, + * we would also need to create a Gradle compatible console handler and use it to display + * output and request user input (debugger) commands. That should be possible just by starting + * a compile or run task with special changes to configuration (e.g. fork = false, stdin = ...) + *

+ * Ideally, we'd like full control over this in only one place, the build/run system with + * Gradle/Maven, or completely in the XDK implementation. For the latter, which will also + * help us break out and port current functionality to the WIP XTC Language Server, it makes + * sense to add an abstraction layer for communication between the user and the debugger + * process. This interface could run over sockets or whatever is available, and would + * contain one implementation of the interface that implements the current stdout/stdin + * mode, that is the only way to talk to the XTC debugger ATM. + */ + private final ListProperty modules; + + @Inject + public DefaultXtcRuntimeExtension(final Project project) { + super(project); + this.modules = objects.listProperty(XtcRunModule.class).value(emptyList()); + } + + public static XtcRunModule createModule(final Project project, final String name) { + return new DefaultXtcRunModule(project, name); + } + + private XtcRunModule createModule(final String name) { + return createModule(project, name); + } + + private XtcRunModule addModule(final XtcRunModule runModule) { + modules.add(runModule); + return runModule; + } + + @Override + public XtcRunModule module(final Action action) { + final var runModule = project.getObjects().newInstance(DefaultXtcRunModule.class, project); + action.execute(runModule); + logger.info("{} Resolved xtcRunModule configuration: {}", prefix, runModule); + return addModule(runModule); + } + + @Override + public void moduleName(final String name) { + addModule(createModule(name)); + } + + @Override + public void moduleNames(final String... names) { + Arrays.asList(names).forEach(this::moduleName); + } + + @Override + public ListProperty getModules() { + return modules; + } + + @Override + public void setModuleNames(final List moduleNames) { + modules.get().clear(); + moduleNames.forEach(this::moduleName); + } + + @Override + public void setModuleNames(final String... moduleNames) { + setModuleNames(Arrays.asList(moduleNames)); + } + + @Override + public void setModules(final List modules) { + this.modules.get().clear(); + modules.forEach(this::addModule); + } + + @Override + public void setModules(final XtcRunModule... modules) { + setModules(Arrays.asList(modules)); + } + + @Override + public boolean isEmpty() { + return modules.get().isEmpty(); + } +} diff --git a/plugin/src/main/java/org/xtclang/plugin/internal/DefaultXtcSourceDirectorySet.java b/plugin/src/main/java/org/xtclang/plugin/internal/DefaultXtcSourceDirectorySet.java new file mode 100644 index 0000000000..b2ee194dc6 --- /dev/null +++ b/plugin/src/main/java/org/xtclang/plugin/internal/DefaultXtcSourceDirectorySet.java @@ -0,0 +1,15 @@ +package org.xtclang.plugin.internal; + +import org.gradle.api.file.SourceDirectorySet; +import org.gradle.api.internal.file.DefaultSourceDirectorySet; +import org.gradle.api.internal.tasks.DefaultTaskDependencyFactory; +import org.xtclang.plugin.XtcSourceDirectorySet; + +import javax.inject.Inject; + +public abstract class DefaultXtcSourceDirectorySet extends DefaultSourceDirectorySet implements XtcSourceDirectorySet { + @Inject + public DefaultXtcSourceDirectorySet(final SourceDirectorySet sourceDirectorySet) { + super(sourceDirectorySet, DefaultTaskDependencyFactory.withNoAssociatedProject()); + } +} diff --git a/plugin/src/main/java/org/xtclang/plugin/launchers/BuildThreadLauncher.java b/plugin/src/main/java/org/xtclang/plugin/launchers/BuildThreadLauncher.java new file mode 100644 index 0000000000..ac3287d60a --- /dev/null +++ b/plugin/src/main/java/org/xtclang/plugin/launchers/BuildThreadLauncher.java @@ -0,0 +1,130 @@ +package org.xtclang.plugin.launchers; + +import org.gradle.api.Project; +import org.gradle.process.ExecResult; +import org.xtclang.plugin.XtcLauncherTaskExtension; +import org.xtclang.plugin.internal.DefaultXtcLauncherTaskExtension; +import org.xtclang.plugin.launchers.XtcExecResult.XtcExecResultBuilder; +import org.xtclang.plugin.tasks.XtcLauncherTask; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintStream; +import java.lang.reflect.Method; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.Objects; + +public class BuildThreadLauncher> extends XtcLauncher { + /** + * The launcher invocation method, in a variant that cannot do System.exit(), to safeguard the + * "fork = false" configuration, which should be used for debugging purposes only. + */ + private static final String LAUNCH_METHOD_NAME = "launch"; + private static final Class LAUNCH_METHOD_PARAMS = String[].class; + + private final Method main; + + public BuildThreadLauncher(final Project project, final T task) { + super(project, task); + this.main = resolveMethod(task); + } + + @Override + protected boolean validateCommandLine(final CommandLine cmd) { + Objects.requireNonNull(cmd); + final var mainClassName = cmd.getMainClassName(); + final var jvmArgs = cmd.getJvmArgs(); + logger.info("{} WARNING: Task will launch '{}' from its build process. No JavaExec/Exec will be performed.", prefix, mainClassName); + if (DefaultXtcLauncherTaskExtension.hasModifiedJvmArgs(jvmArgs)) { + logger.warn("{} WARNING: Task has non-default JVM args ({}). These will be ignored, as launcher is configured not to fork.", prefix, jvmArgs); + } + return false; + } + + private static PrintStream printStream(final OutputStream out) { + return out instanceof PrintStream ? (PrintStream)out : new PrintStream(out); + } + + @Override + public ExecResult apply(final CommandLine cmd) { + logger.info("{} Launching task {}}", prefix, this); + + validateCommandLine(cmd); + + final var oldIn = System.in; + final var oldOut = System.out; + final var oldErr = System.err; + final var builder = resultBuilder(cmd); + try { + if (hasVerboseLogging()) { + logger.lifecycle("{} WARNING: (equivalent to what we are executing without forking in current thread) JavaExec command: {}", prefix, cmd.toString()); + } + // TODO: Rewrite super.redirectIo so we can reuse it here. That is prettier. Push and pop streams to field? + // (may be even more readable to implement it as a try-with-resources of some kind) + if (task.hasStdinRedirect()) { + System.setIn(task.getStdin().get()); + } + if (task.hasStdoutRedirect()) { + System.setOut(printStream(task.getStdout().get())); + } + if (task.hasStderrRedirect()) { + System.setErr(printStream(task.getStderr().get())); + } + main.invoke(null, (Object)cmd.toList().toArray(new String[0])); + builder.exitValue(0); + } catch (final IllegalAccessException e) { + throw buildException("Failed to invoke '{}.{}' through reflection: {}", main.getDeclaringClass().getName(), main.getName(), e.getMessage()); + } catch (final Throwable t) { + handleThrowable(builder, t); + } finally { + System.setIn(oldIn); + System.setOut(oldOut); + System.setErr(oldErr); + } + return createExecResult(builder); + } + + private void handleThrowable(final XtcExecResultBuilder builder, final Throwable t) { + final var cause = t.getCause(); + if (cause == null) { + throw buildException("Unexpected throwable from invocation to '{}.{}' through reflection: {}", + main.getDeclaringClass().getName(), main.getName(), t.getMessage()); + } + + // Check if cause was a launcher exception. + // + // TODO: This is a rather hacky way of checking the LauncherException. + // We do not want to refer to classes outside the plugin at compile time, because we + // may not always (actually quite seldom) want to bundle the plugin with its javatools.jar. + // Doing so, however, gives us some convenient ways to quickly debug a build, but it + // has a bit of a dependency skew code smell to it, along with the extra copy of the + // javatools, so we prefer to avoid it if can, or at least never assume we have + // compile time access to javatools in the plugin. + final boolean isError = cause.toString().contains("isError=true"); + logger.warn("{} LauncherException caught in {}, isError={}", prefix, getClass().getSimpleName(), isError, cause); + builder.exitValue(isError ? -1 : 0); + if (isError) { + builder.failure(cause); + } + } + + @SuppressWarnings("unused") + private Class dynamicallyLoadJar(final File jar, final String className) throws IOException { + try (final var classLoader = new URLClassLoader(new URL[]{jar.toURI().toURL()})) { + return classLoader.loadClass(className); + } catch (final ClassNotFoundException e) { + throw buildException(e, "Failed to load class from jar '{}': {}", jar, e.getMessage()); + } + } + + @SuppressWarnings("SameParameterValue") + private static Method resolveMethod(final XtcLauncherTask task) { + try { + return Class.forName(task.getJavaLauncherClassName()).getMethod(LAUNCH_METHOD_NAME, LAUNCH_METHOD_PARAMS); + } catch (final ClassNotFoundException | NoSuchMethodException e) { + throw task.buildException(e, "Failed to resolve method '{}' in class '{}': {}.", LAUNCH_METHOD_NAME, task.getJavaLauncherClassName(), e.getMessage()); + } + } +} diff --git a/plugin/src/main/java/org/xtclang/plugin/launchers/ChildProcessLauncher.java b/plugin/src/main/java/org/xtclang/plugin/launchers/ChildProcessLauncher.java new file mode 100644 index 0000000000..d1b76c62d8 --- /dev/null +++ b/plugin/src/main/java/org/xtclang/plugin/launchers/ChildProcessLauncher.java @@ -0,0 +1,6 @@ +package org.xtclang.plugin.launchers; + +@SuppressWarnings("unused") +public class ChildProcessLauncher { + // TODO: Implement me +} diff --git a/plugin/src/main/java/org/xtclang/plugin/launchers/CommandLine.java b/plugin/src/main/java/org/xtclang/plugin/launchers/CommandLine.java new file mode 100644 index 0000000000..fda3500805 --- /dev/null +++ b/plugin/src/main/java/org/xtclang/plugin/launchers/CommandLine.java @@ -0,0 +1,114 @@ +package org.xtclang.plugin.launchers; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +public final class CommandLine { + private final List args; + private final List jvmArgs; + private final String mainClass; + + public CommandLine(final String mainClass, final List jvmArgs) { + this(mainClass, jvmArgs, Collections.emptyList()); + } + + CommandLine(final String mainClass, final List jvmArgs, final List args) { + this.mainClass = mainClass; + this.jvmArgs = Collections.unmodifiableList(jvmArgs); + this.args = new ArrayList<>(args); + } + + public String getMainClassName() { + return mainClass; + } + + public List getJvmArgs() { + return jvmArgs; + } + + public String getIdentifier() { + final int dot = mainClass.lastIndexOf('.'); + return (dot == - 1 || dot == mainClass.length() - 1) ? mainClass : mainClass.substring(dot + 1); + } + + /** + * A boolean argument is not "false" or "true". It's true if it's defined, and if it's false, + * it's not on the command line. For example "-nowarn" means "turn off all warnings", but + * not "-nowarn true" or "-nowarn false" + */ + public void addBoolean(final String name, final boolean value) { + if (value) { + args.add(name); + } + } + + public void add(final String name, final T value) { + args.add(name); + args.add(Objects.requireNonNull(value).toString()); + } + + public void addRepeated(final String name, final Collection values) { + for (final File arg : values) { + add(name, arg.getAbsolutePath()); + } + } + + public void addRaw(final String arg) { + args.add(arg); + } + + public void addRaw(final List argList) { + args.addAll(argList); + } + + public int size() { + return args.size(); + } + + public boolean isEmpty() { + return size() == 0; + } + + public List toList() { + return Collections.unmodifiableList(args); // TODO probably do all allocation in the plugin thru Gradle.Project.objectFactories. + } + + public CommandLine copy() { + return new CommandLine(mainClass, jvmArgs, args); + } + + @Override + public String toString() { + return toString(mainClass, jvmArgs, args, null); + } + + public String toString(final File javaTools) { + return toString(mainClass, jvmArgs, args, javaTools); + } + + public static String toString(final String clazz, final List jvmArgs, final List args, final File javaTools) { + if (args.isEmpty()) { + return "[no arguments]"; + } + + final StringBuilder sb = new StringBuilder("java"); + for (final var arg : jvmArgs) { + sb.append(' ').append(arg.trim()); + } + if (javaTools != null) { + sb.append(" -cp ").append(javaTools.getAbsolutePath()); + } + if (clazz != null) { + sb.append(' ').append(clazz); + } + for (final var arg : args) { + sb.append(' ').append(arg.trim()); + } + + return sb.toString(); + } +} diff --git a/plugin/src/main/java/org/xtclang/plugin/launchers/JavaExecLauncher.java b/plugin/src/main/java/org/xtclang/plugin/launchers/JavaExecLauncher.java new file mode 100644 index 0000000000..2a8bb8ec96 --- /dev/null +++ b/plugin/src/main/java/org/xtclang/plugin/launchers/JavaExecLauncher.java @@ -0,0 +1,145 @@ +package org.xtclang.plugin.launchers; + +import org.gradle.api.Project; +import org.gradle.process.ExecResult; +import org.xtclang.plugin.XtcLauncherTaskExtension; +import org.xtclang.plugin.XtcPluginUtils.FileUtils; +import org.xtclang.plugin.XtcProjectDelegate; +import org.xtclang.plugin.tasks.XtcLauncherTask; + +import java.io.File; +import java.io.IOException; +import java.util.zip.ZipFile; + +import static org.xtclang.plugin.XtcPluginConstants.JAR_MANIFEST_PATH; +import static org.xtclang.plugin.XtcPluginConstants.XDK_CONFIG_NAME_JAVATOOLS_INCOMING; +import static org.xtclang.plugin.XtcPluginUtils.FileUtils.readXdkVersionFromJar; + +// TODO: Add more info to the LauncherException, and if we can reflect it out for the "javatools bundled with +// the plugin" use case, let's do that. One thing I find that I would like very much is if the LauncherException +// threw me the failed launcher command line as well. + +/** + * Launcher logic that runs the XTC launchers from classes on the classpath. + */ +public class JavaExecLauncher> extends XtcLauncher { + public JavaExecLauncher(final Project project, final T task) { + super(project, task); + } + + @Override + public ExecResult apply(final CommandLine cmd) { + logger.info("{} Launching task: {}}", prefix, this); + + final var javaToolsJar = resolveJavaTools(); + if (javaToolsJar == null) { + throw buildException("Failed to resolve 'javatools.jar' in any classpath."); + } + + logger.info("{} {} (launcher: {}); Using 'javatools.jar' in classpath from: {}", prefix, cmd.getIdentifier(), cmd.getClass(), javaToolsJar); + if (hasVerboseLogging()) { + logger.lifecycle("{} JavaExec command (launcher {}): {}", prefix, getClass().getSimpleName(), cmd.toString(javaToolsJar)); + } + + final var builder = resultBuilder(cmd); + return createExecResult(builder.execResult(project.getProject().javaexec(spec -> { + redirectIo(builder, spec); + spec.classpath(javaToolsJar); + spec.getMainClass().set(cmd.getMainClassName()); + spec.args(cmd.toList()); + spec.jvmArgs(cmd.getJvmArgs()); + spec.setIgnoreExitValue(true); + }))); + } + + private File resolveJavaTools() { + // TODO: Way too complicated, just making absolutely sure that we don't mix class paths for e.g. XDK development, and something + // that is a distribution installed locally, thinking one is the other. This can be solved through artifact signing instead. + + /* + * Verify that we have a javatools jar in our classpath. This will probably be simplified later, but is now + * made to overly detailed ensure correctness for semantically versioned artifact declarations being + * substituted by includedBuilds as well as being retrieved from a repository. These two worlds can + * coexist in the XDK build, which bootstraps itself through the plugin from source code. + * + * The XDK build will refer to the Java tools dependencies by their incoming config, which will reflect + * any changes done to Javatools during development. The rest of the world will refer to the "xdk" or + * "xdkDistribution" dependencies, with the same versioned artifact name as parameter, which will + * cause it to be picked from the XDK instead (or from the extracted XDK zip file). + * + * These two worlds can coexist in the XDK build, and are thus tested for correctness. The XDK + * proper uses its "javatools" build, built from the latest source code. The manualTests project + * asks for an XDK dependency for the Java tools instead, verifying that the just built XDK + * resolves correctly. + * + * In the case of the dependency being present in both locations (should not happen, but is theoretically + * possible if something has transitive dependencies), we verify that the jar files retrieved are + * identical binaries. We should probably just switch to checking if exactly one jar file exists and + * fail if there are multiple configurations. However, in order to ensure correctness of all dependencies, + * we keep this code here for now. It will very likely go away in the future, and assume and assert that + * there is only one configuration available to consume, containing the javatools.jar. + */ + final var javaToolsFromConfig = filesFrom(true, XDK_CONFIG_NAME_JAVATOOLS_INCOMING) + .filter(FileUtils::isValidJavaToolsArtifact); + final var javaToolsFromXdk = project.fileTree(XtcProjectDelegate.getXdkContentsDir(project)) + .filter(FileUtils::isValidJavaToolsArtifact); + + logger.info(""" + {} javaToolsFromConfig files: {} + {} javaToolsFromXdk files: {} + """.trim(), prefix, javaToolsFromConfig.getFiles(), prefix, javaToolsFromXdk.getFiles()); + + final File resolvedFromConfig = javaToolsFromConfig.isEmpty() ? null : javaToolsFromConfig.getSingleFile(); + final File resolvedFromXdk = javaToolsFromXdk.isEmpty() ? null : javaToolsFromXdk.getSingleFile(); + if (resolvedFromConfig == null && resolvedFromXdk == null) { + throw buildException("ERROR: Failed to resolve 'javatools.jar' from any configuration or dependency."); + } + + logger.info(""" + {} Check for 'javatools.jar' in {} config and XDK (unpacked zip, or module collection) dependency, if present. + {} Resolved to: [xdkJavaTools: {}, xdkContents: {}] + """.trim(), prefix, XDK_CONFIG_NAME_JAVATOOLS_INCOMING, prefix, resolvedFromConfig, resolvedFromXdk); + + final String versionConfig = readXdkVersionFromJar(resolvedFromConfig); + final String versionXdk = readXdkVersionFromJar(resolvedFromXdk); + if (resolvedFromConfig != null && resolvedFromXdk != null) { + if (!versionConfig.equals(versionXdk) || !areIdenticalFiles(resolvedFromConfig, resolvedFromXdk)) { + logger.warn("{} Different 'javatools.jar' files resolved, preferring the non-XDK version: {}", prefix, resolvedFromConfig.getAbsolutePath()); + return processJar(resolvedFromConfig); + } + } + + if (resolvedFromConfig != null) { + assert resolvedFromXdk == null; + logger.info("{} Resolved unique 'javatools.jar' from config/artifacts/dependencies: {} (version: {})", prefix, resolvedFromConfig.getAbsolutePath(), versionConfig); + return processJar(resolvedFromConfig); + } + + logger.info("{} Resolved unique 'javatools.jar' from XDK: {} (version: {})", prefix, resolvedFromXdk.getAbsolutePath(), versionXdk); + return processJar(resolvedFromXdk); + } + + private boolean areIdenticalFiles(final File f1, final File f2) { + try { + return FileUtils.areIdenticalFiles(f1, f2); + } catch (final IOException e) { + throw buildException("{} Resolved non-identical multiple 'javatools.jar' ('{}' and '{}')", + prefix, f1.getAbsolutePath(), f2.getAbsolutePath()); + } + } + + @SuppressWarnings("UnusedReturnValue") + private boolean checkIsJarFile(final File file) { + try (final ZipFile zip = new ZipFile(file)) { + return zip.getEntry(JAR_MANIFEST_PATH) != null; + } catch (final IOException e) { + throw buildException("Failed to read jar file: '{}' (is the format correct?)", file.getAbsolutePath()); + } + } + + protected File processJar(final File file) { + assert file.exists(); + checkIsJarFile(file); + return file; + } +} diff --git a/plugin/src/main/java/org/xtclang/plugin/launchers/NativeBinaryLauncher.java b/plugin/src/main/java/org/xtclang/plugin/launchers/NativeBinaryLauncher.java new file mode 100644 index 0000000000..44d88a9795 --- /dev/null +++ b/plugin/src/main/java/org/xtclang/plugin/launchers/NativeBinaryLauncher.java @@ -0,0 +1,61 @@ +package org.xtclang.plugin.launchers; + +import org.gradle.api.Project; +import org.gradle.process.ExecResult; +import org.xtclang.plugin.XtcLauncherTaskExtension; +import org.xtclang.plugin.internal.DefaultXtcLauncherTaskExtension; +import org.xtclang.plugin.tasks.XtcLauncherTask; + +import java.io.File; +import java.util.Objects; +import java.util.StringTokenizer; + +@SuppressWarnings("unused") +public class NativeBinaryLauncher> extends XtcLauncher { + + private final String commandName; + + public NativeBinaryLauncher(final Project project, final T task) { + super(project, task); + this.commandName = task.getNativeLauncherCommandName(); + } + + @Override + protected boolean validateCommandLine(final CommandLine cmd) { + final var mainClassName = cmd.getMainClassName(); + final var jvmArgs = cmd.getJvmArgs(); + if (DefaultXtcLauncherTaskExtension.hasModifiedJvmArgs(jvmArgs)) { + logger.warn("{} WARNING: Launcher for mainClassName '{}' has non-default JVM args ({}). These will be ignored, as we are running a native launcher.", prefix, mainClassName, jvmArgs); + } + return super.validateCommandLine(cmd); + } + + private File findOnPath(final String commandName) { + final var path = Objects.requireNonNull(System.getenv("PATH")); + final var st = new StringTokenizer(path, File.pathSeparator); + while (st.hasMoreTokens()) { + final var cmd = new File(st.nextToken(), commandName); + if (cmd.exists() && cmd.canExecute()) { + logger.info("{} Successfully resolved path for command '{}': '{}'", prefix, commandName, cmd.getAbsolutePath()); + return cmd; + } + } + throw buildException("Could not resolve " + commandName + " from system path: " + path); + } + + @Override + public ExecResult apply(final CommandLine cmd) { + logger.info("{} Launching task: {}}", prefix, this); + validateCommandLine(cmd); + if (hasVerboseLogging()) { + logger.lifecycle("{} NativeExec command: {}", prefix, cmd.toString()); + } + final var builder = resultBuilder(cmd); + return createExecResult(builder.execResult(project.exec(spec -> { + redirectIo(builder, spec); + spec.setExecutable(findOnPath(commandName)); + spec.setArgs(cmd.toList()); + spec.setIgnoreExitValue(true); + }))); + } +} diff --git a/plugin/src/main/java/org/xtclang/plugin/launchers/XtcExecResult.java b/plugin/src/main/java/org/xtclang/plugin/launchers/XtcExecResult.java new file mode 100644 index 0000000000..24ceb1340a --- /dev/null +++ b/plugin/src/main/java/org/xtclang/plugin/launchers/XtcExecResult.java @@ -0,0 +1,194 @@ +package org.xtclang.plugin.launchers; + +import org.gradle.process.ExecResult; +import org.gradle.process.internal.ExecException; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +public final class XtcExecResult implements ExecResult { + private final int exitValue; + private final Throwable failure; + private final String out; + private final String err; + + XtcExecResult(final int exitValue, final Throwable failure, final String out, final String err) { + this.exitValue = exitValue; + this.failure = failure; + // TODO: Remember to use the ExecResult.stdoutContents variables instead of the horrible stuff we do with byte arrays. + this.out = out == null ? "" : out; + this.err = err == null ? "" : err; + } + + public static XtcExecResultBuilder builder(final Class launcherClass, final CommandLine cmd) { + return new XtcExecResultBuilder(launcherClass, cmd); + } + + @SuppressWarnings("unused") + public boolean isSuccessful() { + return exitValue == 0; + } + + @SuppressWarnings("unused") + public String getOutputStdout() { + return out; + } + + @SuppressWarnings("unused") + public String getOutputStderr() { + return err; + } + + @SuppressWarnings("unused") + public Throwable getFailure() { + return failure; + } + + @Override + public int getExitValue() { + return exitValue; + } + + public boolean hasOutputs() { + return !out.isEmpty() || !err.isEmpty(); + } + + @Override + public ExecResult assertNormalExitValue() throws ExecException { + if (exitValue != 0) { + throw new ExecException("XTC Launcher exited with non-zero exit code: " + exitValue, failure); + } + return this; + } + + @Override + public ExecResult rethrowFailure() throws ExecException { + if (failure != null) { + throw new ExecException("XTC Launcher exited with exception: " + failure.getMessage(), failure); + } + return this; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(getClass().getSimpleName()).append(": [exitValue=").append(exitValue); + if (failure != null) { + sb.append(", failure=").append(failure); + } else { + sb.append(", SUCCESS (no failure object)"); + } + sb.append(']'); + return sb.toString(); + } + + /** + * Extension of output stream that also flushes and echoes to console, as well as + * the cached stream. TODO: Make configurable. + */ + private static class XtcExecOutputStream extends OutputStream { + private final OutputStream out; + private final boolean alwaysFlush; + + @SuppressWarnings("unused") + XtcExecOutputStream() { + this(false); + } + + XtcExecOutputStream(final boolean alwaysFlush) { + this.out = new ByteArrayOutputStream(); + this.alwaysFlush = alwaysFlush; + } + + @Override + public void write(final int b) throws IOException { + out.write(b); + if (alwaysFlush) { + out.flush(); + } + } + + @Override + public String toString() { + return out.toString(); + } + } + + /** + * Subclass to a Gradle Exec Result. + */ + public static final class XtcExecResultBuilder { + private final Class launcherClass; + private final CommandLine cmd; + private final XtcExecOutputStream out; + private final XtcExecOutputStream err; + + private int exitValue; + private boolean hasExitValue; + private Throwable failure; + + private XtcExecResultBuilder(final Class launcherClass, final CommandLine cmd) { + this.launcherClass = launcherClass; + this.cmd = cmd; + this.hasExitValue = false; // has exit value been set? + // Todo we should no have to care about streams, right? Or use the ones given? + this.out = new XtcExecOutputStream(false); + this.err = new XtcExecOutputStream(true); + } + + boolean hasExitValue() { + return hasExitValue; + } + + @SuppressWarnings("unused") + boolean isSuccessful() { + return hasExitValue && exitValue == 0; + } + + @SuppressWarnings("unused") + CommandLine getCommandLine() { + return cmd; + } + + OutputStream getOut() { + return out; + } + + OutputStream getErr() { + return err; + } + + @SuppressWarnings("UnusedReturnValue") + XtcExecResultBuilder exitValue(final int exitValue) { + this.exitValue = exitValue; + this.hasExitValue = true; + return this; + } + + @SuppressWarnings("UnusedReturnValue") + XtcExecResultBuilder failure(final Throwable failure) { + this.failure = new ExecException(launcherClass.getSimpleName() + ' ' + failure.getMessage(), failure); + return this; + } + + @SuppressWarnings("UnusedReturnValue") + XtcExecResultBuilder execResult(final ExecResult execResult) { + exitValue(execResult.getExitValue()); + try { + execResult.rethrowFailure(); + execResult.assertNormalExitValue(); + } catch (final ExecException e) { + failure(e); + } + return this; + } + + private static String outputAsString(final XtcExecOutputStream out) { + return out.toString(); + } + + XtcExecResult build() { + return new XtcExecResult(exitValue, failure, outputAsString(out), outputAsString(err)); + } + } +} diff --git a/plugin/src/main/java/org/xtclang/plugin/launchers/XtcLauncher.java b/plugin/src/main/java/org/xtclang/plugin/launchers/XtcLauncher.java new file mode 100644 index 0000000000..426597f946 --- /dev/null +++ b/plugin/src/main/java/org/xtclang/plugin/launchers/XtcLauncher.java @@ -0,0 +1,64 @@ +package org.xtclang.plugin.launchers; + +import org.gradle.api.Project; +import org.gradle.process.BaseExecSpec; +import org.gradle.process.ExecResult; +import org.xtclang.plugin.ProjectDelegate; +import org.xtclang.plugin.XtcLauncherTaskExtension; +import org.xtclang.plugin.tasks.XtcLauncherTask; + +import static org.xtclang.plugin.launchers.XtcExecResult.XtcExecResultBuilder; + +public abstract class XtcLauncher> extends ProjectDelegate { + protected final T task; + protected final String taskName; + + protected XtcLauncher(final Project project, final T task) { + super(project); + this.task = task; + this.taskName = task.getName(); + } + + @Override + public String toString() { + return String.format("%s (launcher='%s', task='%s', fork=%s, native=%s).", prefix, getClass().getSimpleName(), taskName, isFork(), isNativeLauncher()); + } + + protected boolean isFork() { + return task.getFork().get(); + } + + protected boolean isNativeLauncher() { + return task.getUseNativeLauncher().get(); + } + + protected void redirectIo(final XtcExecResultBuilder builder, final BaseExecSpec spec) { + // TODO, simplify, just send a stream setter for the various streams or our own class based on the existing ExecResult.contentsOfOutput* or something. + if (task.hasStdinRedirect()) { + spec.setStandardInput(task.getStdin().get()); + } + if (task.hasStdoutRedirect()) { + spec.setStandardOutput(task.getStdout().get()); + } + if (task.hasStderrRedirect()) { + spec.setErrorOutput(task.getStderr().get()); + } + } + + protected XtcExecResult createExecResult(final XtcExecResultBuilder builder) { + assert builder.hasExitValue(); + // TODO: System.exit callback if we are running in the builder thread, or things get nasty. + final var result = builder.build(); + assert result.isSuccessful() || result.getFailure() != null : "Should always have a failure for an XtcExecResult"; + return result; + } + + protected final XtcExecResultBuilder resultBuilder(final CommandLine cmd) { + return XtcExecResult.builder(getClass(), cmd); + } + + @SuppressWarnings("UnusedReturnValue") + protected boolean validateCommandLine(final CommandLine cmd) { + return true; + } +} diff --git a/plugin/src/main/java/org/xtclang/plugin/tasks/XtcCompileTask.java b/plugin/src/main/java/org/xtclang/plugin/tasks/XtcCompileTask.java new file mode 100644 index 0000000000..ddee455720 --- /dev/null +++ b/plugin/src/main/java/org/xtclang/plugin/tasks/XtcCompileTask.java @@ -0,0 +1,264 @@ +package org.xtclang.plugin.tasks; + +import kotlin.Pair; +import org.gradle.api.file.Directory; +import org.gradle.api.provider.ListProperty; +import org.gradle.api.provider.Property; +import org.gradle.api.provider.Provider; +import org.gradle.api.tasks.CacheableTask; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.InputDirectory; +import org.gradle.api.tasks.Internal; +import org.gradle.api.tasks.Optional; +import org.gradle.api.tasks.OutputDirectory; +import org.gradle.api.tasks.PathSensitive; +import org.gradle.api.tasks.PathSensitivity; +import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.TaskAction; +import org.xtclang.plugin.XtcCompilerExtension; +import org.xtclang.plugin.XtcPluginUtils; +import org.xtclang.plugin.XtcPluginUtils.FileUtils; +import org.xtclang.plugin.XtcProjectDelegate; +import org.xtclang.plugin.launchers.CommandLine; + +import javax.inject.Inject; +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import static org.xtclang.plugin.XtcPluginConstants.XTC_COMPILER_CLASS_NAME; +import static org.xtclang.plugin.XtcPluginConstants.XTC_COMPILER_LAUNCHER_NAME; + +@CacheableTask +public class XtcCompileTask extends XtcSourceTask implements XtcCompilerExtension { + // Per-task configuration properties only. + private final ListProperty outputFilenames; + + // Default values for inputs and outputs below are inherited from extension, can be reset per task + protected final Property disableWarnings; + protected final Property isStrict; + protected final Property hasQualifiedOutputName; + protected final Property hasVersionedOutputName; + protected final Property xtcVersion; + protected final Property shouldForceRebuild; + + /** + * Create an XTC Compile task. This goes through the Gradle build script, and task creation through + * the project ObjectFactory. + *

+ * The reason we need the @Inject is that the Kotlin compiler adds a secret parameter to the generated + * constructor, if the class uses variables from the build script. That parameter is a reference to the + * enclosing build script. As a result, the task needs to have the @Inject annotation in the constructor + * so that Gradle will correctly instantiate the task with build script reference (which is what happens + * when you instantiate it from the ObjectFactory in a project): + */ + @Inject + public XtcCompileTask(final XtcProjectDelegate delegate, final String taskName, final SourceSet sourceSet) { + super(delegate, taskName, sourceSet); + + // The outputFilenames property only exists in the compile task, not in the compile configuration. + this.outputFilenames = objects.listProperty(String.class).value(new ArrayList<>()); + + // Conventions inherited from extension; can be reset on a per-task basis, of course. + this.disableWarnings = objects.property(Boolean.class).convention(ext.getDisableWarnings()); + this.isStrict = objects.property(Boolean.class).convention(ext.getStrict()); + this.hasQualifiedOutputName = objects.property(Boolean.class).convention(ext.getQualifiedOutputName()); + this.hasVersionedOutputName = objects.property(Boolean.class).convention(ext.getVersionedOutputName()); + this.shouldForceRebuild = objects.property(Boolean.class).convention(ext.getForceRebuild()); + this.xtcVersion = objects.property(String.class).convention(ext.getXtcVersion()); + } + + @Internal + @Override + public final String getNativeLauncherCommandName() { + return XTC_COMPILER_LAUNCHER_NAME; + } + + @Internal + @Override + public final String getJavaLauncherClassName() { + return XTC_COMPILER_CLASS_NAME; + } + + /** + * Add an output Filename mapping. + * TODO Why does IntelliJ think these are unused? Check that it doesn't lead to any unknown dependency problems for the Plugin. + */ + public void outputFilename(final String from, final String to) { + outputFilenames.add(from); + outputFilenames.add(to); + } + + public void outputFilename(final Pair pair) { + outputFilenames.add(pair.getFirst()); + outputFilenames.add(pair.getSecond()); + } + + public void outputFilename(final Provider from, final Provider to) { + outputFilenames.add(from); + outputFilenames.add(to); + } + + @Input + ListProperty getOutputFilenames() { + return outputFilenames; + } + + @InputDirectory + @PathSensitive(PathSensitivity.ABSOLUTE) + Provider getInputXdkContents() { + return delegate.getXdkContentsDir(); + } + + @OutputDirectory + Provider getOutputXtcModules() { + return delegate.getXtcCompilerOutputDirModules(sourceSet); + } + + @Input + @Override + public Property getQualifiedOutputName() { + return hasQualifiedOutputName; + } + + @Input + @Override + public Property getVersionedOutputName() { + return hasVersionedOutputName; + } + + @Optional + @Input + @Override + public Property getXtcVersion() { + return xtcVersion; + } + + @Input + @Override + public Property getStrict() { + return isStrict; + } + + @Input + @Override + public Property getForceRebuild() { + return shouldForceRebuild; + } + + @Input + @Override + public Property getDisableWarnings() { + return disableWarnings; + } + + @OutputDirectory + Provider getOutputDirectory() { + // TODO We can make this configurable later. + return delegate.getXtcCompilerOutputDirModules(sourceSet); + } + + @TaskAction + public void compile() { + start(); + + final var args = new CommandLine(XTC_COMPILER_CLASS_NAME, getJvmArgs().get()); + + if (getForceRebuild().get()) { + logger.warn("{} WARNING: Force Rebuild was set; touching everything in sourceSet '{}' and its resources.", prefix, sourceSet.getName()); + touchAllSource(); // The source set remains the same, but hopefully doing this "touch" as the first executable action will still be before computation of changes. + } + + File outputDir = delegate.getXtcCompilerOutputDirModules(sourceSet).get().getAsFile(); + args.add("-o", outputDir.getAbsolutePath()); + + logger.info("{} Output directory for {} is : {}", prefix, sourceSet.getName(), outputDir); + + sourceSet.getResources().getSrcDirs().forEach(dir -> { + logger.info("{} Resolving resource dir (build): '{}'.", prefix, dir); + if (!dir.exists()) { + logger.info("{} Resource does not exist: '{}' (ignoring passing as input to compiler)", prefix, dir); + } else { + logger.info("{} Adding resource: {}", prefix, dir); + args.add("-r", dir.getAbsolutePath()); + } + }); + + args.addBoolean("--version", getShowVersion().get()); + args.addBoolean("--rebuild", getForceRebuild().get()); + args.addBoolean("--nowarn", getDisableWarnings().get()); + args.addBoolean("--verbose", getIsVerbose().get()); + args.addBoolean("--strict", getStrict().get()); + args.addBoolean("--qualify", getQualifiedOutputName().get()); + // If xtcVersion is set, we stamp that, otherwise we ignore it for now. It may be that we should stamp it + // as the xcc version used to compile if no flag is given? + final String moduleVersion = resolveModuleVersion(); + if (moduleVersion != null) { + if (delegate.hasVerboseLogging()) { + logger.lifecycle("{} Stamping XTC module with version: '{}'", prefix, moduleVersion); + } + args.add("--set-version", moduleVersion); + } + args.addRepeated("-L", resolveXtcModulePath()); + final var sourceFiles = resolveXtcSourceFiles().stream().map(File::getAbsolutePath).toList(); + if (sourceFiles.isEmpty()) { + logger.warn("{} No source file found for source set: '{}'", prefix, sourceSet.getName()); + } + sourceFiles.forEach(args::addRaw); + + final var launcher = createLauncher(); + handleExecResult(launcher.apply(args)); + finalizeOutputs(); + // TODO outputFilename default task property? + } + + private String resolveModuleVersion() { + // TODO: We need to tell the plugin, when we build it, which version it has from the catalog. This is actually the XTC artifact that needs to be asked its version. The launcher? the xdk dependency? Figure this one out. + if (getXtcVersion().isPresent()) { + return getXtcVersion().get(); + } + return null; + } + + private void finalizeOutputs() { + project.fileTree(getOutputXtcModules()).filter(FileUtils::isValidXtcModule).forEach(oldFile -> { + final String oldName = oldFile.getName(); + final String newName = resolveOutputFilename(oldName); + if (oldName.equals(newName)) { + logger.info("{} Finalizing compiler output XTC binary filename: '{}'", prefix, oldName); + } else { + final File newFile = new File(oldFile.getParentFile(), newName); + logger.info("{} Changing and finalizing compiler output XTC filename: '{}' to '{}'", prefix, oldName, newName); + logger.info("{} File tree scan: {} should be renamed to {}", delegate, oldFile, newFile); + if (!oldFile.renameTo(newFile)) { + // TODO does this update the output? Seems like it. Write a unit test. + throw buildException("Failed to rename '{}' to '{}", oldFile, newFile); + } + } + }); + } + + private Set resolveXtcSourceFiles() { + final var resolvedSources = getSource().filter(this::isTopLevelXtcSourceFile).getFiles(); + logger.info("{} Resolved top level sources (should be module definitions, or XTC will fail later): {}", prefix, resolvedSources); + return resolvedSources; + } + + private Set resolveXtcModulePath() { + return resolveModulePath(getInputDeclaredDependencyModules()); + } + + private String resolveOutputFilename(final String from) { + final List list = outputFilenames.get(); + for (int i = 0; i < list.size(); i += 2) { + final String key = list.get(i); + final String value = list.get(i + 1); + if (key.equals(from)) { + return value; + } + } + return from; + } +} + diff --git a/plugin/src/main/java/org/xtclang/plugin/tasks/XtcDefaultTask.java b/plugin/src/main/java/org/xtclang/plugin/tasks/XtcDefaultTask.java new file mode 100644 index 0000000000..20fc0c9f11 --- /dev/null +++ b/plugin/src/main/java/org/xtclang/plugin/tasks/XtcDefaultTask.java @@ -0,0 +1,70 @@ +package org.xtclang.plugin.tasks; + +import org.gradle.api.DefaultTask; +import org.gradle.api.Project; +import org.gradle.api.logging.Logger; +import org.gradle.api.model.ObjectFactory; +import org.gradle.api.tasks.Internal; +import org.xtclang.plugin.ProjectDelegate; +import org.xtclang.plugin.XtcBuildRuntimeException; +import org.xtclang.plugin.XtcProjectDelegate; + +// TODO: This is intended a common superclass to avoid the messy delegate pattern. +// We also put XTC launcher common logic in here, like e.g. fork, jvmArgs and such. + +// TODO: We'd like our tasks to have the same kind of extension pattern as the XtcProjectDelegate +// now that we have a working inheritance hierarchy no longer constrained to multiple inheritance +// or various Gradle APIs. +public abstract class XtcDefaultTask extends DefaultTask { + // TODO gradually remove the delegate and distribute the logic to its correct places in the "normal" Gradle plugin and DSL APIs and implementations. + protected final XtcProjectDelegate delegate; + protected final Project project; + protected final String prefix; + protected final String projectName; + protected final ObjectFactory objects; + protected final String taskName; + protected final Logger logger; + + private boolean isResolvable; + + protected XtcDefaultTask(final XtcProjectDelegate delegate, final String taskName) { + this.delegate = delegate; // TODO get rid of delegates. + this.taskName = taskName; + this.project = delegate.getProject(); + this.projectName = project.getName(); + this.objects = project.getObjects(); + this.logger = project.getLogger(); + assert projectName.equals(delegate.getProjectName()) : "Delegate field mismatch for projectName."; + this.prefix = ProjectDelegate.prefix(projectName, taskName); + } + + protected void start() { + isResolvable = true; + } + + @Internal + protected boolean isResolvable() { + return isResolvable; + } + + protected void checkResolvable() { + checkResolvable(this); + } + + @SuppressWarnings("UnusedReturnValue") + protected T checkResolvable(final T configData) { + // Used to implement sanity checks that we have started a TaskAction before resolving configuration contents. + if (!isResolvable()) { + throw buildException("Task '{}' attempts to use configuration before it is fully resolved.", taskName); + } + return configData; + } + + public XtcBuildRuntimeException buildException(final String msg, final Object... args) { + return buildException(null, msg, args); + } + + public XtcBuildRuntimeException buildException(final Throwable t, final String msg, final Object... args) { + return delegate.buildException(t, msg, args); + } +} diff --git a/plugin/src/main/java/org/xtclang/plugin/tasks/XtcExtractXdkTask.java b/plugin/src/main/java/org/xtclang/plugin/tasks/XtcExtractXdkTask.java new file mode 100644 index 0000000000..d13e2919d1 --- /dev/null +++ b/plugin/src/main/java/org/xtclang/plugin/tasks/XtcExtractXdkTask.java @@ -0,0 +1,81 @@ +package org.xtclang.plugin.tasks; + +import org.gradle.api.file.Directory; +import org.gradle.api.file.FileCollection; +import org.gradle.api.file.RelativePath; +import org.gradle.api.provider.Provider; +import org.gradle.api.tasks.CacheableTask; +import org.gradle.api.tasks.InputFiles; +import org.gradle.api.tasks.OutputDirectory; +import org.gradle.api.tasks.PathSensitive; +import org.gradle.api.tasks.PathSensitivity; +import org.gradle.api.tasks.TaskAction; +import org.xtclang.plugin.XtcProjectDelegate; + +import javax.inject.Inject; +import java.io.File; + +import static org.xtclang.plugin.XtcPluginConstants.XDK_CONFIG_NAME_INCOMING; +import static org.xtclang.plugin.XtcPluginConstants.XDK_CONFIG_NAME_INCOMING_ZIP; +import static org.xtclang.plugin.XtcPluginConstants.XDK_EXTRACT_TASK_NAME; +import static org.xtclang.plugin.XtcPluginConstants.XDK_JAVATOOLS_ARTIFACT_ID; +import static org.xtclang.plugin.XtcPluginConstants.XDK_JAVATOOLS_ARTIFACT_SUFFIX; +import static org.xtclang.plugin.XtcPluginConstants.XDK_VERSION_PATH; +import static org.xtclang.plugin.XtcPluginConstants.XTC_MODULE_FILE_EXTENSION; +import static org.xtclang.plugin.XtcPluginUtils.FileUtils.getFileExtension; + +@CacheableTask +public abstract class XtcExtractXdkTask extends XtcDefaultTask { + private static final String XDK_ARCHIVE_DEFAULT_EXTENSION = "zip"; + + @Inject + public XtcExtractXdkTask(final XtcProjectDelegate project) { + super(project, XDK_EXTRACT_TASK_NAME); + } + + private static boolean isXdkArchive(final File file) { + return XDK_ARCHIVE_DEFAULT_EXTENSION.equals(getFileExtension(file)); + } + + @InputFiles + @PathSensitive(PathSensitivity.ABSOLUTE) + FileCollection getInputXdkArchive() { + return delegate.filesFrom(XDK_CONFIG_NAME_INCOMING_ZIP, XDK_CONFIG_NAME_INCOMING); + } + + @OutputDirectory + Provider getOutputXtcModules() { + return delegate.getXdkContentsDir(); + } + + @TaskAction + public void extractXdk() { + start(); + + // The task is configured at this point. We should indeed have found a zip archive from some xdkDistributionProvider somewhere. + final var archives = delegate + .filesFrom(true, XDK_CONFIG_NAME_INCOMING_ZIP, XDK_CONFIG_NAME_INCOMING) + .filter(XtcExtractXdkTask::isXdkArchive); + + if (archives.isEmpty()) { + logger.info("{} Project does NOT depend on the XDK; {} is a nop.", prefix, taskName); + return; + } + + // If there are no archives, we do not depend on the XDK. + final var archiveFile = archives.getSingleFile(); + project.copy(config -> { + logger.info("{} CopySpec: XDK archive file dependency: {}", prefix, archiveFile); + config.from(project.zipTree(archiveFile)); + config.include( + "**/*." + XTC_MODULE_FILE_EXTENSION, + "**/" + XDK_JAVATOOLS_ARTIFACT_ID + '*' + XDK_JAVATOOLS_ARTIFACT_SUFFIX, + XDK_VERSION_PATH); + config.eachFile(fileCopyDetails -> fileCopyDetails.setRelativePath(new RelativePath(true, fileCopyDetails.getName()))); + config.setIncludeEmptyDirs(false); + config.into(getOutputXtcModules()); + }); + + logger.info("{} Finished unpacking XDK archive: {} -> {}.", prefix, archiveFile.getAbsolutePath(), getOutputXtcModules().get()); + } +} diff --git a/plugin/src/main/java/org/xtclang/plugin/tasks/XtcLauncherTask.java b/plugin/src/main/java/org/xtclang/plugin/tasks/XtcLauncherTask.java new file mode 100644 index 0000000000..b1687b0c2a --- /dev/null +++ b/plugin/src/main/java/org/xtclang/plugin/tasks/XtcLauncherTask.java @@ -0,0 +1,339 @@ +package org.xtclang.plugin.tasks; + +import org.gradle.api.file.Directory; +import org.gradle.api.file.FileCollection; +import org.gradle.api.provider.ListProperty; +import org.gradle.api.provider.Property; +import org.gradle.api.provider.Provider; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.InputFiles; +import org.gradle.api.tasks.Internal; +import org.gradle.api.tasks.Optional; +import org.gradle.api.tasks.PathSensitive; +import org.gradle.api.tasks.PathSensitivity; +import org.gradle.api.tasks.SourceSet; +import org.gradle.process.ExecResult; +import org.xtclang.plugin.XtcLauncherTaskExtension; +import org.xtclang.plugin.XtcProjectDelegate; +import org.xtclang.plugin.launchers.BuildThreadLauncher; +import org.xtclang.plugin.launchers.JavaExecLauncher; +import org.xtclang.plugin.launchers.NativeBinaryLauncher; +import org.xtclang.plugin.launchers.XtcLauncher; + +import java.io.File; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import static java.util.Objects.requireNonNull; +import static org.xtclang.plugin.XtcPluginConstants.EMPTY_FILE_COLLECTION; +import static org.xtclang.plugin.XtcPluginConstants.XDK_CONFIG_NAME_CONTENTS; +import static org.xtclang.plugin.XtcPluginConstants.XDK_CONFIG_NAME_JAVATOOLS_INCOMING; +import static org.xtclang.plugin.XtcPluginConstants.XTC_CONFIG_NAME_MODULE_DEPENDENCY; +import static org.xtclang.plugin.XtcPluginConstants.XTC_LANGUAGE_NAME; +import static org.xtclang.plugin.XtcPluginUtils.FileUtils.isValidXtcModule; +import static org.xtclang.plugin.XtcPluginUtils.argumentArrayToList; +import static org.xtclang.plugin.XtcPluginUtils.capitalize; +import static org.xtclang.plugin.XtcPluginUtils.singleArgumentIterableProvider; +import static org.xtclang.plugin.XtcProjectDelegate.incomingXtcModuleDependencies; + +/** + * Abstract class that represents and XTC Launcher execution (i.e. Compiler, Runner, Disassembler etc.), + * anything that goes through the XTC Launcher to spawn or call different processes + */ +public abstract class XtcLauncherTask extends XtcDefaultTask implements XtcLauncherTaskExtension { + protected final SourceSet sourceSet; + + // All inherited from launcher task extension and turned into input + protected final Property stdin; + protected final Property stdout; + protected final Property stderr; + protected final ListProperty jvmArgs; + protected final Property isVerbose; + protected final Property isFork; + protected final Property showVersion; + protected final Property useNativeLauncher; + + protected final E ext; + + protected XtcLauncherTask(final XtcProjectDelegate delegate, final String taskName, final SourceSet sourceSet, final E ext) { + super(delegate, taskName); + this.sourceSet = sourceSet; + this.ext = ext; + this.stdin = objects.property(InputStream.class); + this.stdout = objects.property(OutputStream.class); + this.stderr = objects.property(OutputStream.class); + if (ext.getStdin().isPresent()) { + stdin.set(ext.getStdin()); + } + if (ext.getStdout().isPresent()) { + stdout.set(ext.getStdout()); + } + if (ext.getStderr().isPresent()) { + stderr.set(ext.getStderr()); // TODO maybe rename the properties to standardOutput, errorOutput etc to conform to Gradle name standard. Right now we clearly want them to be separated from any defaults, though, so we know our launcher tasks pick the correct configured streams. + } + this.jvmArgs = objects.listProperty(String.class).convention(ext.getJvmArgs()); + this.isVerbose = objects.property(Boolean.class).convention(ext.getVerbose()); + this.isFork = objects.property(Boolean.class).convention(ext.getFork()); + this.showVersion = objects.property(Boolean.class).convention(ext.getShowVersion()); + this.useNativeLauncher = objects.property(Boolean.class).convention(ext.getUseNativeLauncher()); + } + + @Internal + protected E getExtension() { + return ext; + } + + @Optional + @InputFiles + @PathSensitive(PathSensitivity.ABSOLUTE) + FileCollection getInputDeclaredDependencyModules() { + return delegate.filesFrom(incomingXtcModuleDependencies(sourceSet)); // xtcModule and xtcModuleTest dependencies declared in the project dependency { scope section + } + + @InputFiles + @PathSensitive(PathSensitivity.ABSOLUTE) + FileCollection getInputXtcJavaToolsConfig() { + return project.files(project.getConfigurations().getByName(XDK_CONFIG_NAME_JAVATOOLS_INCOMING)); + } + + public boolean hasStdinRedirect() { + return stdin.isPresent(); + } + + public boolean hasStdoutRedirect() { + return stdout.isPresent(); + } + + public boolean hasStderrRedirect() { + return stderr.isPresent(); + } + + @SuppressWarnings("unused") + public boolean hasOutputRedirects() { + return hasStdoutRedirect() || hasStderrRedirect(); + } + + @Optional + @Input + public Property getStdin() { + return stdin; + } + + @Optional + @Input + public Property getStdout() { + return stdout; + } + + @Optional + @Input + public Property getStderr() { + return stderr; + } + + @Override + public void jvmArg(final Provider arg) { + jvmArgs(singleArgumentIterableProvider(project, arg)); + } + + @Override + public void jvmArgs(final String... args) { + jvmArgs.addAll(argumentArrayToList(args)); + } + + @Override + public void jvmArgs(final Iterable args) { + jvmArgs.addAll(args); + } + + @Override + public void jvmArgs(final Provider> provider) { + jvmArgs.addAll(provider); + } + + @Override + public void setJvmArgs(final Iterable elements) { + jvmArgs.set(elements); + } + + @Override + public void setJvmArgs(final Provider> provider) { + jvmArgs.set(provider); + } + + @Input + public Property getVerbose() { + return isVerbose; + } + + @Input + public Property getFork() { + return isFork; + } + + @Input + public Property getShowVersion() { + return showVersion; + } + + @Input + public Property getUseNativeLauncher() { + return useNativeLauncher; + } + + @Optional + @Input + public ListProperty getJvmArgs() { + return jvmArgs; + } + + @Input + public Property getIsVerbose() { + return isVerbose; + } + + @Internal + public abstract String getJavaLauncherClassName(); + + @Internal + public abstract String getNativeLauncherCommandName(); + + protected ExecResult handleExecResult(final ExecResult result) { + final int exitValue = result.getExitValue(); + if (exitValue != 0) { + getLogger().error("{} terminated abnormally (exitValue: {}). Rethrowing exception.", prefix, exitValue); + } + result.rethrowFailure(); + result.assertNormalExitValue(); + return result; + } + + protected XtcLauncher> createLauncher() { + if (getUseNativeLauncher().get()) { + logger.info("{} Created XTC launcher: native executable.", prefix); + return new NativeBinaryLauncher<>(project, this); + } else if (getFork().get()) { + logger.info("{} Created XTC launcher: Java process forked from build.", prefix); + return new JavaExecLauncher<>(project, this); + } else { + logger.warn("{} WARNING: Created XTC launcher: Running launcher in the same thread as the build process. This is not recommended for production use.", prefix); + return new BuildThreadLauncher<>(project, this); + } + } + + protected Set resolveModulePath(final FileCollection inputXtcModules) { + return resolveModulePath(inputXtcModules, false); + //final var modulePathFiles = resolveModulePath(inputXtcModules, false); + //final var modulePathFilesAllSource = resolveModulePath(inputXtcModules, true); + //System.err.println(" ** " + modulePathFiles); + //System.err.println(" ** " + modulePathFilesAllSource); + //assert modulePathFiles.equals(modulePathFilesAllSource) : "An XTC Task should never resolve all project source sets differently than its own source set."; + //return modulePathFilesAllSource; + } + + @SuppressWarnings("SameParameterValue") + private Set resolveModulePath(final FileCollection inputXtcModules, final boolean includeAllSourceSets) { + logger.info("{} Adding RESOLVED configurations from: {}", prefix, inputXtcModules.getFiles()); + final var map = new HashMap>(); + + // All xtc modules and resources from our xtcModule dependencies declared in the project + map.put(XTC_CONFIG_NAME_MODULE_DEPENDENCY, resolveFiles(inputXtcModules)); + + // All contents of the XDK. We can reduce that to a directory, since we know the structure, and that it's one directory + map.put(XDK_CONFIG_NAME_CONTENTS, resolveDirectories(delegate.getXdkContentsDir())); + + // TODO: It's probably always enough to traverse the per-task sourceSet, and will save time. + // The option to iterate over all source sets is still there for completeness, but will most + // likely go away. ATM we just want to merge a working Gradle XTC Plugin that handles dependencies + // in a well-tested manner, though. + final List sourceSets = includeAllSourceSets ? List.copyOf(delegate.getSourceSets()) : List.of(sourceSet); + for (final var sourceSet : sourceSets) { + final var name = capitalize(sourceSet.getName()); + final var modules = delegate.getXtcCompilerOutputDirModules(sourceSet); + // xtcMain - Normally the only one we need to use + // xtcMainFiles - This is used to generate runAll task contents. + map.put(XTC_LANGUAGE_NAME + name, resolveDirectories(modules)); + } + + map.forEach((k, v) -> logger.info("{} Resolved files: {} -> {}", prefix, k, v)); + logger.info("{} Resolving module path:", prefix); + return verifyModulePath(map); + } + + private Set verifyModulePath(final Map> map) { + logger.info("{} ModulePathMap: [{} keys and {} values]", prefix, map.keySet().size(), map.values().stream().mapToInt(Set::size).sum()); + + final var modulePathList = new ArrayList(); + map.forEach((provider, files) -> { + logger.info("{} Module path from: '{}':", prefix, provider); + if (files.isEmpty()) { + logger.info("{} (empty)", prefix); + } + files.forEach(f -> logger.info("{} {}", prefix, f.getAbsolutePath())); + + modulePathList.addAll(files.stream().filter(f -> { + if (f.isDirectory()) { + logger.info("{} Adding directory to module path ({}).", prefix, f.getAbsolutePath()); + } else if (!isValidXtcModule(f)) { + logger.warn("{} Has a non .xtc module file on the module path ({}). Was this intended?", prefix, f.getAbsolutePath()); + return false; + } + return true; + }).toList()); + }); + + final Set modulePathSet = modulePathList.stream().collect(Collectors.toUnmodifiableSet()); + final int modulePathListSize = modulePathList.size(); + final int modulePathSetSize = modulePathSet.size(); + + // Check that we don't have name collisions with the same dependency declared in several places. + if (modulePathListSize != modulePathSetSize) { + logger.warn("{} There are {} duplicated modules on the full module path.", prefix, modulePathListSize - modulePathSetSize); + } + + checkDuplicatesInModulePaths(modulePathSet); + + // Check that all modules on path are XTC files. + logger.info("{} Final module path: {}", prefix, modulePathSet); + return modulePathSet; + } + + private void checkDuplicatesInModulePaths(final Set modulePathSet) { + for (final File module : modulePathSet) { + // find modules with the same name (or TODO: with the same identity) + if (module.isDirectory()) { + // TODO, sanity check directories later. The only cause of concern are identical ones, and that is not fatal, but may merit a warning. + // The Set data structure already takes care of silently removing them, however. + continue; + } + final List dupes = modulePathSet.stream().filter(File::isFile).filter(f -> f.getName().equals(module.getName())).toList(); + assert (!dupes.isEmpty()); + if (dupes.size() != 1) { + throw buildException("A dependency with the same name is defined in more than one ({}) location on the module path.", dupes.size()); + } + } + } + + public static Set resolveFiles(final FileCollection files) { + return files.isEmpty() ? EMPTY_FILE_COLLECTION : files.getAsFileTree().getFiles(); + } + + public static Set resolveDirectories(final Set files) { + return files.stream().map(f -> requireNonNull(f.getParentFile())).collect(Collectors.toUnmodifiableSet()); + } + + @SuppressWarnings("unused") + protected Set resolveFiles(final Provider dirProvider) { + return resolveFiles(project.files(dirProvider)); + } + + protected Set resolveDirectories(final Provider dirProvider) { + return resolveDirectories(resolveFiles(project.files(dirProvider))); + } +} diff --git a/plugin/src/main/java/org/xtclang/plugin/tasks/XtcRunAllTask.java b/plugin/src/main/java/org/xtclang/plugin/tasks/XtcRunAllTask.java new file mode 100644 index 0000000000..b0e9788fa3 --- /dev/null +++ b/plugin/src/main/java/org/xtclang/plugin/tasks/XtcRunAllTask.java @@ -0,0 +1,26 @@ +package org.xtclang.plugin.tasks; + +import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.TaskAction; +import org.xtclang.plugin.XtcProjectDelegate; + +import javax.inject.Inject; + +public class XtcRunAllTask extends XtcRunTask { + @Inject + public XtcRunAllTask(final XtcProjectDelegate delegate, final String taskName, final SourceSet moduleSourceSet) { + super(delegate, taskName, moduleSourceSet); + } + + @Override + public boolean isRunAllTask() { + return true; + } + + @Override + @TaskAction + public void run() { + logger.warn("{} Running all XTC modules, even if they aren't configured to be run by default.", prefix); + super.run(); + } +} diff --git a/plugin/src/main/java/org/xtclang/plugin/tasks/XtcRunTask.java b/plugin/src/main/java/org/xtclang/plugin/tasks/XtcRunTask.java new file mode 100644 index 0000000000..f26666aef8 --- /dev/null +++ b/plugin/src/main/java/org/xtclang/plugin/tasks/XtcRunTask.java @@ -0,0 +1,279 @@ +package org.xtclang.plugin.tasks; + +import org.gradle.api.Action; +import org.gradle.api.file.Directory; +import org.gradle.api.file.FileCollection; +import org.gradle.api.logging.LogLevel; +import org.gradle.api.provider.ListProperty; +import org.gradle.api.provider.Provider; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.InputDirectory; +import org.gradle.api.tasks.InputFiles; +import org.gradle.api.tasks.Internal; +import org.gradle.api.tasks.Optional; +import org.gradle.api.tasks.PathSensitive; +import org.gradle.api.tasks.PathSensitivity; +import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.TaskAction; +import org.gradle.process.ExecResult; +import org.xtclang.plugin.XtcBuildRuntimeException; +import org.xtclang.plugin.XtcProjectDelegate; +import org.xtclang.plugin.XtcRunModule; +import org.xtclang.plugin.XtcRuntimeExtension; +import org.xtclang.plugin.internal.DefaultXtcRunModule; +import org.xtclang.plugin.internal.DefaultXtcRuntimeExtension; +import org.xtclang.plugin.launchers.CommandLine; +import org.xtclang.plugin.launchers.XtcLauncher; + +import javax.inject.Inject; +import java.io.File; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +import static java.util.Collections.emptySet; +import static org.gradle.api.logging.LogLevel.ERROR; +import static org.gradle.api.logging.LogLevel.INFO; +import static org.gradle.api.logging.LogLevel.LIFECYCLE; +import static org.xtclang.plugin.XtcPluginConstants.XTC_RUNNER_CLASS_NAME; +import static org.xtclang.plugin.XtcPluginConstants.XTC_RUNNER_LAUNCHER_NAME; +import static org.xtclang.plugin.XtcProjectDelegate.incomingXtcModuleDependencies; + +/** + * Task that runs and XTC module, given at least its name, using the module path from + * the XTC environment. + *

+ * If the 'runXtc' task is called without any module specifications, it will try to run every module compiled + * from the source set in undefined order. That would be the exact equivalent of calling 'runAllXtc' + * (which does not need to be configured, and is a task added by the plugin). + * + * @see XtcRunAllTask + */ + +// TODO: Add modules {} segment to runtime DSL +// TODO: Add a generic xtcPlugin or xtc extension, where we can set stuff like, e.g. log level for the plugin (which does not redirect) +// TODO: Add WorkerExecutor and the Gradle Worker API to execute in parallel if there are no dependencies. +// Any task with zero defined outputs is not cacheable, which should be enough for all run tasks. +// TODO: @CacheableTask in any form? +public abstract class XtcRunTask extends XtcLauncherTask implements XtcRuntimeExtension { + private final Map executedModules; // TODO we can cache output here to if we want. + + /** + * Create an XTC run task, currently delegating instead of inheriting the plugin project + * delegate. We are slowly getting rid of this delegate pattern, now that the intra-plugin + * needed types have been resolved. + * + * @param delegate Project delegate + * @param taskName Name of this run task + * @param sourceSet Source set for the code of the module to be executed. + */ + @Inject + public XtcRunTask(final XtcProjectDelegate delegate, final String taskName, final SourceSet sourceSet) { + super(delegate, taskName, sourceSet, delegate.resolveXtcRuntimeExtension()); + this.executedModules = new LinkedHashMap<>(); + // TODO: Currently we just inherit modules from the run spec, we can change then in the run task later; e.g. // this.modules = objects.listProperty(XtcRunModule.class).convention(getExtension().getModules()); + } + + @SuppressWarnings("unused") + @Internal + public boolean isRunAllTask() { + return false; + } + + @Internal + @Override + public final String getNativeLauncherCommandName() { + return XTC_RUNNER_LAUNCHER_NAME; + } + + @Internal + @Override + public final String getJavaLauncherClassName() { + return XTC_RUNNER_CLASS_NAME; + } + + @Optional + @InputFiles + @PathSensitive(PathSensitivity.ABSOLUTE) + FileCollection getInputDeclaredDependencyModules() { + return delegate.filesFrom(incomingXtcModuleDependencies(sourceSet)); // xtcModule and xtcModuleTest dependencies declared in the project dependency { scope section + } + + @Optional // we may have a build that only relies on modules build by other projects, or other dependencies. + @InputFiles + @PathSensitive(PathSensitivity.ABSOLUTE) + FileCollection getInputModulesCompiledByProject() { + return delegate.getXtcCompilerOutputModules(sourceSet); // The output of the XTC compiler for this project and source set. + } + + @InputDirectory + @PathSensitive(PathSensitivity.ABSOLUTE) + Provider getInputXdkModules() { + return delegate.getXdkContentsDir(); // Modules in the XDK directory, if one exists. + } + + @Input + @Override + public ListProperty getModules() { + return getExtension().getModules(); + } + + private XtcBuildRuntimeException extensionOnly(final String operation) { + return buildException("Operation '{}' only available through xtcRun extension DSL at the moment.", operation); + } + + @Override + public XtcRunModule module(final Action action) { + throw extensionOnly("module"); + } + + @Override + public void moduleName(final String name) { + throw extensionOnly("moduleName"); + } + + @Override + public void moduleNames(final String... modules) { + throw extensionOnly("moduleNames"); + } + + @Override + public void setModules(final List modules) { + throw extensionOnly("setModules"); + } + + @Override + public void setModules(final XtcRunModule... modules) { + throw extensionOnly("setModules"); + } + + @Override + public void setModuleNames(final List moduleNames) { + throw extensionOnly("setModuleNames"); + } + + @Override + public void setModuleNames(final String... moduleNames) { + throw extensionOnly("setModuleNames"); + } + + @Internal + @Override + public boolean isEmpty() { + return getExtension().isEmpty(); + } + + @TaskAction + public void run() { + start(); + final var cmd = new CommandLine(XTC_RUNNER_CLASS_NAME, getJvmArgs().get()); + cmd.addBoolean("--version", getShowVersion().get()); + cmd.addBoolean("--verbose", getIsVerbose().get()); + cmd.addRepeated("-L", resolveModulePath(getInputDeclaredDependencyModules())); + final var launcher = createLauncher(); + moduleRunQueue().forEach(module -> runOne(module, launcher, cmd.copy())); + logFinishedRuns(); + } + + private void logFinishedRuns() { + checkResolvable(); + final int count = executedModules.size(); + logger.info("{} Task executed {} modules:", prefix, count); + int i = 0; + for (final var entry : executedModules.entrySet()) { + final DefaultXtcRunModule config = entry.getKey(); + final ExecResult result = entry.getValue(); + final String index = String.format("(%2d/%2d)", ++i, count); + final boolean success = result.getExitValue() == 0; + final LogLevel level = success ? (delegate.hasVerboseLogging() ? LIFECYCLE : INFO) : ERROR; + logger.log(level, "{} {} {} {}", prefix, index, config.getModuleName().get(), config.toString()); + logger.log(level, "{} {} {} {}", prefix, index, success ? "SUCCESS" : "FAILURE", result); + } + } + + protected Stream moduleRunQueue() { + final var modulesToRun = resolveModulesToRun(); + logger.info("{} Queued up {} module(s) to execute:", prefix, modulesToRun.size()); + // TODO: Allow parallel execution + return modulesToRun.stream(); + } + + /** + * Check if there are module { ... } declarations without names. TODO: Can use mandatory flag + * NOTE: This function expects that the configuration phase is finished and everything resolves. + */ + private List validatedModules() { + return getModules().get().stream().filter(m -> { + if (!m.validate()) { + throw buildException("ERROR: XtcRunModule was declared without a valid moduleName property: {}", m); + } + return true; + }).toList(); + } + + protected Collection resolveModulesToRun() { + // Given the module definition in the xtcRun closure in the DSL, create their equivalent POJOs. + if (getExtension().isEmpty()) { + // 1) No modules were declared + logger.warn("{} Configuration does not contain specified modules to run. Will default to 'xtcRunAll' task.", prefix); + + // 2) Examine all compiled modules for this project. + final var allModules = resolveCompiledModules(); + if (allModules.isEmpty()) { + // 3) We have no compiled modules for the project, return an empty execution queue. + logger.warn("{} 'here is nothing in the module path to run. Aborting.", prefix); + return emptySet(); + } + + // 4) We do have modules compiled for this project. Add them all to the execution queue. + allModules.forEach(m -> logger.info("{} Module '{}' added to execution queue.", prefix, m)); + return allModules; + } + + return validatedModules(); + } + + private Collection resolveCompiledModuleFiles() { + final var allFiles = getInputModulesCompiledByProject().getAsFileTree().getFiles(); + logger.info("{} All XTC modules compiled for SourceSet '{}':", prefix, sourceSet.getName()); + allFiles.forEach(f -> logger.info("{} {}", prefix, f.getAbsolutePath())); + return allFiles; + } + + private Collection resolveCompiledModules() { + final var moduleFiles = resolveCompiledModuleFiles(); + final var allModules = moduleFiles.stream().map(File::getAbsolutePath).map(this::createModuleNamed).toList(); + logger.info("{} Resolved {} module names or module file paths to run.", prefix, allModules.size()); + return allModules; + } + + private XtcRunModule createModuleNamed(final String name) { + return DefaultXtcRuntimeExtension.createModule(project, name); + } + + @SuppressWarnings("UnusedReturnValue") + private ExecResult runOne(final XtcRunModule runConfig, final XtcLauncher> launcher, final CommandLine cmd) { + logger.info("{} Executing resolved xtcRuntime module closure: {}", prefix, runConfig); + final var moduleMethod = runConfig.getMethodName().get(); + if (!runConfig.hasDefaultMethodName()) { + cmd.add("--method", moduleMethod); + } + + final var moduleName = runConfig.getModuleName().get(); + cmd.addRaw(moduleName); + cmd.addRaw(runConfig.getModuleArgs().get()); + + final ExecResult result = launcher.apply(cmd); + executedModules.put((DefaultXtcRunModule)runConfig, result); + logger.info("{} Finished executing: {}", prefix, moduleName); + + return handleExecResult(result); + } + + @Override + public String toString() { + return projectName + ':' + taskName + " [class: " + getClass().getSimpleName() + ']'; + } +} diff --git a/plugin/src/main/java/org/xtclang/plugin/tasks/XtcSourceTask.java b/plugin/src/main/java/org/xtclang/plugin/tasks/XtcSourceTask.java new file mode 100644 index 0000000000..7d1ff8fc62 --- /dev/null +++ b/plugin/src/main/java/org/xtclang/plugin/tasks/XtcSourceTask.java @@ -0,0 +1,233 @@ +package org.xtclang.plugin.tasks; + +import groovy.lang.Closure; +import org.gradle.api.file.ConfigurableFileCollection; +import org.gradle.api.file.FileTree; +import org.gradle.api.file.FileTreeElement; +import org.gradle.api.specs.Spec; +import org.gradle.api.tasks.IgnoreEmptyDirectories; +import org.gradle.api.tasks.InputFiles; +import org.gradle.api.tasks.Internal; +import org.gradle.api.tasks.PathSensitive; +import org.gradle.api.tasks.PathSensitivity; +import org.gradle.api.tasks.SkipWhenEmpty; +import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.util.PatternFilterable; +import org.gradle.api.tasks.util.PatternSet; +import org.gradle.internal.Factory; +import org.jetbrains.annotations.NotNull; +import org.xtclang.plugin.XtcCompilerExtension; +import org.xtclang.plugin.XtcProjectDelegate; + +import javax.inject.Inject; +import java.io.File; +import java.util.HashSet; +import java.util.Set; + +import static org.xtclang.plugin.XtcPluginConstants.XTC_SOURCE_FILE_EXTENSION; +import static org.xtclang.plugin.XtcPluginUtils.FileUtils.hasFileExtension; + +public abstract class XtcSourceTask extends XtcLauncherTask implements PatternFilterable { + // This is just necessary since we assume some things about module definition source locations. It should not be exported. + private static final String XDK_TURTLE_SOURCE_FILENAME = "mack.x"; + + private final PatternFilterable patternSet; + + private ConfigurableFileCollection sourceFiles; + + @SuppressWarnings("this-escape") + protected XtcSourceTask(final XtcProjectDelegate delegate, final String taskName, final SourceSet sourceSet) { + super(delegate, taskName, sourceSet, delegate.resolveXtcCompileExtension()); + this.patternSet = getPatternSetFactory().create(); + this.sourceFiles = objects.fileCollection(); + } + + @Inject + protected Factory getPatternSetFactory() { + throw new UnsupportedOperationException("XtcSourceTask.getPatternSetFactory()"); + } + + @SuppressWarnings("unused") + @Internal + protected PatternFilterable getPatternSet() { + return patternSet; + } + + /** + * Returns the source for this task, after the include and exclude patterns have been applied. Ignores source files which do not exist. + * + *

+ * The {@link PathSensitivity} for the sources is configured to be {@link PathSensitivity#ABSOLUTE}. + * If your sources are less strict, please change it accordingly by overriding this method in your subclass. + *

+ * + * @return The source. + */ + @InputFiles + @SkipWhenEmpty + @IgnoreEmptyDirectories + @PathSensitive(PathSensitivity.ABSOLUTE) + public FileTree getSource() { + return sourceFiles.getAsFileTree().matching(patternSet); + } + + /** + * Sets the source for this task. + * + * @param source The source. + * @since 4.0 + */ + public void setSource(final FileTree source) { + setSource((Object) source); + } + + /** + * Sets the source for this task. The given source object is evaluated as per {@link org.gradle.api.Project#files(Object...)}. + * + * @param source The source. + */ + public void setSource(final Object source) { + sourceFiles = objects.fileCollection().from(source); + } + + /** + * Adds some source to this task. The given source objects will be evaluated as per {@link org.gradle.api.Project#files(Object...)}. + * + * @param sources The source to add + * @return this + */ + @SuppressWarnings("unused") + public XtcSourceTask source(final Object... sources) { + sourceFiles.from(sources); + return this; + } + + @Override + public @NotNull XtcSourceTask include(final String @NotNull ... includes) { + patternSet.include(includes); + return this; + } + + @Override + public @NotNull XtcSourceTask include(final @NotNull Iterable includes) { + patternSet.include(includes); + return this; + } + + @Override + public @NotNull XtcSourceTask include(final @NotNull Spec includeSpec) { + patternSet.include(includeSpec); + return this; + } + + @SuppressWarnings("rawtypes") + @Override + public @NotNull XtcSourceTask include(final @NotNull Closure includeSpec) { + patternSet.include(includeSpec); + return this; + } + + @Override + public @NotNull XtcSourceTask exclude(final String @NotNull ... excludes) { + patternSet.exclude(excludes); + return this; + } + + @Override + public @NotNull XtcSourceTask exclude(final @NotNull Iterable excludes) { + patternSet.exclude(excludes); + return this; + } + + @Override + public @NotNull XtcSourceTask exclude(final @NotNull Spec excludeSpec) { + patternSet.exclude(excludeSpec); + return this; + } + + @SuppressWarnings("rawtypes") + @Override + public @NotNull XtcSourceTask exclude(final @NotNull Closure excludeSpec) { + patternSet.exclude(excludeSpec); + return this; + } + + @Override + @Internal + public @NotNull Set getIncludes() { + return patternSet.getIncludes(); + } + + @Override + public @NotNull XtcSourceTask setIncludes(final @NotNull Iterable includes) { + patternSet.setIncludes(includes); + return this; + } + + @Override + @Internal + public @NotNull Set getExcludes() { + return patternSet.getExcludes(); + } + + @Override + public @NotNull XtcSourceTask setExcludes(final @NotNull Iterable excludes) { + patternSet.setExcludes(excludes); + return this; + } + + /** + * Update the lastModified on all source files to 'now' in the epoch. This is probably overkill, as it is used + * only for "forceRebuild", which really making the compileXtc tasks non-cacheable and never up + * to date during configuration, should be enough to accomplish. TODO: Verify this. + */ + public void touchAllSource() { + getSource().forEach(src -> { + final var before = src.lastModified(); + final var after = touch(src); + logger.info("{} *** File: {} (before: {}, after: {})", prefix, src.getAbsolutePath(), before, after); + }); + logger.info("{} Updated lastModified of {}.getSource() and resources to 'now' in the epoch.", prefix, taskName); + } + + private long touch(final File file) { + return touch(file, System.currentTimeMillis()); + } + + private long touch(final File file, final long now) { + final var oldLastModified = file.lastModified(); + if (!file.setLastModified(now)) { + logger.warn("{} Failed to update modification time stamp for file: {}", prefix, file.getAbsolutePath()); + } + logger.info("{} Touch file: {} (timestamp: {} -> {})", prefix, file.getAbsolutePath(), oldLastModified, now); + assert(file.lastModified() == now); + return now; + } + + protected boolean isXtcSourceFile(final File file) { + // TODO: Previously we called a Launcher method to ensure this was a module, but all these files should be in the top + // level directory of a source set, and this means that xtc will assume they are all module definitions, and fail if this + // is not the case. We used to check for this in the plugin, but we really do not want the compile time dependency to + // the javatools.jar in the plugin, as the plugin comes in early. This would have bad side effects, like "clean" would + // need to build the javatools.jar, if it wasn't there, just to immediately delete it again. + return file.isFile() && hasFileExtension(file, XTC_SOURCE_FILE_EXTENSION); + } + + protected boolean isTopLevelXtcSourceFile(final File file) { + return !file.isDirectory() && isXtcSourceFile(file) && isTopLevelSource(file); + } + + protected boolean isTopLevelSource(final File file) { + assert file.isFile(); + final var topLevelSourceDirs = new HashSet<>(sourceSet.getAllSource().getSrcDirs()); + final var dir = file.getParentFile(); + assert (dir != null && dir.isDirectory()); + final var isTopLevelSrc = topLevelSourceDirs.contains(dir); + logger.info("{} Checking if {} is a module definition (currently, just checking if it's a top level file): {}", prefix, file.getAbsolutePath(), isTopLevelSrc); + if (isTopLevelSrc || XDK_TURTLE_SOURCE_FILENAME.equalsIgnoreCase(file.getName())) { + logger.info("{} Found module definition: {}", prefix, file.getAbsolutePath()); + return true; + } + return false; + } +} diff --git a/plugin/xtc-plugin.properties b/plugin/xtc-plugin.properties new file mode 100644 index 0000000000..22f4bfc76c --- /dev/null +++ b/plugin/xtc-plugin.properties @@ -0,0 +1,30 @@ +# +# XTC plugin specific properties +# +# All properties files in a project hierarchy are ingested on build, and redefinitions of the same property +# deeper in the file tree overwrite those at shallower levels, to avoid per-module configuration granularity +# +# For example, some of the repository information on where we publish our artifact are defined here, but +# some in the ancestral root gradle.properties file, and some (secrets), typically in +# $GRADLE_USER_HOME/gradle.properties +# + +org.xtclang.plugin.id=org.xtclang.xtc-plugin +org.xtclang.plugin.display.name=XTC Language Gradle plugin +org.xtclang.plugin.description=A plugin that teaches Gradle the XTC language. First step to language server debugging and IDE support. +org.xtclang.plugin.implementation.class=org.xtclang.plugin.XtcPlugin +org.xtclang.plugin.website=https://xtclang.org +org.xtclang.plugin.vcs.url=https://github.com/xtclang/xvm + +# The default is to derive javatools.jar from the xdk or xdkDistribution configurations. However, it is useful to be able to +# invoke it from the same thread without JavaExec when debugging crossovers between the build system and the XDK +# implementation. +org.xtclang.plugin.bundle.javatools=true + +# Do we want to Gradle artifact to be built too? The contents are redundant compared to the parent Maven publication, +# but the metadata is not, which is required for XTC support in an XTC project outside the XDK build. +org.xtclang.plugin.isAutomatedPublishing=true + +# Should we install the plugin as part of the installLocalDist, so we can use +# something like XDK_HOME/plugin/repo as a plugin source? +org.xtclang.publish.localDist=false diff --git a/settings.gradle.kts b/settings.gradle.kts index ba97e40b7d..a3578422ed 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,25 +1,41 @@ -rootProject.name = "xvm" +/** + * Root settings file. Used for build aggregation, and configuring logic that must be ready + * before we reach the root project build. + */ + +pluginManagement { + includeBuild("build-logic/aggregator") + includeBuild("build-logic/settings-plugins") + includeBuild("build-logic/common-plugins") +} -include(":javatools_utils") // produces javatools_utils.jar for org.xvm.utils package -include(":javatools_unicode") // produces data files -> :lib_ecstasy/resources, only on request -include(":javatools") // produces javatools.jar -include(":javatools_turtle") // produces *only* a source zip file (no .xtc), and only on request -include(":javatools_bridge") // produces *only* a source zip file (no .xtc), and only on request -include(":javatools_launcher") // produces native executables (Win, Mac, Linux), only on request -include(":lib_ecstasy") // produces *only* a source zip file (no .xtc), and only on request -include(":lib_aggregate") // produces aggregate.xtc -include(":lib_collections") // produces collections.xtc -include(":lib_crypto") // produces crypto.xtc -include(":lib_net") // produces net.xtc -include(":lib_json") // produces json.xtc -include(":lib_oodb") // produces oodb.xtc -include(":lib_jsondb") // produces jsondb.xtc -include(":lib_web") // produces web.xtc -include(":lib_webauth") // produces webauth.xtc -include(":lib_xenia") // produces xenia.xtc +plugins { + `gradle-enterprise` +} -// TODO(":wiki") -include(":xdk") // builds the above modules (ecstasy.xtc, javatools_bridge.xtc, json.xtc, etc.) -// drags in Java libraries (javatools_utils, javatools), native launchers, wiki, etc. +gradleEnterprise { + buildScan { + val isCi = System.getenv("CI") != null + val isGitHubAction = System.getenv("GITHUB_ACTIONS") == "true" + publishAlwaysIf(isGitHubAction) + publishOnFailure() + termsOfServiceUrl = "https://gradle.com/terms-of-service" + termsOfServiceAgree = "yes" + isUploadInBackground = isCi + tag(if (isCi) "CI" else "LOCAL") + capture { + isTaskInputFiles = true + isBuildLogging = true + isTestLogging = true + } + } +} -include(":manualTests") // temporary; allowing gradle test execution \ No newline at end of file +includeBuild("javatools_utils") +includeBuild("javatools") +includeBuild("javatools_unicode") +includeBuild("plugin") +includeBuild("xdk") +includeBuild("manualTests") + +rootProject.name = "xvm" diff --git a/xdk.properties b/xdk.properties new file mode 100644 index 0000000000..ba81e66e98 --- /dev/null +++ b/xdk.properties @@ -0,0 +1,59 @@ +# +# This file contains global XDK properties, that do not explicitly configure Gradle or its DSL infra. +# + +# GitHub XTC organization repository properties. +# (We require additional external/secret properties "org.xtclang.repo.github.[user, token]" +# These are typically defined outside the project in $GRADLE_USER_HOME/gradle.properties) +org.xtclang.repo.github.url=https://maven.pkg.github.com/xtclang/xvm +org.xtclang.repo.github.org=xtclang +org.xtclang.repo.github.tasks.group=github + +# Should we rebuild the unicode files from fresh templates as part of the XDK build? +# (Note: this modifies source control files in the project.) +# org.xtclang.unicode.rebuild=false + +# Should we sign the plugin and XDK artifacts that are published? +# Requires external/secret properties "org.xtclang.signing.[keyId, password, secretKeyRingFile]" +# +# TODO: This is not required until we start with signed publications, for example to mavenCentral() +# and other places it is required. +org.xtclang.publications.sign=false + +# Should we do a sanity check that we can execute a sample XTC application as +# part of the XDK build? Later this will be moved into the unit test cycle. +# +# Example use case to enable for one build: "ORG_XTCLANG_BUILD_SANITY_CHECK_RUNTIME=true ./gradlew build --scan" +# +# The "forceRebuild" sub property is only relevant if the master property without the forceRebuild +# suffix is set to true. +# +# (Currently the only automated unit tests during the build are for Java +# modules, but this will change when we enable Plugin unit testing, XUnit or +# other XTC test methodologies.) +org.xtclang.build.sanityCheckRuntime=false +org.xtclang.build.sanityCheckRuntime.forceRebuild=true + +# Should we delete an existing local distribution before creating a new one? Note that +# running the "installLocalDist" task always backs up any existing local installation to +# the build directory before it runs, regardless of if we allow overwrites or not. +# Setting this flag to "true" just writes the new distribution to the existing local +# distribution folder, without removing any of the files. The default is "true". +org.xtclang.build.allowOverwriteLocalDist=true + +# Force republications of Gradle plugins every build, without explicitly changing versions. +org.xtclang.publish.build.identifiers=false + +# Should install build the distExe by default - currently requires running in an environment +# or container where 'makensis' with the EnVar plugin is installed and available. +# org.xtclang.install.distExe=false + +# Java Properties; used by the XTC precompiled Java convention plugin. +org.xtclang.java.jdk=21 +org.xtclang.java.enablePreview=false +org.xtclang.java.maxWarnings=100 +org.xtclang.java.maxErrors=100 +org.xtclang.java.warningsAsErrors=true +org.xtclang.java.lint=true +org.xtclang.java.test.stdout=false +org.xtclang.java.maxHeap=4G diff --git a/xdk/build.gradle.kts b/xdk/build.gradle.kts index b974ebbc79..48cc1faa8e 100644 --- a/xdk/build.gradle.kts +++ b/xdk/build.gradle.kts @@ -1,602 +1,331 @@ -/* - * Build files for the XDK. +import XdkBuildLogic.Companion.XDK_ARTIFACT_NAME_DISTRIBUTION_ARCHIVE +import XdkDistribution.Companion.DISTRIBUTION_TASK_GROUP +import org.gradle.api.attributes.Category.CATEGORY_ATTRIBUTE +import org.gradle.api.attributes.Category.LIBRARY +import org.gradle.api.attributes.LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE +import org.gradle.api.logging.LogLevel.INFO +import org.gradle.language.base.plugins.LifecycleBasePlugin.BUILD_GROUP +import org.xtclang.plugin.tasks.XtcCompileTask +import java.io.ByteArrayOutputStream +import java.nio.file.Files +import java.nio.file.attribute.FileAttribute + +/** + * XDK root project, collecting the lib_* xdk builds as includes, not includedBuilds ATM, + * and producing one outgoing artifact with provided layout for the XDK being shipped. */ -import java.nio.file.Paths - -val javatools = project(":javatools") -val launcher = project(":javatools_launcher") -val turtle = project(":javatools_turtle") -val bridge = project(":javatools_bridge") -val ecstasy = project(":lib_ecstasy") -val aggregate = project(":lib_aggregate") -val collections = project(":lib_collections") -val crypto = project(":lib_crypto") -val net = project(":lib_net") -val json = project(":lib_json") -val oodb = project(":lib_oodb") -val jsondb = project(":lib_jsondb") -val web = project(":lib_web") -val webauth = project(":lib_webauth") -val xenia = project(":lib_xenia") - -val ecstasyMain = "${ecstasy.projectDir}/src/main" -val turtleMain = "${turtle.projectDir}/src/main" -val bridgeMain = "${bridge.projectDir}/src/main" -val javatoolsJar = "${javatools.buildDir}/libs/javatools.jar" -val launcherMain = "${launcher.projectDir}/src/main" -val aggregateMain = "${aggregate.projectDir}/src/main" -val collectionsMain = "${collections.projectDir}/src/main" -val cryptoMain = "${crypto.projectDir}/src/main" -val netMain = "${net.projectDir}/src/main" -val jsonMain = "${json.projectDir}/src/main" -val oodbMain = "${oodb.projectDir}/src/main" -val jsondbMain = "${jsondb.projectDir}/src/main" -val webMain = "${web.projectDir}/src/main" -val webauthMain = "${webauth.projectDir}/src/main" -val xeniaMain = "${xenia.projectDir}/src/main" - -val xdkDir = "$buildDir/xdk" -val binDir = "$xdkDir/bin" -val libDir = "$xdkDir/lib" -val coreLib = "$libDir/ecstasy.xtc" -val javaDir = "$xdkDir/javatools" -val turtleLib = "$javaDir/javatools_turtle.xtc" -val bridgeLib = "$javaDir/javatools_bridge.xtc" - -val distDir = "$buildDir/dist" - -val xdkVersion = rootProject.version -var distName = xdkVersion -val isCI = System.getenv("CI") -val buildNum = System.getenv("BUILD_NUMBER") -if (isCI != null && isCI != "0" && isCI != "false" && buildNum != null) { - distName = "${distName}ci${buildNum}" - - val output = java.io.ByteArrayOutputStream() - project.exec { - commandLine("git", "rev-parse", "HEAD") - standardOutput = output - setIgnoreExitValue(true) - } - val changeId = output.toString().trim() - if (changeId.length > 0) { - distName = "${distName}+${changeId}" - } +plugins { + alias(libs.plugins.xdk.build.publish) + alias(libs.plugins.xtc) + alias(libs.plugins.tasktree) + alias(libs.plugins.versions) + distribution // TODO: Create our own XDK distribution plugin, or put it in the XTC plugin } -println("*** XDK distName=${distName}") -tasks.register("clean") { - group = "Build" - description = "Delete previous build results" - delete("$buildDir") +val xtcLauncherBinaries by configurations.registering { + isCanBeResolved = true + isCanBeConsumed = false } -val copyOutline = tasks.register("copyOutline") { - from("$projectDir/src/main/resources") { - include("xdk/**") - } - into("$buildDir") - doLast { - println("Finished task: copyOutline") +/** + * Local configuration to provide an xdk-distribution, which contains versioned zip and tar.gz XDKs. + */ +val xdkProvider by configurations.registering { + isCanBeConsumed = true + isCanBeResolved = false + // TODO these are added twice to the archive configuration. We probably don't want that. + outgoing.artifact(tasks.distZip) { + extension = "zip" } -} - -val copyJavatools = tasks.register("copyJavatools") { - from(javatoolsJar) - into("$javaDir/") - - dependsOn(javatools.tasks["build"]) - dependsOn(copyOutline) - doLast { - println("Finished task: copyJavatools") + attributes { + attribute(CATEGORY_ATTRIBUTE, objects.named(LIBRARY)) + attribute(LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(XDK_ARTIFACT_NAME_DISTRIBUTION_ARCHIVE)) } } -val compileEcstasy = tasks.register("compileEcstasy") { - group = "Build" - description = "Build ecstasy.xtc and javatools_turtle.xtc modules" - - dependsOn(javatools.tasks["build"]) - dependsOn(copyJavatools) - - classpath(javatoolsJar) - args("-o", "$libDir", - "--set-version", "$xdkVersion", - "$ecstasyMain/x/ecstasy.x", - "$turtleMain/x/mack.x") - mainClass.set("org.xvm.tool.Compiler") - - doLast { - file("$libDir/mack.xtc"). - renameTo(file("$turtleLib")) - println("Finished task: compileEcstasy") +val xtcUnicodeConsumer by configurations.registering { + isCanBeResolved = true + isCanBeConsumed = false + // TODO: Can likely remove these. + attributes { + attribute(CATEGORY_ATTRIBUTE, objects.named(LIBRARY)) + attribute(LIBRARY_ELEMENTS_ATTRIBUTE, objects.named("unicodeDir")) } } -val compileAggregate = tasks.register("compileAggregate") { - group = "Build" - description = "Build aggregate.xtc module" - - dependsOn(javatools.tasks["build"]) - - shouldRunAfter(compileCollections) - - classpath(javatoolsJar) - args("-o", "$libDir", - "--set-version", "$xdkVersion", - "-L", "$coreLib", - "-L", "$turtleLib", - "$aggregateMain/x/aggregate.x") - mainClass.set("org.xvm.tool.Compiler") +dependencies { + xdkJavaTools(libs.javatools) + xtcModule(libs.xdk.ecstasy) + xtcModule(libs.xdk.aggregate) + xtcModule(libs.xdk.collections) + xtcModule(libs.xdk.crypto) + xtcModule(libs.xdk.json) + xtcModule(libs.xdk.jsondb) + xtcModule(libs.xdk.net) + xtcModule(libs.xdk.oodb) + xtcModule(libs.xdk.web) + xtcModule(libs.xdk.webauth) + xtcModule(libs.xdk.xenia) + xtcModule(libs.javatools.bridge) + @Suppress("UnstableApiUsage") + xtcLauncherBinaries(project(path = ":javatools-launcher", configuration = "xtcLauncherBinaries")) } -val compileCollections = tasks.register("compileCollections") { - group = "Build" - description = "Build collections.xtc module" +private val semanticVersion: SemanticVersion by extra - dependsOn(javatools.tasks["build"]) +private val xdkDist = xdkBuildLogic.distro() - shouldRunAfter(compileEcstasy) - - classpath(javatoolsJar) - args("-o", "$libDir", - "--set-version", "$xdkVersion", - "-L", "$coreLib", - "-L", "$turtleLib", - "$collectionsMain/x/collections.x") - mainClass.set("org.xvm.tool.Compiler") -} - -val compileCrypto = tasks.register("compileCrypto") { - group = "Build" - description = "Build crypto.xtc module" - - dependsOn(javatools.tasks["build"]) - - shouldRunAfter(compileEcstasy) - - classpath(javatoolsJar) - args("-o", "$libDir", - "--set-version", "$xdkVersion", - "-L", "$coreLib", - "-L", "$turtleLib", - "$cryptoMain/x/crypto.x") - mainClass.set("org.xvm.tool.Compiler") -} - -val compileNet = tasks.register("compileNet") { - group = "Build" - description = "Build net.xtc module" - - dependsOn(javatools.tasks["build"]) - - shouldRunAfter(compileCrypto) - - classpath(javatoolsJar) - args("-o", "$libDir", - "--set-version", "$xdkVersion", - "-L", "$coreLib", - "-L", "$turtleLib", - "-L", "$libDir", - "$netMain/x/net.x") - mainClass.set("org.xvm.tool.Compiler") -} - -val compileJson = tasks.register("compileJson") { - group = "Build" - description = "Build json.xtc module" - - dependsOn(javatools.tasks["build"]) - - shouldRunAfter(compileEcstasy) +/** + * Propagate the "version" part of the semanticVersion to all XTC compilers in all subprojects (the XDK modules + * will get stamped with the Gradle project version, as defined in VERSION in the repo root). + */ - classpath(javatoolsJar) - args("-o", "$libDir", - "--set-version", "$xdkVersion", - "-L", "$coreLib", - "-L", "$turtleLib", - "$jsonMain/x/json.x") - mainClass.set("org.xvm.tool.Compiler") +subprojects { + tasks.withType().configureEach { + /* + * Add version stamp to XDK module from the XDK build global semantic version single source of truth. + */ + assert(version == semanticVersion.artifactVersion) + xtcVersion = semanticVersion.artifactVersion + } } -val compileOODB = tasks.register("compileOODB") { - group = "Build" - description = "Build oodb.xtc module" - - dependsOn(javatools.tasks["build"]) - - shouldRunAfter(compileEcstasy) - - classpath(javatoolsJar) - args("-o", "$libDir", - "--set-version", "$xdkVersion", - "-L", "$coreLib", - "-L", "$turtleLib", - "$oodbMain/x/oodb.x") - mainClass.set("org.xvm.tool.Compiler") +publishing { + // TODO: Use the Nexus publication plugin and + // ./gradlew publishToSonatype closeAndReleaseSonatypeStagingRepository + // (incorporate that in aggregate publish task in xvm/build.gradle.kts) + publications { + val xdkArchive by registering(MavenPublication::class) { + with(project) { + groupId = group.toString() + artifactId = project.name + version = version.toString() + } + pom { + name = "xdk" + description = "XTC Language Software Development Kit (XDK) Distribution Archive" + url = "https://xtclang.org" + } + logger.info("$prefix Publication '$name' configured for '$groupId:$artifactId:$version'") + artifact(tasks.distZip) { + extension = "zip" + } + } + } } -val compileJsonDB = tasks.register("compileJsonDB") { - group = "Build" - description = "Build jsondb.xtc module" - - dependsOn(javatools.tasks["build"]) - - shouldRunAfter(compileJson, compileOODB) - - classpath(javatoolsJar) - args("-o", "$libDir", - "--set-version", "$xdkVersion", - "-L", "$coreLib", - "-L", "$turtleLib", - "-L", "$libDir", - "$jsondbMain/x/jsondb.x") - mainClass.set("org.xvm.tool.Compiler") +private fun shouldPublishPluginToLocalDist(): Boolean { + return project.getXdkPropertyBoolean("org.xtclang.publish.localDist", false) } -val compileWeb = tasks.register("compileWeb") { - group = "Execution" - description = "Build web.xtc module" - - dependsOn(javatools.tasks["build"]) - - shouldRunAfter(compileNet, compileJson) - - classpath(javatoolsJar) - args("-o", "$libDir", - "--set-version", "$xdkVersion", - "-L", "$coreLib", - "-L", "$turtleLib", - "-L", "$libDir", - "$webMain/x/web.x") - mainClass.set("org.xvm.tool.Compiler") +val publishPluginToLocalDist by tasks.registering { + group = BUILD_GROUP + // TODO: includeBuild dependency; Slightly hacky - use a configuration from the plugin project instead. + if (shouldPublishPluginToLocalDist()) { + dependsOn(gradle.includedBuild("plugin").task(":publishAllPublicationsToBuildRepository")) + outputs.dir(buildRepoDirectory) + doLast { + logger.info("$prefix Published plugin to build repository: ${buildRepoDirectory.get()}") + } + } } -val compileWebauth = tasks.register("compileWebauth") { - group = "Execution" - description = "Build webauth.xtc module" - - dependsOn(javatools.tasks["build"]) - - shouldRunAfter(compileOODB, compileWeb) - - classpath(javatoolsJar) - args("-o", "$libDir", - "--set-version", "$xdkVersion", - "-L", "$coreLib", - "-L", "$turtleLib", - "-L", "$libDir", - "$webauthMain/x/webauth.x") - mainClass.set("org.xvm.tool.Compiler") +/** + * Set up the distribution layout. This is executed during the config phase, which means that we can't + * resolve outputs to other tasks to their explicit destination files yet, unless they have run. + * However, we _can_ use a from spec that refers to a task, which then becomes a dependency. + */ +distributions { + main { + distributionBaseName = xdkDist.distributionName + assert(distributionBaseName.get() == "xdk") // TODO: Should really rename the distribution to "xdk" explicitly per convention. + contents { + // TODO: Why do we need the indirect - likely change these to lazy properties through map format. + // TODO WE should really not do get() here. + val resources = tasks.processResources.get().outputs.files.asFileTree + logger.info("$prefix Distribution contents need to use lazy resources.") + /* + * 1) copy build plugin repository publication of the XTC plugin to install/xdk/repo + * 2) copy xdk resources/main/xdk to install/xdk/libexec + * 3) copy javatools_launcher/bin/\* to install/xdk/libexec/bin/ + * 4) copy XDK modules to install/xdk/libexec/lib + * 5) copy javatools.jar, turtle and bridge to install/xdk/libexec/javatools + */ + from(resources) { + eachFile { + path = path.replace("xdk", "libexec") + path = path.replace("libexec-", "xdk-") // TODO: Hacky. + includeEmptyDirs = false + } + } + from(xtcLauncherBinaries) { + into("libexec/bin") + } + from(configurations.xtcModule) { + // This copies everything not a javatools jar into libexec/javatools, which is where XTC wants the + // javatools_turtle.xtc and javatools_bridge.xtc modules. + // TODO consider breaking out javatools_bridge.xtc, javatools_turtle.xtc into a separate configuration. + into("libexec/lib") + exclude("**/javatools*") + } + from(configurations.xtcModule) { + into("libexec/javatools") + include("**/javatools*") + } + from(configurations.xdkJavaTools) { + rename { + assert(it.endsWith(".jar")) + it.replace(Regex("-.*.jar"), ".jar") + } + into("libexec/javatools") // should just be one file with corrected dependencies, assert? + } + if (shouldPublishPluginToLocalDist()) { + val published = publishPluginToLocalDist.get().outputs.files + from(published) { + into("repo") + } + } + from(tasks.xtcVersionFile) + } + } } -val compileXenia = tasks.register("compileXenia") { - group = "Execution" - description = "Build xenia.xtc module" - - dependsOn(javatools.tasks["build"]) - - shouldRunAfter(compileWeb, compileWebauth) - - classpath(javatoolsJar) - args("-o", "$libDir", - "--set-version", "$xdkVersion", - "-L", "$coreLib", - "-L", "$turtleLib", - "-L", "$libDir", - "$xeniaMain/x/xenia.x") - mainClass.set("org.xvm.tool.Compiler") +val cleanXdk by tasks.registering(Delete::class) { + subprojects.forEach { + delete(it.layout.buildDirectory) + } + delete(compositeRootBuildDirectory) } -val compileBridge = tasks.register("compileBridge") { - group = "Execution" - description = "Build javatools_bridge.xtc module" - - dependsOn(javatools.tasks["build"]) - - shouldRunAfter(compileCollections, compileOODB, compileNet, compileWeb, compileXenia) - - classpath(javatoolsJar) - args("-o", "$libDir", - "--set-version", "$xdkVersion", - "-L", "$coreLib", - "-L", "$turtleLib", - "-L", "$libDir", - "$bridgeMain/x/_native.x") - mainClass.set("org.xvm.tool.Compiler") - +val clean by tasks.existing { + dependsOn(cleanXdk) doLast { - file("$libDir/_native.xtc"). - renameTo(file("$bridgeLib")) - println("Finished task: compileBridge") + logger.info("$prefix WARNING: Note that running 'clean' is often unnecessary with a properly configured build cache.") } } -val build = tasks.register("build") { - group = "Build" - description = "Build the XDK" - - // we assume that the launcher project has been built - val launcher = project(":javatools_launcher") - val linux_launcher = "${launcher.buildDir}/exe/linux_launcher" - val macos_launcher = "${launcher.buildDir}/exe/macos_launcher" - val windows_launcher = "${launcher.buildDir}/exe/windows_launcher.exe" - - // compile Ecstasy - val coreSrc = fileTree(ecstasyMain).getFiles().stream(). - mapToLong({f -> f.lastModified()}).max().orElse(0) - val coreDest = file("$coreLib").lastModified() - - val turtleSrc = fileTree(turtleMain).getFiles().stream(). - mapToLong({f -> f.lastModified()}).max().orElse(0) - val turtleDest = file("$turtleLib").lastModified() - - if (coreSrc > coreDest || turtleSrc > turtleDest) { - dependsOn(compileEcstasy) - } else { - dependsOn(copyJavatools) - } - - // compile aggregate.xtclang.org - val aggregateSrc = fileTree(aggregateMain).getFiles().stream(). - mapToLong({f -> f.lastModified()}).max().orElse(0) - val aggregateDest = file("$libDir/aggregate.xtc").lastModified() - - if (aggregateSrc > aggregateDest) { - dependsOn(compileAggregate) - } - - // compile collections.xtclang.org - val collectionsSrc = fileTree(collectionsMain).getFiles().stream(). - mapToLong({f -> f.lastModified()}).max().orElse(0) - val collectionsDest = file("$libDir/collections.xtc").lastModified() - - if (collectionsSrc > collectionsDest) { - dependsOn(compileCollections) - } - - // compile crypto.xtclang.org - val cryptoSrc = fileTree(cryptoMain).getFiles().stream(). - mapToLong({f -> f.lastModified()}).max().orElse(0) - val cryptoDest = file("$libDir/crypto.xtc").lastModified() - - if (cryptoSrc > cryptoDest) { - dependsOn(compileCrypto) - } - - // compile net.xtclang.org - val netSrc = fileTree(netMain).getFiles().stream(). - mapToLong({f -> f.lastModified()}).max().orElse(0) - val netDest = file("$libDir/net.xtc").lastModified() - - if (netSrc > netDest) { - dependsOn(compileNet) - } - - // compile json.xtclang.org - val jsonSrc = fileTree(jsonMain).getFiles().stream(). - mapToLong({f -> f.lastModified()}).max().orElse(0) - val jsonDest = file("$libDir/json.xtc").lastModified() - - if (jsonSrc > jsonDest) { - dependsOn(compileJson) - } - - // compile oodb.xtclang.org - val oodbSrc = fileTree(oodbMain).getFiles().stream(). - mapToLong({f -> f.lastModified()}).max().orElse(0) - val oodbDest = file("$libDir/oodb.xtc").lastModified() - - if (oodbSrc > oodbDest) { - dependsOn(compileOODB) - } +val distTar by tasks.existing(Tar::class) { + compression = Compression.GZIP + archiveExtension = "tar.gz" +} - // compile jsondb.xtclang.org - val jsondbSrc = fileTree(jsondbMain).getFiles().stream(). - mapToLong({f -> f.lastModified()}).max().orElse(0) - val jsondbDest = file("$libDir/jsondb.xtc").lastModified() +val distZip by tasks.existing(Zip::class) - if (jsondbSrc > jsondbDest) { - dependsOn(compileJsonDB) +val assembleDist by tasks.existing { + if (xdkDist.shouldCreateWindowsDistribution()) { + logger.warn("$prefix Task '$name' is configured to build a Windows installer. Environment needs '${XdkDistribution.MAKENSIS}' and the EnVar plugin.") + dependsOn(distExe) } +} - // compile web.xtclang.org - val webSrc = fileTree(webMain).getFiles().stream(). - mapToLong({f -> f.lastModified()}).max().orElse(0) - val webDest = file("$libDir/web.xtc").lastModified() - - if (webSrc > webDest) { - dependsOn(compileWeb) - } - - // compile webauth.xtclang.org - val webauthSrc = fileTree(webauthMain).getFiles().stream(). - mapToLong({f -> f.lastModified()}).max().orElse(0) - val webauthDest = file("$libDir/webauth.xtc").lastModified() - - if (webauthSrc > webauthDest) { - dependsOn(compileWebauth) - } - - // compile xenia.xtclang.org - val xeniaSrc = fileTree(xeniaMain).getFiles().stream(). - mapToLong({f -> f.lastModified()}).max().orElse(0) - val xeniaDest = file("$libDir/xenia.xtc").lastModified() - - if (xeniaSrc > xeniaDest) { - dependsOn(compileXenia) - } - - // compile _native.xtclang.org - val bridgeSrc = fileTree(bridgeMain).getFiles().stream(). - mapToLong({f -> f.lastModified()}).max().orElse(0) - val bridgeDest = file(bridgeLib).lastModified() +val distExe by tasks.registering { + group = DISTRIBUTION_TASK_GROUP + description = "Use an NSIS compatible plugin to create the Windows .exe installer." - if (bridgeSrc > bridgeDest) { - dependsOn(compileBridge, compileNet, compileCrypto) - } - - doLast { - copy { - from(linux_launcher, macos_launcher, windows_launcher) - into(binDir) - } - println("Finished task: build") + onlyIf { + xdkDist.shouldCreateWindowsDistribution() } -} - -// TODO wiki -val prepareDirs = tasks.register("prepareDirs") { - mustRunAfter("clean") + // TODO: Why do we need this dependency? Likely just remove it. + dependsOn(installDist) - doLast { - mkdir("$distDir") + val nsi = file("src/main/nsi/xdkinstall.nsi") + val makensis = XdkBuildLogic.findExecutableOnPath(XdkDistribution.MAKENSIS) + onlyIf { + makensis != null } -} -tasks.register("dist-local") { - group = "Distribution" - description = "Copy the xdk to the local homebrew cellar" - - dependsOn(build) + val inputDir = layout.buildDirectory.dir("install/xdk") + val outputFile = layout.buildDirectory.file("distributions/xdk-$version.exe") + inputs.dir(inputDir) + outputs.file(outputFile) + // notes: + // - requires NSIS to be installed (e.g. "sudo apt install nsis" works on Debian/Ubuntu) + // - requires the "makensis" command to be in the path + // - requires the EnVar plugin to be installed (i.e. unzipped) into NSIS + // - requires the x.ico file to be in the same directory as the nsi file doLast { - // getting the homebrew xdl location using "readlink -f `which xec`" command - val output = java.io.ByteArrayOutputStream() - - project.exec { - commandLine("which", "xec") - standardOutput = output - setIgnoreExitValue(true) + if (makensis == null) { + throw buildException("Cannot find '${XdkDistribution.MAKENSIS}' in PATH.") } - - val xecLink = output.toString().trim() - if (xecLink.length > 0) { - val xecFile = Paths.get(xecLink).toRealPath() - val libexecDir = file("$xecFile/../..") - var updated = false; - - val srcBin = fileTree("$binDir").getFiles().stream(). - mapToLong({f -> f.lastModified()}).max().orElse(0) - val dstBin = fileTree("$libexecDir/bin").getFiles().stream(). - mapToLong({f -> f.lastModified()}).max().orElse(0) - if (srcBin > dstBin) { - copy { - from("$binDir/") - into("$libexecDir/bin") - } - updated = true; - } - - val srcLib = fileTree("$libDir/").getFiles().stream(). - mapToLong({f -> f.lastModified()}).max().orElse(0) - val dstLib = fileTree("$libexecDir/lib").getFiles().stream(). - mapToLong({f -> f.lastModified()}).max().orElse(0) - if (srcLib > dstLib) { - copy { - from("$libDir/") - into("$libexecDir/lib") - } - updated = true; - } - - val srcJts = fileTree("$javaDir/").getFiles().stream(). - mapToLong({f -> f.lastModified()}).max().orElse(0) - val dstJts = fileTree("$libexecDir/javatools").getFiles().stream(). - mapToLong({f -> f.lastModified()}).max().orElse(0) - if (srcJts > dstJts) { - copy { - from("$javaDir/") - into("$libexecDir/javatools") - } - updated = true; - } - - if (updated) { - println("Updated local homebrew directory $libexecDir") - } + if (!nsi.exists()) { + throw buildException("Cannot find 'nsi' file: ${nsi.absolutePath}") } - else { - println("Missing local homebrew installation; run \"brew install xdk\" command first") + logger.info("$prefix Writing Windows installer: ${outputFile.get()}") + val stdout = ByteArrayOutputStream() + exec { + environment( + "NSIS_SRC" to inputDir.get(), + "NSIS_ICO" to xdkIconFile, + "NSIS_OUT" to outputFile.get(), + "NSIS_VER" to xdkDist.distributionVersion + ) + commandLine(makensis.toFile().absolutePath, nsi.absolutePath, "-NOCD") + standardOutput = stdout } + makensis.toFile().setExecutable(true) + logger.info("$prefix Finished building distribution: '$name'") + stdout.toString().lines().forEach { logger.info("$prefix $it") } } } -val distTGZ = tasks.register("distTGZ") { - group = "Distribution" - description = "Create the XDK .tar.gz file" - dependsOn(build) - dependsOn(prepareDirs) - - archiveFileName.set("xdk-${distName}.tar.gz") - destinationDirectory.set(file("$distDir/")) - compression = Compression.GZIP - from("$buildDir/") { - include("xdk/**") +val test by tasks.existing { + val sanityCheckRuntime = getXdkPropertyBoolean("org.xtclang.build.sanityCheckRuntime", false) + if (sanityCheckRuntime) { + logger.lifecycle("$prefix Sanity check runtimes after build: $sanityCheckRuntime.") + dependsOn(gradle.includedBuild("manualTests").task(":runXtc")) } } -val distZIP = tasks.register("distZIP") { - group = "Distribution" - description = "Create the XDK .zip file" - - dependsOn(build) - dependsOn(prepareDirs) - - archiveFileName.set("xdk-${distName}.zip") - destinationDirectory.set(file("$distDir/")) - from("$buildDir/") { - include("xdk/**") +/** + * Take the output of assembleDist and put it in an installation directory. + */ +val installDist by tasks.existing { + doLast { + logger.info("$prefix '$name' Installed distribution to '${project.layout.buildDirectory.get()}/install/' directory.") + logger.info("$prefix Installation files:") + printTaskOutputs(INFO) } } -val distEXE = tasks.register("distEXE") { - group = "Distribution" - description = "Create the XDK .exe file (Windows installer)" +/** + * Overwrite or install a local distribution of the XDK under rootProjectDir/build/dist, which + * you could add to your path, for example + * + * TODO: @aalmiray recommends application plugin run script generation, and that makes sense to me. + * It should be possible to hook up JReleaser and/or something like launch4j to cross compile + * binary launchers for different platforms, if that is what we want instead of the current + * solution. + */ +val installLocalDist by tasks.registering { + group = DISTRIBUTION_TASK_GROUP + description = "Creates an XDK installation in root/build/dist, for the current platform." + + val localDistDir = compositeRootBuildDirectory.dir("dist") - dependsOn(build) - dependsOn(prepareDirs) + dependsOn(installDist) + inputs.files(installDist) + outputs.dir(localDistDir) doLast { - val output = java.io.ByteArrayOutputStream() - project.exec { - commandLine("which", "makensis") - standardOutput = output - setIgnoreExitValue(true) - } - if (output.toString().trim().length > 0) { - // notes: - // - requires NSIS to be installed (e.g. "sudo apt install nsis" works on Debian/Ubuntu) - // - requires the "makensis" command to be in the path - // - requires the EnVar plugin to be installed (i.e. unzipped) into NSIS - - val src = file("src/main/nsi/xdkinstall.nsi") - val dest = "${distDir}/xdk-${distName}.exe" - val ico = "${launcherMain}/c/x.ico" - - project.exec { - environment("NSIS_SRC", "${xdkDir}") - environment("NSIS_ICO", "${ico}") - environment("NSIS_VER", "${distName}") - environment("NSIS_OUT", "${dest}") - commandLine("makensis", "${src}", "-NOCD") - } + // Sync, not copy, so we can do this declaratively, Gradle input/output style, without horrible file system logic. + sync { + from(project.layout.buildDirectory.dir("install/xdk")) + into(localDistDir) } - else { - println("*** Failure building \"distEXE\": Missing \"makensis\" command") + + // Create symlinks for launcher. + val binDir = mkdir(localDistDir.get().dir("bin")) + val launcherExe = xdkDist.resolveLauncherFile(localDistDir) + + // TODO: The launchers should just be application plugin scripts, this is kind of ridiculous. + listOf("xcc", "xec", "xtc").forEach { + val symLink = File(binDir, it) + logger.info("$prefix Creating symlink for launcher '$it' -> '${launcherExe.asFile}' (on Windows, this may require developer mode settings).") + Files.createSymbolicLink(symLink.toPath(), launcherExe.asFile.toPath()) } } } - -tasks.register("dist") { - group = "Distribution" - description = "Create the various XDK distributions" - - dependsOn(prepareDirs) - dependsOn(distTGZ) - dependsOn(distZIP) - dependsOn(distEXE) -} \ No newline at end of file diff --git a/xdk/settings.gradle.kts b/xdk/settings.gradle.kts new file mode 100644 index 0000000000..ce067d9829 --- /dev/null +++ b/xdk/settings.gradle.kts @@ -0,0 +1,49 @@ +pluginManagement { + includeBuild("../build-logic/settings-plugins") + includeBuild("../build-logic/common-plugins") + includeBuild("../plugin") +} + +includeBuild("../manualTests") + +plugins { + id("org.xtclang.build.common") +} + +rootProject.name = "xdk" + +val xdkProjectPath = rootDir + +/** + * The explicit XDK subprojects that are built for each library included in the XDK. + */ +listOfNotNull( + "lib_ecstasy", + "lib_collections", + "lib_aggregate", + "lib_crypto", + "lib_net", + "lib_json", + "lib_jsondb", + "lib_oodb", + "lib_web", + "lib_webauth", + "lib_xenia", + "javatools_turtle", + "javatools_launcher", + "javatools_bridge" +).forEach { p -> + fun projectName(name: String): String { + return name.replace('_', '-') + } + val prefix = "[xdk]" + val path = File(xdkProjectPath.parentFile, p) + val projectName = projectName(p) + if (!path.exists()) { + throw GradleException("$prefix Can't find expected XDK project: '$projectName' (at: ${path.absolutePath})") + } + logger.info("$prefix Resolved XDK subproject '$projectName' (at: '${path.absolutePath}')") + include(":$p") + project(":$p").projectDir = path + project(":$p").name = projectName +}