Skip to content

Commit

Permalink
Replace gradle-node-plugin with own tasks
Browse files Browse the repository at this point in the history
Removing gradle-node-plugin cuts the project's dependency footprint
slightly, and enables Windows builds to succeed.

Executing `pnpm` from a Gradle task proved straightforward. Also added a
frontendInstall task, removed the separate "Install Node packages" step
from run-tests.yaml, and refined `inputs` and `outputs` for more precise
dependency tracking.

Specifically learned about using `outputs.upToDateWhen { true }` as a
means to prevent `frontendInstall` from always running from:

- https://discuss.gradle.org/t/why-does-my-task-run-even-though-non-of-its-inputs-has-changed/5350/2

---

I entertained the notion of fixing Windows builds by removing
gradle-node-plugin after a couple of days of trying to fix that plugin.

The moral of the following story:

- Sometimes you really _shouldn't_ add a dependency if you can do what
  you need to do without it.

When starting the project, I thought running Node.js from Gradle might
be tricky, so I searched for a plugin. As it turned out,
node-gradle/gradle-node-plugin was available, and seemed to work well on
macOS and Linux. However, it broke the project on Windows, because it
failed trying to execute `uname` on Windows.

I got the plugin successfully working on Windows by introducing the
following PowerShell command to replace uname (in
com.github.gradle.node.NodePlugin.addPlatform()):

```kotlin
if (name.toLowerCase().contains("windows")) {
    executable = "powershell.exe"
    args = listOf(
        "-Command",
        "& {Get-CimInstance Win32_OperatingSystem | " +
            "Select-Object -ExpandProperty OSArchitecture}"
    )
}
```

I also updated com.github.gradle.node.util.parseOsArch to handle the
"ARM 64-bit Processor" value returned by the command.

However, many tests continued to fail, which I realized was because no
`win-arm64` build of Node.js was available for the default Node.js
version 18.17.1:

- https://nodejs.org/dist/v18.17.1/

So I updated the default to 20.10.0 (and the npm version to 10.2.3,
which ships with 20.10.0). All the (very slow) tests began to pass
except for NpmProxy_integTest and YarnProxy_integTest.

After some creative debugging whereby I emitted the test-supplied values
for HTTP_PROXY and HTTPS_PROXY, I could see that Node couldn't see the
test-supplied values. Some digging revealed that past versions of Gradle
and its TestKit didn't have native support for arm64 that would allow
for setting environment variables:

- gradle/gradle: TestKit does not pass environment variables for older
  Gradle versions when running on M1 mac #22012:
  gradle/gradle#22012

It seemed that Gradle 7.5.1, the default used by gradle-node-plugin,
should be OK. However, it didn't seem to be.

And this is where the despair really started kicking in.

I tried various combinations of updating the JDK from 11 to 17.0.9,
Gradle from 7.5.1 to 8.5, and updating the build.gradle.kts file.

I thought updating to 17.0.9 might solve some problems, and it seemed as
high as I could go to still suport 7.5.1:

- https://docs.gradle.org/current/userguide/compatibility.html

One minor change to the build file involved moving merging entries from
the deprecated pluginBundle block to the existing gradlePlugin block:

- https://docs.gradle.org/current/userguide/upgrading_version_7.html#pluginbundle_dropped_in_plugin_publish_plugin

Another involved disabling the com.cinnober.gradle.semver-git plugin,
because launching subprocesses during the configuration phase is now an
error when using the configuration cache:

- https://docs.gradle.org/current/userguide/upgrading_version_7.html#use_providers_to_run_external_processes
- https://docs.gradle.org/current/userguide/configuration_cache.html#config_cache:requirements:external_processes

I also deleted the ill advised and useless org.gradle.test-retry plugin
and its configuration. The tests didn't appear to be unstable, so the
tests would run multiple times to keep failing in the same way. If they
didn't fail the same way, or sometimes passed, that'd've been even
worse. See:

- https://mike-bland.com/2023/09/13/the-inverted-test-pyramid.html

Then the build would fail while compiling Kotlin:

```text
  API version 1.3 is no longer supported; please, use version 1.4 or
  greater.
```

So I set in the build file:

