diff --git a/.gitattributes b/.gitattributes index f63e59aa..ed246870 100644 --- a/.gitattributes +++ b/.gitattributes @@ -23,7 +23,7 @@ *.scm text *.sql text *.sh text -*.bat text +*.bat text eol=crlf # templates *.ejs text @@ -92,3 +92,4 @@ AUTHORS text *.woff binary *.pyc binary *.pdf binary +*.jar binary diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 70796c47..2f0bf006 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,7 +27,7 @@ jobs: strategy: matrix: - node-version: [16.x, 18.x, 20.x] + node-version: [16.x, 18.x, 20.x, 22.x] os: [ubuntu-latest, windows-latest, macos-latest] steps: @@ -39,7 +39,7 @@ jobs: - uses: actions/setup-java@v4 with: distribution: 'temurin' - java-version: '11' + java-version: '17' - name: Environment Information run: | diff --git a/.gitignore b/.gitignore index 2e8cc1bc..ead624c1 100644 --- a/.gitignore +++ b/.gitignore @@ -29,12 +29,8 @@ example **/assets/www/cordova.js /test/.externalNativeBuild - -/test/androidx/gradle -/test/androidx/gradlew -/test/androidx/gradlew.bat /test/androidx/cdv-gradle-config.json - +/test/androidx/tools /test/assets/www/.tmp* /test/assets/www/cordova.js /test/bin diff --git a/.ratignore b/.ratignore index 33962717..89fbaa9e 100644 --- a/.ratignore +++ b/.ratignore @@ -8,3 +8,6 @@ intermediates reports test-results node_modules +gradle +gradlew +gradlew.bat diff --git a/LICENSE b/LICENSE index c2f944b4..cc0431d9 100644 --- a/LICENSE +++ b/LICENSE @@ -187,7 +187,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2015-2020 Apache Cordova + Copyright 2015-2024 Apache Cordova Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -200,3 +200,4 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + diff --git a/framework/build.gradle b/framework/build.gradle index d1f1d4fe..02ea7e8f 100644 --- a/framework/build.gradle +++ b/framework/build.gradle @@ -50,8 +50,8 @@ android { buildToolsVersion cordovaConfig.BUILD_TOOLS_VERSION compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaLanguageVersion.of(cordovaConfig.JAVA_SOURCE_COMPATIBILITY) + targetCompatibility JavaLanguageVersion.of(cordovaConfig.JAVA_TARGET_COMPATIBILITY) } // For the Android Cordova Lib, we allow changing the minSdkVersion, but it is at the users own risk diff --git a/framework/cdv-gradle-config-defaults.json b/framework/cdv-gradle-config-defaults.json index 6ccf469e..2895afc7 100644 --- a/framework/cdv-gradle-config-defaults.json +++ b/framework/cdv-gradle-config-defaults.json @@ -1,10 +1,10 @@ { "MIN_SDK_VERSION": 24, - "SDK_VERSION": 33, + "SDK_VERSION": 34, "COMPILE_SDK_VERSION": null, - "GRADLE_VERSION": "7.6", - "MIN_BUILD_TOOLS_VERSION": "33.0.2", - "AGP_VERSION": "7.4.2", + "GRADLE_VERSION": "8.7", + "MIN_BUILD_TOOLS_VERSION": "34.0.0", + "AGP_VERSION": "8.3.0", "KOTLIN_VERSION": "1.7.21", "ANDROIDX_APP_COMPAT_VERSION": "1.6.1", "ANDROIDX_WEBKIT_VERSION": "1.6.0", @@ -12,5 +12,8 @@ "GRADLE_PLUGIN_GOOGLE_SERVICES_VERSION": "4.3.15", "IS_GRADLE_PLUGIN_GOOGLE_SERVICES_ENABLED": false, "IS_GRADLE_PLUGIN_KOTLIN_ENABLED": false, - "PACKAGE_NAMESPACE": "io.cordova.helloCordova" + "PACKAGE_NAMESPACE": "io.cordova.helloCordova", + "JAVA_SOURCE_COMPATIBILITY": 8, + "JAVA_TARGET_COMPATIBILITY": 8, + "KOTLIN_JVM_TARGET": null } diff --git a/lib/builders/ProjectBuilder.js b/lib/builders/ProjectBuilder.js index 815d08f3..024a0221 100644 --- a/lib/builders/ProjectBuilder.js +++ b/lib/builders/ProjectBuilder.js @@ -25,7 +25,7 @@ const events = require('cordova-common').events; const CordovaError = require('cordova-common').CordovaError; const check_reqs = require('../check_reqs'); const PackageType = require('../PackageType'); -const { compareByAll } = require('../utils'); +const { compareByAll, isWindows } = require('../utils'); const { createEditor } = require('properties-parser'); const CordovaGradleConfigParserFactory = require('../config/CordovaGradleConfigParserFactory'); @@ -85,9 +85,7 @@ class ProjectBuilder { } getArgs (cmd, opts) { - let args = [ - '-b', path.join(this.root, 'build.gradle') - ]; + let args = []; if (opts.extraArgs) { args = args.concat(opts.extraArgs); } @@ -116,16 +114,27 @@ class ProjectBuilder { return args; } - /* - * This returns a promise - */ - runGradleWrapper (gradle_cmd) { - const gradlePath = path.join(this.root, 'gradlew'); - const wrapperGradle = path.join(this.root, 'wrapper.gradle'); - if (fs.existsSync(gradlePath)) { - // Literally do nothing, for some reason this works, while !fs.existsSync didn't on Windows + getGradleWrapperPath () { + let wrapper = path.join(this.root, 'tools', 'gradlew'); + + if (isWindows()) { + wrapper += '.bat'; + } + + return wrapper; + } + + /** + * Installs/updates the gradle wrapper + * @param {string} gradleVersion The gradle version to install. Ignored if CORDOVA_ANDROID_GRADLE_DISTRIBUTION_URL environment variable is defined + * @returns {Promise} + */ + async installGradleWrapper (gradleVersion) { + if (process.env.CORDOVA_ANDROID_GRADLE_DISTRIBUTION_URL) { + events.emit('verbose', `Overriding Gradle Version via CORDOVA_ANDROID_GRADLE_DISTRIBUTION_URL (${process.env.CORDOVA_ANDROID_GRADLE_DISTRIBUTION_URL})`); + await execa('gradle', ['-p', path.join(this.root, 'tools'), 'wrapper', '--gradle-distribution-url', process.env.CORDOVA_ANDROID_GRADLE_DISTRIBUTION_URL], { stdio: 'inherit' }); } else { - return execa(gradle_cmd, ['-p', this.root, 'wrapper', '-b', wrapperGradle], { stdio: 'inherit' }); + await execa('gradle', ['-p', path.join(this.root, 'tools'), 'wrapper', '--gradle-version', gradleVersion], { stdio: 'inherit' }); } } @@ -275,23 +284,14 @@ class ProjectBuilder { prepEnv (opts) { const self = this; + const config = this._getCordovaConfig(); return check_reqs.check_gradle() - .then(function (gradlePath) { - return self.runGradleWrapper(gradlePath); + .then(function () { + events.emit('verbose', `Using Gradle: ${config.GRADLE_VERSION}`); + return self.installGradleWrapper(config.GRADLE_VERSION); }).then(function () { return self.prepBuildFiles(); }).then(() => { - const config = this._getCordovaConfig(); - // update/set the distributionUrl in the gradle-wrapper.properties - const gradleWrapperPropertiesPath = path.join(self.root, 'gradle/wrapper/gradle-wrapper.properties'); - const gradleWrapperProperties = createEditor(gradleWrapperPropertiesPath); - const distributionUrl = process.env.CORDOVA_ANDROID_GRADLE_DISTRIBUTION_URL || `https://services.gradle.org/distributions/gradle-${config.GRADLE_VERSION}-all.zip`; - gradleWrapperProperties.set('distributionUrl', distributionUrl); - gradleWrapperProperties.save(); - - events.emit('verbose', `Gradle Distribution URL: ${distributionUrl}`); - }) - .then(() => { const signingPropertiesPath = path.join(self.root, `${opts.buildType}${SIGNING_PROPERTIES}`); if (fs.existsSync(signingPropertiesPath)) fs.removeSync(signingPropertiesPath); @@ -317,7 +317,7 @@ class ProjectBuilder { * Returns a promise. */ async build (opts) { - const wrapper = path.join(this.root, 'gradlew'); + const wrapper = this.getGradleWrapperPath(); const args = this.getArgs(opts.buildType === 'debug' ? 'debug' : 'release', opts); events.emit('verbose', `Running Gradle Build: ${wrapper} ${args.join(' ')}`); @@ -338,7 +338,7 @@ class ProjectBuilder { } clean (opts) { - const wrapper = path.join(this.root, 'gradlew'); + const wrapper = this.getGradleWrapperPath(); const args = this.getArgs('clean', opts); return execa(wrapper, args, { stdio: 'inherit', cwd: path.resolve(this.root) }) .then(() => { diff --git a/lib/create.js b/lib/create.js index 7f5d6013..303ced12 100755 --- a/lib/create.js +++ b/lib/create.js @@ -113,20 +113,15 @@ function prepBuildFiles (projectPath) { buildModule.getBuilder(projectPath).prepBuildFiles(); } -function copyBuildRules (projectPath, isLegacy) { +function copyBuildRules (projectPath) { const srcDir = path.join(ROOT, 'templates', 'project'); - if (isLegacy) { - // The project's build.gradle is identical to the earlier build.gradle, so it should still work - fs.copySync(path.join(srcDir, 'legacy', 'build.gradle'), path.join(projectPath, 'legacy', 'build.gradle')); - fs.copySync(path.join(srcDir, 'wrapper.gradle'), path.join(projectPath, 'wrapper.gradle')); - } else { - fs.copySync(path.join(srcDir, 'build.gradle'), path.join(projectPath, 'build.gradle')); - fs.copySync(path.join(srcDir, 'app', 'build.gradle'), path.join(projectPath, 'app', 'build.gradle')); - fs.copySync(path.join(srcDir, 'app', 'repositories.gradle'), path.join(projectPath, 'app', 'repositories.gradle')); - fs.copySync(path.join(srcDir, 'repositories.gradle'), path.join(projectPath, 'repositories.gradle')); - fs.copySync(path.join(srcDir, 'wrapper.gradle'), path.join(projectPath, 'wrapper.gradle')); - } + fs.copySync(path.join(srcDir, 'build.gradle'), path.join(projectPath, 'build.gradle')); + fs.copySync(path.join(srcDir, 'app', 'build.gradle'), path.join(projectPath, 'app', 'build.gradle')); + fs.copySync(path.join(srcDir, 'app', 'repositories.gradle'), path.join(projectPath, 'app', 'repositories.gradle')); + fs.copySync(path.join(srcDir, 'repositories.gradle'), path.join(projectPath, 'repositories.gradle')); + + copyGradleTools(projectPath); } function copyScripts (projectPath) { @@ -176,6 +171,12 @@ function validateProjectName (project_name) { return Promise.resolve(); } +function copyGradleTools (projectPath) { + const srcDir = path.join(ROOT, 'templates', 'project'); + + fs.copySync(path.resolve(srcDir, 'tools'), path.resolve(projectPath, 'tools')); +} + /** * Creates an android application with the given options. * diff --git a/lib/prepare.js b/lib/prepare.js index 5a7289e5..284904c4 100644 --- a/lib/prepare.js +++ b/lib/prepare.js @@ -110,7 +110,10 @@ function getUserGradleConfig (configXml) { { xmlKey: 'AndroidXWebKitVersion', gradleKey: 'ANDROIDX_WEBKIT_VERSION', type: String }, { xmlKey: 'GradlePluginGoogleServicesVersion', gradleKey: 'GRADLE_PLUGIN_GOOGLE_SERVICES_VERSION', type: String }, { xmlKey: 'GradlePluginGoogleServicesEnabled', gradleKey: 'IS_GRADLE_PLUGIN_GOOGLE_SERVICES_ENABLED', type: Boolean }, - { xmlKey: 'GradlePluginKotlinEnabled', gradleKey: 'IS_GRADLE_PLUGIN_KOTLIN_ENABLED', type: Boolean } + { xmlKey: 'GradlePluginKotlinEnabled', gradleKey: 'IS_GRADLE_PLUGIN_KOTLIN_ENABLED', type: Boolean }, + { xmlKey: 'AndroidJavaSourceCompatibility', gradleKey: 'JAVA_SOURCE_COMPATIBILITY', type: Number }, + { xmlKey: 'AndroidJavaTargetCompatibility', gradleKey: 'JAVA_TARGET_COMPATIBILITY', type: Number }, + { xmlKey: 'AndroidKotlinJVMTarget', gradleKey: 'KOTLIN_JVM_TARGET', type: String } ]; return configXmlToGradleMapping.reduce((config, mapping) => { diff --git a/package.json b/package.json index d733b228..1399d388 100644 --- a/package.json +++ b/package.json @@ -19,8 +19,7 @@ "unit-tests": "jasmine --config=spec/unit/jasmine.json", "cover": "nyc jasmine --config=spec/coverage.json", "e2e-tests": "jasmine --config=spec/e2e/jasmine.json", - "java-unit-tests": "node test/run_java_unit_tests.js", - "clean:java-unit-tests": "node test/clean.js" + "java-unit-tests": "node test/run_java_unit_tests.js" }, "author": "Apache Software Foundation", "license": "Apache-2.0", diff --git a/spec/unit/builders/ProjectBuilder.spec.js b/spec/unit/builders/ProjectBuilder.spec.js index f2cb633a..388beab5 100644 --- a/spec/unit/builders/ProjectBuilder.spec.js +++ b/spec/unit/builders/ProjectBuilder.spec.js @@ -20,6 +20,7 @@ const fs = require('fs-extra'); const path = require('path'); const rewire = require('rewire'); +const { isWindows } = require('../../../lib/utils'); describe('ProjectBuilder', () => { const rootDir = '/root'; @@ -128,19 +129,24 @@ describe('ProjectBuilder', () => { }); }); - describe('runGradleWrapper', () => { - it('should run the provided gradle command if a gradle wrapper does not already exist', () => { - spyOn(fs, 'existsSync').and.returnValue(false); - builder.runGradleWrapper('/my/sweet/gradle'); - expect(execaSpy).toHaveBeenCalledWith('/my/sweet/gradle', jasmine.any(Array), jasmine.any(Object)); + describe('installGradleWrapper', () => { + beforeEach(() => { + execaSpy.and.resolveTo(); }); - it('should do nothing if a gradle wrapper exists in the project directory', () => { - spyOn(fs, 'existsSync').and.returnValue(true); - builder.runGradleWrapper('/my/sweet/gradle'); - expect(execaSpy).not.toHaveBeenCalledWith('/my/sweet/gradle', jasmine.any(Array), jasmine.any(Object)); + it('should run gradle wrapper 8.7', async () => { + await builder.installGradleWrapper('8.7'); + expect(execaSpy).toHaveBeenCalledWith('gradle', ['-p', path.normalize('/root/tools'), 'wrapper', '--gradle-version', '8.7'], jasmine.any(Object)); + }); + + it('CORDOVA_ANDROID_GRADLE_DISTRIBUTION_URL should override gradle version', async () => { + process.env.CORDOVA_ANDROID_GRADLE_DISTRIBUTION_URL = 'https://dist.local'; + await builder.installGradleWrapper('8.7'); + delete process.env.CORDOVA_ANDROID_GRADLE_DISTRIBUTION_URL; + expect(execaSpy).toHaveBeenCalledWith('gradle', ['-p', path.normalize('/root/tools'), 'wrapper', '--gradle-distribution-url', 'https://dist.local'], jasmine.any(Object)); }); }); + describe('build', () => { beforeEach(() => { spyOn(builder, 'getArgs'); @@ -170,7 +176,12 @@ describe('ProjectBuilder', () => { builder.build({}); - expect(execaSpy).toHaveBeenCalledWith(path.join(rootDir, 'gradlew'), testArgs, jasmine.anything()); + let gradle = path.join(rootDir, 'tools', 'gradlew'); + if (isWindows()) { + gradle += '.bat'; + } + + expect(execaSpy).toHaveBeenCalledWith(gradle, testArgs, jasmine.anything()); }); it('should reject if the spawn fails', () => { @@ -227,8 +238,13 @@ describe('ProjectBuilder', () => { const gradleArgs = ['test', 'args', '-f']; builder.getArgs.and.returnValue(gradleArgs); + let gradle = path.join(rootDir, 'tools', 'gradlew'); + if (isWindows()) { + gradle += '.bat'; + } + return builder.clean(opts).then(() => { - expect(execaSpy).toHaveBeenCalledWith(path.join(rootDir, 'gradlew'), gradleArgs, jasmine.anything()); + expect(execaSpy).toHaveBeenCalledWith(gradle, gradleArgs, jasmine.anything()); }); }); diff --git a/templates/project/app/build.gradle b/templates/project/app/build.gradle index ed155ec3..4d531aae 100644 --- a/templates/project/app/build.gradle +++ b/templates/project/app/build.gradle @@ -181,6 +181,10 @@ task cdvPrintProps { android { namespace cordovaConfig.PACKAGE_NAMESPACE + buildFeatures { + buildConfig true + } + defaultConfig { versionCode cdvVersionCode ?: new BigInteger("" + privateHelpers.extractIntFromManifest("versionCode")) applicationId cordovaConfig.PACKAGE_NAMESPACE @@ -248,8 +252,39 @@ android { } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaLanguageVersion.of(cordovaConfig.JAVA_SOURCE_COMPATIBILITY) + targetCompatibility JavaLanguageVersion.of(cordovaConfig.JAVA_TARGET_COMPATIBILITY) + } + + if (cordovaConfig.IS_GRADLE_PLUGIN_KOTLIN_ENABLED) { + if (cordovaConfig.KOTLIN_JVM_TARGET == null) { + // If the value is null, fallback to JAVA_TARGET_COMPATIBILITY, + // as they generally should be equal + def javaTarget = JavaLanguageVersion.of(cordovaConfig.JAVA_TARGET_COMPATIBILITY) + + // check if javaTarget is <= 8; if so, we need to prefix it with "1." + // Starting with 9 and later, the value can be used as is. + if (javaTarget.compareTo(JavaLanguageVersion.of(8)) <= 0) { + javaTarget = "1." + javaTarget + } + + cordovaConfig.KOTLIN_JVM_TARGET = javaTarget + } + + // Similar to above, check if kotlin target is <= 8, if so prefix it. + // This allows the user to use consistent set of values in config.xml + // Rather than having to be aware whether the "1."" prefix is needed. + // This check is only done if the value isn't already prefixed with 1. + if ( + !cordovaConfig.KOTLIN_JVM_TARGET.startsWith("1.") && + JavaLanguageVersion.of(cordovaConfig.KOTLIN_JVM_TARGET).compareTo(JavaLanguageVersion.of(8)) <= 0 + ) { + cordovaConfig.KOTLIN_JVM_TARGET = "1." + cordovaConfig.KOTLIN_JVM_TARGET + } + + kotlinOptions { + jvmTarget = cordovaConfig.KOTLIN_JVM_TARGET + } } if (cdvReleaseSigningPropertiesFile) { diff --git a/test/clean.js b/templates/project/tools/settings.gradle similarity index 59% rename from test/clean.js rename to templates/project/tools/settings.gradle index 88148cdb..c6c0f3e6 100644 --- a/test/clean.js +++ b/templates/project/tools/settings.gradle @@ -17,16 +17,13 @@ under the License. */ -const fs = require('fs-extra'); -const path = require('path'); - -/** - * This script is to be run manually (e.g. by npm run clean:java-unit-tests) if - * you want to upgrade gradlew or test its proper generation. - */ - -for (const variant of ['androidx']) { - for (const file of ['gradlew', 'gradlew.bat']) { - fs.removeSync(path.join(__dirname, variant, file)); - } -} +// This is an empty project used to provide Gradle Tooling +// Using the main project which loads AGP will enforce a minimum version +// requirement on the end-user, requiring a gradle install that satisfies AGP +// version requirements. +// To avoid that, we utilise this empty project of which we can +// freely run the gradle wrapper task against to obtain the +// wrapper at the desired version, without being restricted by AGP's version +// requirements. +// Of course, the installed wrapper must still be of at least the minimum +// required version of AGP for the build to work correctly. diff --git a/templates/project/wrapper.gradle b/templates/project/wrapper.gradle deleted file mode 100644 index 88e63116..00000000 --- a/templates/project/wrapper.gradle +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -//This file is intentionally just a comment diff --git a/test/androidx/wrapper.gradle b/test/androidx/wrapper.gradle deleted file mode 100644 index 6cef6051..00000000 --- a/test/androidx/wrapper.gradle +++ /dev/null @@ -1,22 +0,0 @@ -/* Licensed to the Apache Software Foundation (ASF) under one - or more contributor license agreements. See the NOTICE file - distributed with this work for additional information - regarding copyright ownership. The ASF licenses this file - to you under the Apache License, Version 2.0 (the - "License"); you may not use this file except in compliance - with the License. You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, - software distributed under the License is distributed on an - "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - KIND, either express or implied. See the License for the - specific language governing permissions and limitations - under the License. -*/ - -wrapper { - apply from: '../../framework/cordova.gradle' - gradleVersion = cordovaConfig.GRADLE_VERSION -} diff --git a/test/run_java_unit_tests.js b/test/run_java_unit_tests.js index ba41e3a2..816ab6b0 100644 --- a/test/run_java_unit_tests.js +++ b/test/run_java_unit_tests.js @@ -28,7 +28,7 @@ class AndroidTestRunner { constructor (testTitle, projectDir) { this.testTitle = testTitle; this.projectDir = projectDir; - this.gradleWrapper = path.join(this.projectDir, 'gradlew'); + this.gradleWrapper = path.join(this.projectDir, 'tools/gradlew'); } _gradlew (...args) { @@ -42,8 +42,18 @@ class AndroidTestRunner { ); } + _getGradleVersion () { + const config = JSON.parse( + fs.readFileSync(path.resolve(this.projectDir, '../../framework/cdv-gradle-config-defaults.json'), { + encoding: 'utf-8' + }) + ); + + return config.GRADLE_VERSION; + } + _createProjectBuilder () { - return new ProjectBuilder(this.projectDir).runGradleWrapper('gradle'); + return new ProjectBuilder(this.projectDir).installGradleWrapper(this._getGradleVersion()); } run () { @@ -52,15 +62,16 @@ class AndroidTestRunner { .then(_ => { // TODO we should probably not only copy these files, but instead create a new project from scratch fs.copyFileSync(path.resolve(this.projectDir, '../../framework/cdv-gradle-config-defaults.json'), path.resolve(this.projectDir, 'cdv-gradle-config.json')); + fs.copySync(path.resolve(this.projectDir, '../../templates/project/tools'), path.resolve(this.projectDir, 'tools')); fs.copyFileSync( path.join(__dirname, '../templates/project/assets/www/cordova.js'), path.join(this.projectDir, 'app/src/main/assets/www/cordova.js') ); }) .then(_ => this._createProjectBuilder()) - .then(_ => this._gradlew('--version')) + .then(_ => this._gradlew(['--version'])) .then(_ => console.log(`[${this.testTitle}] Gradle wrapper is ready. Running tests now.`)) - .then(_ => this._gradlew('test')) + .then(_ => this._gradlew(['test'])) .then(_ => console.log(`[${this.testTitle}] Java unit tests completed successfully`)); } }