我目前正在尝试将项目从Gradle 3.5迁移到最新的Gradle版本4.9,并且遇到了问题,即Gradle找不到执行JUnit测试所需的资源。这些资源是预先生成的,并且包含一些配置文件和参考数据。
该项目使用npm和Gradle的组合进行编译。通常,我们运行一个npm脚本,该脚本使用run-s依次执行几个任务,例如webpack构建以生成一些静态资源。最后,我们使用Gradle将所有内容一起编译到一个jar中。最后的Gradle任务还执行junit测试。此步骤失败,并使整个构建失败。 Gradle赛跑者抱怨找不到一些资源,尽管将它们正确地复制到了正确的位置。
当我在最后一个Gradle构建步骤失败之后立即通过调用./gradlew test --rerun-tasks
执行Gradle包装器时,它将成功执行JUnit测试。
在使用Gradle 3.5时,它就像一个吊饰。我知道,Gradle 4.x中的目录结构已更改,但这似乎不是问题。
我的怀疑是,这与我们通常运行整个构建过程的方式有关。与npm相结合。
该项目使用了node.js 8.11.3随附的npm verison 5.6.0。项目中有两个package.json
文件,一个在项目的根级别,一个在子目录中。这是package.json
在根级别上的样子(我删除了一些相关的部分,例如存储库URL等):
{
"name": "foobar",
"version": "1.0.0",
"scripts": {
"reloaded:production:build": "cd src/main/webapp-reloaded && npm run production:build",
"prod:build": "run-s clean:build gradle:translate build:assets prod:build:js reloaded:production:build && ./gradlew --refresh-dependencies --info --warning-mode all prod",
"prod:build:js": "run-s lint prod:build:js:client",
"prod:build:js:client": "webpack --progress --config config/webpack/production.config.js",
"build:assets": "run-s clean:generated generate:assets",
"generate:assets": "run-p generate:soyTemplates generate:icons generate:reactIcons generate:translations",
"generate:icons": "node config/tasks/generateIcons.js",
"generate:reactIcons": "mde-react-icons --files node_modules/mde-icons/svgmin/*.svg --dest src/main/webapp/generated/react-icons --color",
"generate:soyTemplates": "node config/tasks/compileSoy.js",
"gradle:translate": "./gradlew --refresh-dependencies --info --warning-mode all translate",
"clean:build": "rimraf build",
"clean:generated": "rimraf src/main/webapp/generated",
"lint": "eslint src/main/webapp/js/**/*.js"
},
"repository": {
"type": "git",
"url": "..."
},
"contributors": [
"See README.md"
],
"license": "UNLICENSED",
"private": true,
"dependencies": {
"autobind-decorator": "^1.4.0",
"axios": "^0.15.3",
"babel-polyfill": "^6.23.0",
"classnames": "^2.2.5",
"google-maps": "~3.2.1",
"intl": "^1.2.5",
"jquery": "^2.2.4",
"jquery-cycle2": "^2.1.6",
"jquery-lazyload": "^1.9.4",
"lodash": "^4.17.4",
"prop-types": "^15.6.0",
"raf": "^3.4.0",
"react": "^16.2.0",
"react-autocomplete": "^1.7.2",
"react-dom": "^16.2.0",
"react-intl": "^2.3.0",
"react-modal": "^3.1.10",
"react-redux": "^5.0.3",
"redux": "^3.6.0",
"redux-debounced": "^0.4.0",
"redux-form": "^7.2.1",
"redux-thunk": "^2.2.0",
"slick-carousel": "~1.6.0",
"whatwg-fetch": "^2.0.3"
},
"devDependencies": {
"async": "^2.1.5",
"babel-core": "^6.23.1",
"babel-eslint": "^7.2.3",
"babel-loader": "^6.3.2",
"babel-plugin-lodash": "^3.2.11",
"babel-plugin-transform-decorators-legacy": "^1.3.4",
"babel-plugin-transform-object-rest-spread": "^6.23.0",
"babel-preset-latest": "^6.22.0",
"babel-preset-react": "^6.23.0",
"babel-preset-react-optimize": "^1.0.1",
"copy": "^0.3.0",
"css-loader": "^0.28.9",
"eslint": "^3.16.1",
"eslint-plugin-react": "^6.10.0",
"expose-loader": "^0.7.3",
"extract-text-webpack-plugin": "^2.1.0",
"file-loader": "^0.11.2",
"less": "^2.7.2",
"less-loader": "^4.0.0",
"less-plugin-autoprefix": "~1.5.1",
"less-plugin-clean-css": "~1.5.1",
"npm-run-all": "^4.0.2",
"postcss-cssnext": "^3.1.0",
"postcss-import": "^11.0.0",
"postcss-loader": "^2.0.10",
"properties-reader": "0.0.15",
"react-hot-loader": "^3.0.0-beta.6",
"rimraf": "^2.6.1",
"stmux": "^1.5.4",
"style-loader": "^0.19.1",
"webpack": "^2.6.1",
"webpack-dashboard": "^0.3.0",
"webpack-dev-server": "^2.11.1",
"webpack-manifest-plugin": "~1.3.1",
"webpack-merge": "^4.1.0"
},
"optionalDependencies": {
"ttab": "^0.6.1"
}
}
子目录中的package.json
如下所示:
{
"name": "foobar-reloaded",
"version": "0.0.1",
"main": "index.js",
"scripts": {
"tab:js:client": "ttab -t 'Client' npm run dev:build:js:client",
"tab:js:server": "ttab -t '⚛React Renderer' npm run dev:build:js:server",
"tab:server": "ttab -t '☕Java Server' 'cd ../../..; ./gradlew run --no-daemon'",
"production:build": "run-s test:coverage jenkins:flow production:build:js",
"production:build:js": "run-p production:build:js:client production:build:js:server",
"production:build:js:client": "cross-env BUILD_ENV=production NODE_ENV=production webpack --color --config config/webpack/client.config.js",
"production:build:js:server": "cross-env BUILD_ENV=production NODE_ENV=production webpack --config config/webpack/server.config.js",
"flow": "flow",
"jenkins:flow": "flow check",
"lint": "eslint --fix src config e2e/src",
"test": "cross-env NODE_ENV=testing jest",
"test:coverage": "cross-env NODE_ENV=testing jest --coverage",
"test:watch": "cross-env NODE_ENV=testing jest --watch",
"format": "prettier --config .prettierrc --write \"src/**/*.js\" \"config/**/*.js\" \"src/**/*.css\"",
"precommit": "lint-staged",
"prepush": "eslint src config && npm run test:coverage && flow",
},
"repository": {
"type": "git",
},
"lint-staged": {
"*.js": [
"prettier --config .prettierrc --write",
"eslint --fix",
"git add"
],
"*.css": [
"prettier --config .prettierrc --write",
"git add"
]
},
"dependencies": {
"classnames": "^2.2.6",
"file-loader": "^1.1.11",
"history": "^4.7.2",
"intl": "^1.2.5",
"isomorphic-fetch": "^2.2.1",
"lodash": "^4.17.5",
"prop-types": "^15.6.1",
"query-string": "^5.1.1",
"react": "16.2.0",
"react-dom": "16.2.0",
"react-helmet": "^5.2.0",
"react-intl": "^2.4.0",
"react-redux": "^5.0.7",
"react-router-dom": "^4.3.1",
"react-router-scroll-4": "^1.0.0-beta.1",
"redux": "^3.7.2",
"redux-thunk": "^2.3.0",
"serialize-javascript": "^1.4.0",
"testcafe-reporter-html": "^1.1.3",
"trackjs": "^2.10.2",
"url": "^0.11.0"
},
"devDependencies": {
"babel-cli": "^6.26.0",
"babel-core": "^6.26.0",
"babel-eslint": "^8.2.2",
"babel-jest": "^22.4.3",
"babel-loader": "^7.1.4",
"babel-plugin-lodash": "^3.3.4",
"babel-plugin-syntax-dynamic-import": "^6.18.0",
"babel-plugin-transform-decorators-legacy": "^1.3.5",
"babel-plugin-transform-object-rest-spread": "^6.26.0",
"babel-plugin-transform-regenerator": "^6.26.0",
"babel-plugin-transform-runtime": "^6.23.0",
"babel-polyfill": "^6.26.0",
"babel-preset-env": "^1.6.1",
"babel-preset-flow": "^6.23.0",
"babel-preset-react": "^6.24.1",
"cross-env": "^5.2.0",
"css-loader": "^0.28.11",
"enzyme": "^3.2.0",
"enzyme-adapter-react-16": "^1.1.1",
"enzyme-to-json": "^3.3.3",
"eslint": "^4.19.1",
"eslint-config-prettier": "^2.9.0",
"eslint-config-standard": "^11.0.0",
"eslint-plugin-flowtype": "^2.49.3",
"eslint-plugin-import": "^2.10.0",
"eslint-plugin-node": "^6.0.1",
"eslint-plugin-promise": "^3.8.0",
"eslint-plugin-react": "^7.9.1",
"eslint-plugin-standard": "^3.0.1",
"extract-text-webpack-plugin": "^3.0.2",
"flow-bin": "^0.73.0",
"husky": "^0.14.3",
"npm-delay": "^1.0.4",
"identity-obj-proxy": "^3.0.0",
"isomorphic-style-loader": "^4.0.0",
"jest": "^22.4.3",
"lint-staged": "^7.2.0",
"npm-run-all": "^4.1.2",
"postcss-cssnext": "^3.0.2",
"postcss-import": "^11.0.0",
"postcss-loader": "^2.1.3",
"prettier": "1.10.2",
"raw-loader": "^0.5.1",
"react-hot-loader": "^3.1.3",
"stmux": "^1.5.4",
"storybook-readme": "^3.3.0",
"style-loader": "^0.20.3",
"testcafe": "^0.19.1",
"testcafe-react-selectors": "^2.1.0",
"webpack": "^3.11.0",
"webpack-blocks": "1.0.0-rc.2",
"webpack-bundle-analyzer": "^2.13.1",
"webpack-dev-server": "^2.11.2",
"webpack-manifest-plugin": "^1.3.2"
},
"optionalDependencies": {
"ttab": "^0.6.1"
},
"jest": {
"collectCoverageFrom": [
"src/**/*.js",
"!src/**/index.js",
"!node_modules/**"
],
"testMatch": [
"**/?(*\\.)(test)\\.js?(x)"
],
"setupFiles": [
"./config/jest/shim.js",
"./config/jest/setup.js"
],
"snapshotSerializers": [
"enzyme-to-json/serializer"
],
"moduleDirectories": [
"node_modules",
"src"
],
"moduleNameMapper": {
"\\.css$": "identity-obj-proxy"
},
"transform": {
"^.+\\.js$": "babel-jest"
},
"transformIgnorePatterns": [
"node_modules/(?!@mde-ui|@trackking).+\\.js$"
]
}
}
位于项目根目录的build.gradle
文件如下:
import de.foobar.xliff.Translator
import foobar.Refdata
import java.time.LocalDateTime
import java.time.temporal.ChronoUnit
import java.util.function.UnaryOperator
import static java.time.temporal.ChronoUnit.*
apply plugin: 'java'
apply plugin: 'application'
apply plugin: 'com.github.johnrengelman.shadow'
apply plugin: 'findbugs'
apply plugin: 'pmd'
apply plugin: 'com.github.ben-manes.versions'
apply plugin: 'de.undercouch.download'
apply plugin: 'idea'
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
applicationDefaultJvmArgs = ["-XX:+UnlockCommercialFeatures", "-XX:+FlightRecorder"]
mainClassName = "foobar.app.Server"
def resourcesDir = sourceSets.main.output.resourcesDir
def xliffDir = file("$resourcesDir/xliff")
def libDir = file("$rootProject.projectDir/lib")
def soyToolsDir = file("$libDir/soyTools")
tasks.withType(FindBugs) {
reports {
xml.enabled = false
html.enabled = true
}
}
findbugs {
toolVersion = '3.0.1'
excludeFilter = file("$rootProject.projectDir/config/findbugs-exclude.xml")
sourceSets = [sourceSets.main]
showProgress = true
}
pmd {
toolVersion = '6.2.0'
ruleSets = []
ruleSetFiles = files("$rootProject.projectDir/config/pmd.xml")
}
tasks.withType(JavaCompile) {
options.compilerArgs << '-Xlint:unchecked'
options.encoding = 'UTF-8'
}
ext {
gradle_version = '4.9'
tomcat_version = '8.0.51'
slf4j_version = '1.7.25'
log4j2_version = '2.11.0'
metrics_version = '3.2.6'
guice_version = '4.2.0'
rewrite_version = '2.0.12.Final'
soytemplates_version = '2017-08-08'
guava_version = '24.1.1-jre'
optimizely_version = '1.8.0'
build_time = LocalDateTime.now().truncatedTo(MINUTES).toString()
generatedSrcDir = "src/main/webapp/generated"
}
repositories {
flatDir {
dirs 'lib'
}
maven { url '...' }
mavenLocal()
}
buildscript {
repositories {
maven { url '...' }
mavenLocal()
}
dependencies {
classpath 'args4j:args4j:2.33'
classpath 'com.github.jengelman.gradle.plugins:shadow:2.0.1'
classpath 'com.github.ben-manes:gradle-versions-plugin:0.15.0'
classpath 'de.undercouch:gradle-download-task:3.2.0'
}
}
configurations.all {
resolutionStrategy.eachDependency {
if (it.requested.name == 'commons-logging') {
it.useTarget 'org.slf4j:jcl-over-slf4j:1.7.16'
}
}
}
dependencies {
compile group: 'org.apache.logging.log4j', name: 'log4j-core', version: log4j2_version
compile group: 'org.apache.logging.log4j', name: 'log4j-slf4j-impl', version: log4j2_version
compile group: 'org.slf4j', name: 'jul-to-slf4j', version: slf4j_version
compile group: 'com.google.template', name: 'soy', version: soytemplates_version
compile group: 'com.google.code.gson', name: 'gson', version: '2.8.3'
compile group: 'com.google.inject', name: 'guice', version: guice_version
compile group: 'com.google.inject.extensions', name: 'guice-multibindings', version: guice_version
compile group: 'com.google.inject.extensions', name: 'guice-assistedinject', version: guice_version
compile group: 'com.google.guava', name: 'guava', version: guava_version
compile group: 'org.apache.tomcat.embed', name: 'tomcat-embed-core', version: tomcat_version
compile group: 'org.apache.tomcat.embed', name: 'tomcat-embed-logging-juli', version: tomcat_version
compile group: 'io.dropwizard.metrics', name: 'metrics-core', version: metrics_version
compile group: 'io.dropwizard.metrics', name: 'metrics-servlets', version: metrics_version
compile group: 'io.dropwizard.metrics', name: 'metrics-graphite', version: metrics_version
compile group: 'org.ocpsoft.rewrite', name: 'rewrite-servlet', version: rewrite_version
compile group: 'org.ocpsoft.rewrite', name: 'rewrite-config-proxy', version: rewrite_version
compile group: 'com.google.template', name: 'soy', version: soytemplates_version
compile group: 'com.optimizely.ab', name: 'core-api', version: optimizely_version
compile group: 'com.optimizely.ab', name: 'core-httpclient-impl', version: optimizely_version
compile name: 'j2v8_linux_x86_64-master-4.9.0-SNAPSHOT'
compile name: 'j2v8_macos_x86_64-master-4.9.0-SNAPSHOT'
compile group: 'com.spencerwi', name: 'Either.java', version: '1.1.0'
compile group: 'org.apache.commons', name: 'commons-pool2', version: '2.4.2'
compile 'org.apache.httpcomponents:httpcore:4.4.9'
compile 'org.apache.httpcomponents:httpclient:4.5.5'
compile 'com.squareup.retrofit:retrofit:1.8.2'
compile 'io.reactivex:rxjava:1.3.8'
compile 'io.reactivex:rxjava-string:1.1.1'
runtime group: 'com.lmax', name: 'disruptor', version: '3.3.6'
testCompile group: 'org.hamcrest', name: 'hamcrest-all', version: '1.3'
testCompile group: 'junit', name: 'junit', version: '4.12'
testCompile group: 'org.easytesting', name: 'fest-assert', version: '1.4'
testCompile group: 'org.mockito', name: 'mockito-core', version: '1.10.19'
}
shadowJar {
baseName = rootProject.name
classifier = ''
exclude 'META-INF/org/apache/logging/log4j/core/config/plugins/Log4j2Plugins.dat'
}
// build custom Soy functions to compile to Soy JavaScript, to run again if new functions are added
task soyFunctions(dependsOn: classes, type: Jar) {
baseName = "soyFunctions"
from sourceSets.main.output
include "foobar/soy/Functions*"
destinationDir = libDir
}
task soyExtractMsgs(type: Exec) {
def relativeSoyFilesPrefix = "src/main/resources/soy"
def absoluteSoyFilesPrefix = file(relativeSoyFilesPrefix).path + "/"
List<String> inputs = new FileNameFinder()
.getFileNames(relativeSoyFilesPrefix, "**/*.soy")
// absolute paths to relative (to absoluteSoyFilesPrefix) paths
inputs.replaceAll(new UnaryOperator<String>() {
@Override
String apply(String s) {
return s.replace(absoluteSoyFilesPrefix, "")
}
})
def soyArgs = ([
"--outputFile", "$xliffDir/messages.xlf".toString(),
"--sourceLocaleString", "de",
"--inputPrefix", absoluteSoyFilesPrefix
] << inputs).flatten()
def commandArgs = soyArgs.join(" ")
commandLine "sh", "-c", "java -jar $soyToolsDir/SoyMsgExtractor.jar ${commandArgs}"
// leave empty -> https://discuss.gradle.org/t/why-does-a-type-exec-task-require-that-one-not-use/6871/4
doLast {
}
}
task soyTranslate {
doLast {
def trArgs = [
"--src", "$xliffDir/messages.xlf".toString(),
"--mappingSrc", "config/soy/passolo-mapping.json",
"--targetDir", "$xliffDir".toString()
]
Translator.main(trArgs.toArray(new String[trArgs.size()]))
}
}
task soyTranslateAll {
mkdir(xliffDir)
dependsOn soyExtractMsgs
dependsOn soyTranslate
// leave empty -> https://discuss.gradle.org/t/why-does-a-type-exec-task-require-that-one-not-use/6871/4
doLast {
}
}
// manual task to download latest soy tools
task soyDownloadTools {
def soyMavenUrl = "http://repo1.maven.org/maven2/com/google/template/soy/$soytemplates_version"
def soyMsgExtractorName = 'SoyMsgExtractor.jar'
def soyToJsSrcCompilerName = 'SoyToJsSrcCompiler.jar'
def soyFilePrefix = "soy-$soytemplates_version"
def soyMsgExtractorFileName = "$soyFilePrefix-$soyMsgExtractorName"
def soyJsSrcCompilerFileName = "$soyFilePrefix-$soyToJsSrcCompilerName"
def soyJarFiles = [
soyMsgExtractorName:"$soyMavenUrl/$soyMsgExtractorFileName",
soyToJsSrcCompilerName:"$soyMavenUrl/$soyJsSrcCompilerFileName"
]
doLast {
delete soyToolsDir
for (soyJarFile in soyJarFiles) {
download {
def destinationFileName = soyJarFile.key
def sourceFileName = soyJarFile.value
src sourceFileName
dest new File(soyToolsDir, destinationFileName)
onlyIfNewer true
acceptAnyCertificate true
}
}
}
}
task soyUpgrade {
dependsOn soyDownloadTools
dependsOn soyFunctions
}
// deprecated, use soyTranslateAll instead, will be removed soon
task translate {
dependsOn soyTranslateAll
// leave empty -> https://discuss.gradle.org/t/why-does-a-type-exec-task-require-that-one-not-use/6871/4
doLast {
}
}
task generateReferenceData {
doFirst {
def outDir = new File("build/tmp/resources/main/refdata")
def list = ['1',
'2']
list.each { l ->
Refdata.translateJson(new File("config/refdata/${l}.json"), outDir)
}
}
}
task copyAssets(type: Copy) {
destinationDir = resourcesDir
from('src/main/resources/soy') {
into("soy")
}
from('node_modules/react-header/dist/') {
include '*.html'
into("header")
}
from('node_modules/mde-react-footer/dist/') {
include '*.html'
into("footer")
}
from('build/tmp/resources/main/static') {
into("static")
}
from('build/tmp/resources/main/refdata') {
into("refdata")
}
doLast {
println "copied assets to ${destinationDir}"
}
}
task copyTestAssets(type: Copy) {
destinationDir = sourceSets.test.output.resourcesDir
from(resourcesDir)
from("${rootProject.projectDir}/src/test/resources")
doLast {
println "copied test assets to ${destinationDir}"
}
}
task copyTestAssets2(type: Copy) {
destinationDir = new File("${buildDir}/classes/java/test")
from(resourcesDir)
from("${rootProject.projectDir}/src/test/resources")
doLast {
println "copied test assets to ${destinationDir}"
}
}
task showProjectProps {
doLast {
println(project.ext.properties)
}
}
task showBuildscriptClassPath {
doLast {
buildscript.configurations.classpath.each { println it }
}
}
task prod {
dependsOn build
dependsOn soyTranslateAll
dependsOn shadowJar
}
defaultTasks 'shadowJar'
processResources {
dependsOn generateReferenceData
}
processTestResources {
dependsOn generateReferenceData
}
compileTestJava {
dependsOn copyAssets
dependsOn copyTestAssets
dependsOn copyTestAssets2
}
shadowJar {
dependsOn soyTranslateAll
dependsOn copyAssets
}
构建项目时,我们只调用npm i && npm run prod:build
即可触发几个构建步骤,并在最后基本执行./gradlew prod
。最后一步还执行了测试。这些测试的前提条件是复制同样执行的资产,并且所有资产都在相应的目标目录中。尽管如此,在执行测试时,Gradle会抛出错误,指出找不到已生成且位于正确位置的参考数据。
在./gradlew test --rerun-tasks
失败后执行npm i && npm run prod:build
时,将找到参考数据以及其他资源,并且测试成功执行。