```kotlin
tasks.compileKotlin {
    kotlinOptions {
        apiVersion = "1.9"
        languageVersion = "1.9"
```

I also boosted IntelliJ IDEA "Settings > Build, Execution, Deployment >
Compiler > Kotlin Compiler > Target JVM Version" setting to 17. Then I
updated the build file variable:

```kotlin
val compatibilityVersion = JavaVersion.VERSION_17
```

And then, finally, tests still failed because something funny happened
inside the Spock test framework, with a bunch of errors like:

```text
SystemVersion_integTest > use system node version and exec node, npm, npx and yarn program (#gv.version) > use system node version and exec node, npm, npx and yarn program (8.5) FAILED
    java.lang.reflect.InaccessibleObjectException: Unable to make field private final java.util.Map java.util.Collections$UnmodifiableMap.m accessible: module java.base does not "opens java.util" to unnamed module @609db546
        at org.junit.contrib.java.lang.system.EnvironmentVariables.getFieldValue(EnvironmentVariables.java:188)
        at org.junit.contrib.java.lang.system.EnvironmentVariables.getEditableMapOfVariables(EnvironmentVariables.java:150)
        at org.junit.contrib.java.lang.system.EnvironmentVariables.access$200(EnvironmentVariables.java:49)
        at org.junit.contrib.java.lang.system.EnvironmentVariables$EnvironmentVariablesStatement.restoreOriginalVariables(EnvironmentVariables.java:134)
        at org.junit.contrib.java.lang.system.EnvironmentVariables$EnvironmentVariablesStatement.evaluate(EnvironmentVariables.java:125)
        at org.junit.rules.ExternalResource$1.evaluate(ExternalResource.java:54)
        at org.spockframework.junit4.AbstractRuleInterceptor.evaluateStatement(AbstractRuleInterceptor.java:66)
    [ ...snip... ]
```

I already decided at this point to get rid of gradle-node-plugin, and
developed this change. But then I decided to go a little further.

This appears to be related to:

- spock/spockframework: Cannot create mock due to module java.base does
  not "opens java.lang.invoke" #1406
  spockframework/spock#1406

This appears to be a consequence of:

- JEP 396: Strongly Encapsulate JDK Internals by Default
  https://openjdk.org/jeps/396

This apparently shipped in Java 16, and people noticed in in Spock after
upgrading to Java 17.

Based on the information in the spock issue, I tried adding the
following `jvmArgs` entry in the build file:

```kotlin
tasks.withType(Test::class) {
    useJUnitPlatform()
    systemProperty(
        // ...snip...
    )
    jvmArgs("--add-opens", "java.base/java.util=ALL-UNNAMED")
```

Reference for `--add-opens`:

- https://docs.oracle.com/en/java/javase/17/migrate/migrating-jdk-8-later-jdk-releases.html#GUID-12F945EB-71D6-46AF-8C3D-D354FD0B1781

This actually fixed the error, but on macOS, three tests failed with:

```text
> Task :npmInstall FAILED
npm WARN tarball tarball data for npm@https://registry.npmjs.org/npm/-/npm-10.2.3.tgz (sha512-lXdZ7titEN8CH5YJk9C/aYRU9JeDxQ4d8rwIIDsvH3SMjLjHTukB2CFstMiB30zXs4vCrPN2WH6cDq1yHBeJAw==) seems to be corrupted. Trying again.
npm ERR! code EINTEGRITY
npm ERR! sha512-lXdZ7titEN8CH5YJk9C/aYRU9JeDxQ4d8rwIIDsvH3SMjLjHTukB2CFstMiB30zXs4vCrPN2WH6cDq1yHBeJAw== integrity checksum failed when using sha512: wanted sha512-lXdZ7titEN8CH5YJk9C/aYRU9JeDxQ4d8rwIIDsvH3SMjLjHTukB2CFstMiB30zXs4vCrPN2WH6cDq1yHBeJAw== but got sha512-GbUui/rHTl0mW8HhJSn4A0Xg89yCR3I9otgJT1i0z1QBPOVlgbh6rlcUTpHT8Gut9O1SJjWRUU0nEcAymhG2tQ==. (2583937 bytes)
```

The tests being:

- NpmRule_integTest. succeeds to run npm module using npm_run_ when the
  package.json file contains local npm (8.5)
- NpmTask_integTest. execute npm command using the npm version specified
  in the package.json file (8.5)
- NpxTask_integTest. execute npx command using the npm version specified
  in the package.json file (8.5)

And on Windows, after all of that, the NpmProxy and YarnProxy tests
still failed. At least the changes in this commit will allow Windows to
build now, right...?

```text
* What went wrong:
Failed to calculate the value of task ':strcalc:compileJava' property
'javaCompiler'.
WindowsRegistry is not supported on this operating system.
```

Great:

- gradle/native-platform: WindowsRegistry is not supported on this
  operating system. #274
  gradle/native-platform#274 (comment)
- gradle/gradle: Support ARM64 Windows #21703
  gradle/gradle#21703

So much for Windows on arm64 for now.

This was obviously quite the learning journey, but it's only reminded me
again of how much I hate the Java ecosystem. Yet more days I wish I had
back in my life.
  • Loading branch information
mbland committed Dec 23, 2023
1 parent 1e0bac9 commit 8526cc7
Show file tree
Hide file tree
Showing 4 changed files with 29 additions and 45 deletions.
3 changes: 0 additions & 3 deletions .github/workflows/run-tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,6 @@ jobs:
- name: Setup Gradle
uses: gradle/gradle-build-action@v2

- name: Install Node packages
run: ./gradlew --warning-mode=fail pnpmInstall

- name: Build and test
run: ./gradlew --warning-mode=fail build

Expand Down
26 changes: 4 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,26 +47,10 @@ application deployment).
### OS Compatibility

I run Arm64/aarch64 builds of [Ubuntu Linux][] and [Windows 11 Professional][]
under [Parallels Desktop for Mac][] on Apple Silicon. There's some work to allow
WebDriver to use [Chromium][] or [Firefox][] on Linux, as no aarch64 build of
[Google Chrome][] is available. The [node-gradle/gradle-node-plugin][] breaks on
Windows 11 when it tries to execute `uname`:

```text
PS C:\Users\msb\src\mbland\tomcat-servlet-testing-example> ./gradlew.bat
Starting a Gradle Daemon, 1 incompatible and 1 stopped Daemons could not be
reused, use --status for details
FAILURE: Build failed with an exception.
* Where:
Build file 'C:\Users\msb\src\mbland\tomcat-servlet-testing-example\strcalc\build.gradle.kts' line: 8
* What went wrong:
An exception occurred applying plugin request [id: 'com.github.node-gradle.node']
> Failed to apply plugin 'com.github.node-gradle.node'.
> A problem occurred starting process 'command 'uname''
```
under [Parallels Desktop for Mac][] on Apple Silicon. `vite.config.js` contains
a workaround to allow WebDriver to use [Chromium][] or [Firefox][] on Linux, as
no aarch64 build of [Google Chrome][] is available. (I hope to contribute
this workaround upstream, at which point I'll remove it from `vite.config.js`.)

Also, [it doesn't appear as though nested virtualzation will ever be supported by
the aarch 64 Windows 11 on an Apple M1][no-vm-nesting]. This means the
Expand Down Expand Up @@ -802,7 +786,6 @@ TODO(mbland): Document how the following are configured:
- [Selenium: Design patterns and development strategies][]
- [TestTomcat](./strcalc/src/test/java/com/mike_bland/training/testing/utils/TestTomcat.java)
(for medium tests)
- [node-gradle/gradle-node-plugin][]

## Implementing core logic using Test Driven Development and unit tests

Expand All @@ -821,7 +804,6 @@ Coming soon...
[headless Chrome]: https://developer.chrome.com/blog/headless-chrome/
[test doubles]: https://mike-bland.com/2023/09/06/test-doubles.html
[Apache Solr]: https://solr.apache.org/
[node-gradle/gradle-node-plugin]: https://github.com/node-gradle/gradle-node-plugin
[Ubuntu Linux]: https://ubuntu.com/desktop
[Windows 11 Professional]: https://kb.parallels.com/125375/
[Parallels Desktop for Mac]: https://www.parallels.com/products/desktop/
Expand Down
1 change: 0 additions & 1 deletion settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
plugins {
// Apply the foojay-resolver plugin to allow automatic download of JDKs
id("org.gradle.toolchains.foojay-resolver-convention") version "0.7.0"
id("com.github.node-gradle.node") version "7.0.1" apply false
id("io.freefair.lombok") version "8.4" apply false
id("com.github.ben-manes.versions") version "0.50.0" apply false
}
Expand Down
44 changes: 25 additions & 19 deletions strcalc/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import com.github.benmanes.gradle.versions.updates.DependencyUpdatesTask
plugins {
war
jacoco
id("com.github.node-gradle.node")
id("io.freefair.lombok")
id("com.github.ben-manes.versions")
}
Expand Down Expand Up @@ -51,45 +50,52 @@ jacoco {
toolVersion = "0.8.11"
}

// The frontend sources are under src/main/frontend. The Node plugin generates
// the pnpm_build and pnpm_test-ci task definitions from the "scripts" field of
// package.json.
node {
nodeProjectDir = file("src/main/frontend")
}

// Set inputs and outputs for the pnpm_build and pnpn_test-ci frontend tasks.
// This enables Gradle to cache the results instead of executing these tasks
// unconditionally on every build.
// Set inputs and outputs for the frontend tasks. This enables Gradle to cache
// the results instead of executing these tasks unconditionally on every build.
//
// The vite.config.js file in src/main/frontend specifies:
//
// - The output directory, build/webapp
// - The default test results directory, build/test-results/test-frontend
// - The coverage directory, build/reports/frontend/coverage
//
// The vite.config.ci-browser.js file, used by pnpm_test-ci, also specifies
// The vite.config.ci-browser.js file, used by frontendTest, also specifies
// the build/test-results/test-frontend-browser directory.
val frontendDir = project.layout.projectDirectory.dir("src/main/frontend")
val frontendSources = fileTree(frontendDir) {
exclude("node_modules", ".*", "*.md")
}
val frontendOutputDir = project.layout.buildDirectory.dir("webapp").get()
fun frontendCmd(task: Exec, vararg commands: String) {
task.workingDir(frontendDir)
task.commandLine("pnpm", *commands)
}

val frontendBuild = tasks.named<Task>("pnpm_build") {
val frontendInstall = tasks.register<Exec>("frontendInstall") {
description = "Install src/main/frontend npm packages"
inputs.files(frontendDir.files("package.json", "pnpm-lock.yaml"))
outputs.upToDateWhen { true }
frontendCmd(this, "install")
}

val frontendBuild = tasks.register<Exec>("frontendBuild") {
description = "Build src/main/frontend JavaScript into build/webapp"
inputs.files(frontendSources)
dependsOn(frontendInstall)
inputs.files(frontendSources.filter { !it.name.endsWith(".test.js") })
outputs.dir(frontendOutputDir)
frontendCmd(this, "build")
}

val frontendTest = tasks.named<Task>("pnpm_test-ci") {
val frontendTest = tasks.register<Exec>("frontendTest") {
description = "Test frontend JavaScript in src/main/frontend"
dependsOn(frontendBuild)
inputs.files(frontendSources)
frontendCmd(this, "test-ci")

val resultsDir = java.testResultsDir.get()
outputs.dir(resultsDir.dir("test-frontend"))
outputs.dir(resultsDir.dir("test-frontend-browser"))
outputs.dirs(
resultsDir.dir("test-frontend"),
resultsDir.dir("test-frontend-browser")
)
}

// Configure the "war" task generated by the Gradle War plugin to depend upon
Expand Down Expand Up @@ -219,7 +225,7 @@ val relativeToRootDir = fun(absPath: java.nio.file.Path): java.nio.file.Path {
val mergeTestReports = fun(resultsDir: File) {
val taskName = resultsDir.name

// The `pnpm_test-ci` output is already merged. Trying to merge it again
// The `frontendTest` output is already merged. Trying to merge it again
// results in an empty file, so skip it.
if (taskName.startsWith("test-frontend")) return

Expand Down

0 comments on commit 8526cc7

Please sign in to comment.