====== Gradle Organizing Build Logic ======
* [[http://www.gradle.org/docs/current/userguide/organizing_build_logic.html|Organizing Build Logic]]
* Gradle에는 빌드 스크립트 로직을 구성하는 여러 가지 방법이 있다.
* 태스크 클로저에 직접 넣기.
* 여러 태스크가 동일 로직을 사용한다면 메소드로 빼서 호출한다. 멀티 프로젝트에서 이 로직을 사용한다면 부모 프로젝트에 메소드를 선언한다.
* 메소드로 빼 빌드로직이 너무 복잡하다면 OO 모델로 바꿀 수 있다. 특정 디렉토리에 클래스를 넣어두면 Gradle이 자동으로 컴파일하여 빌드 스크립트 클래스패스에 추가한다.
===== 프라퍼티와 메소드 상속 =====
부모 프로젝트에 정의된 모든 메소드와 프라퍼티는 서브 프로젝트에서도 접근 가능하다. 이를 통해 공통 구성을 정의하고 빌드 로직을 메소드로 만들어서 서브 프로젝트에서 사용하도록 하면 된다.
* ''build.gradle''
srcDirName = 'src/java'
def getSrcDir(project) {
return project.file(srcDirName)
}
* ''child/build.gradle''
task show << {
// 상속받은 프라퍼티 사용
println 'srcDirName: ' + srcDirName
File srcDir = getSrcDir(project)
println 'srcDir: ' + rootProject.relativePath(srcDir)
}
* 실행하면
> gradle -q show
srcDirName: src/java
srcDir: child/src/java
===== 구성 주입(Injected configuration) =====
상속보다는 [[gradle:multiproject|Gradle Multi Project]]에 나온 교차 프로젝트 구성방식과 서브프로젝트 구성 방식이 더 좋다. 이유는 주입방식은 빌드 스크립트에서 명백하게 일어나고 서로 다른 프로젝트에 다른 로직을 주입할 수 있다. 그리고 리포지토리, 플러그인, 태스크와 기타 등등 모든 종류의 구성을 주입할 수 있다.
* ''build.gradle''
subprojects {
// 프라퍼티와 메소드를 주입한다.
srcDirName = 'src/java' // 프라퍼티
srcDir = { file(srcDirName) } // 메소드
// Inject a task
task show << {
println 'project: ' + project.path
println 'srcDirName: ' + srcDirName
File srcDir = srcDir()
println 'srcDir: ' + rootProject.relativePath(srcDir)
}
}
// 특정 프로젝트에는 특수한 경우의 구성을 주입한다.
project(':child2') {
srcDirName = "$srcDirName/legacy"
}
* ''child1/build.gradle''
// 주입된 프라퍼티와 메소드를 사용하지만, 값은 덮어 쓴다.
srcDirName = 'java'
def dir = srcDir()
* 실행하면
> gradle -q show
project: :child1
srcDirName: java
srcDir: child1/java
project: :child2
srcDirName: src/java/legacy
srcDir: child2/src/java/legacy
===== buildSrc 프로젝트의 소스 빌드하기 =====
Gradle을 실행하면 ''buildSrc''라는 디렉토리가 존재하는지 검사한다. 존재한다면 자동으로 이 코드를 컴파일하고 테스트한 뒤에 빌드 스크립트의 클래스패스에 집어 넣는다. 그 외에 다른 것을 할 필요가 없다. 여기에 [[gradle:customtask|Custom Task]]와 [[gradle:customplugins|Gradle Custom Plugins]]을 넣으면 된다.
멀티 프로젝트에서는 최상위 프로젝트에만 ''buildSrc''를 둘 수 있다.
* ''buildSrc''의 기본 빌드 스크립트
// --build.gralde이 없어도 아래 빌드 스크립트가 존재하는 것으로 간주한다.--
// 1.6 버전에서는 명시적으로 필요한 것으로 보인다.
apply plugin: 'groovy'
dependencies {
compile gradleApi()
groovy localGroovy()
}
이 말은 ''buildSrc''에 기본 Java/Groovy 프로젝트를 구성할 수 있다는 뜻이다. [[gradle:java|Gradle Java Plugin]], [[gradle:groovy|Gradle Groovy Plugin]]
좀 더 복잡한 것이 필요하다면 자신만의 ''build.gradle''을 만들면 된다. Gradle은 기본 빌드 스크립트를 ''build.gradle''이 있건 없건 무조건 적용한다. 즉, 빌드 스크립트에는 기본값 외에 다른 것만 정의하면 된다. 1.6 버전에서는 기본 설정도 함께 넣어야 하게 바뀐 것 같다.
* ''buildSrc/build.gradle''
apply plugin: 'groovy'
repositories {
mavenCentral()
}
// gradle과 groovy에 관한 의존성은 추가할 필요가 없다.
dependencies {
compile gradleApi()
groovy localGroovy()
testCompile group: 'junit', name: 'junit', version: '4.8.2'
}
''buildSrc'' 프로젝트도 멀티 프로젝트 빌드가 될 수 있다. 하지만 실제 빌드의 클래스패스에 넣고자 하는 ''buildSrc'' 서브 프로젝트는 최상위 ''buildSrc'' 프로젝트의 ''runtime'' 의존성에 설정해야 한다.
* 서브프로젝트를 최상위 ''buildSrc''프로젝트에 넣기 ''build.gradle''
rootProject.dependencies {
runtime project(path)
}
===== JDBC Driver 로딩 못하는 문제 =====
* Gradle 1.6에서 ''buildSrc''의 커스텀 태스크/플러그인 프로젝트에 지정된 JDBC Driver 클래스를 못 찾는 문제가 있다.(''No suitable driver found for jdbc:...'')
* http://stackoverflow.com/questions/6329872/how-to-add-external-jar-files-to-gradle-build-script ''buildSrc/build.gradle''
repositories {
mavenCentral()
}
configurations {
driver
}
dependencies {
driver group: 'mysql', name: 'mysql-connector-java', version: '5.1.16'
}
URLClassLoader loader = GroovyObject.class.classLoader
configurations.driver.each {File file ->
loader.addURL(file.toURL())
}
// JDBC Driver 사용하는 코드 혹은 custom task/plugin 제작
===== 공유 스크립트 =====
[[:gradle|gradle]]에서 "외부 빌드 스크립트로 프로젝트 구성하기" 참조.
===== 커스텀 태스크 =====
[[gradle:customtask|Gradle Custom Task]] 참조.
===== 커스텀 플러그인 =====
[[gradle:customplugins|Gradle Custom Plugins]] 참조.
===== 외부 빌드 실행 =====
[[http://www.gradle.org/docs/current/dsl/org.gradle.api.tasks.GradleBuild.html|GradleBuild]] 태스크를 를 사용하여 다른 빌드의 태스크를 실행할 수 있다. ''dir'' 혹은 ''buildFile'' 프라퍼티로 실행할 빌드를 지정하고 ''tasks'' 프라퍼티로 실행할 태스크를 지정할 수 있다.
* 다른 빌드를 실행하기 ''build.gradle''
task build(type: GradleBuild) {
buildFile = 'other.gradle'
tasks = ['hello']
}
* ''other.gradle''
task hello << {
println "hello from the other build."
}
* 실행하면
> gradle -q build
hello from the other build.
===== 빌드 스크립트 전용 외부 라이브러리 =====
빌드 스크립트가 외부 라이브러리를 필요로 한다면 ''buildScript()'' 메소드를 사용하여 빌드 스크립트 자체의 스크립트 클래스패스에 추가하면 된다. 빌드 스크립트 클래스패스를 지정하는 클로저를 인자로 전달한다.
* 빌드 스크립트의 외부 의존성 지정하기 ''build.gradle''
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath group: 'commons-codec', name: 'commons-codec', version: '1.2' // classpath 구성 사용
}
}
''buildScript()''메소드에 전달된 클로저는 [[http://www.gradle.org/docs/current/javadoc/org/gradle/api/initialization/dsl/ScriptHandler.html|ScriptHandler]]의 인스턴스를 구성한다.
빌드 스크립트의 클래스패스는 **''classpath''** 구성을 사용하여 지정한다. 여기서는 프로젝트 의존성을 제외한 모든 의존성을 지정할 수 있다.
빌드 스크립트 클래스패스를 지정한 위에는 빌드스크립트에서 해당 클래스를 마음대로 사용할 수 있다.
* 외부 의존 클래스 사용하는 빌드 스크립트 ''build.gradle''
import org.apache.commons.codec.binary.Base64
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath group: 'commons-codec', name: 'commons-codec', version: '1.2'
}
}
task encode << {
def byte[] encodedString = new Base64().encode('hello world\n'.getBytes())
println new String(encodedString)
}
* 실행하면
> gradle -q encode
aGVsbG8gd29ybGQK
* 멀티 프로젝트에서는 프로젝트 빌드 스크립트의 의존성이 모든 서브프로젝트에도 적용된다.
* 멀티 프로젝트에서 최상위 프로젝트의 ''buildscript''에 repository를 추가해도, 하위 프로젝트에서 다시 ''buildscript'' 구문을 넣고 의존성을 지정하면 하위 프로젝트의 **buildscriptt** 구문에 repository를 재지정해야 한다.
===== Ant 의존성 추가 =====
빌드 스크립트의 외부 의존성 추가 방식으로는 Ant에 의존성을 추가할 수 없다.
* Ant에 의존성 추가 ''build.gradle''
configurations {
ftpAntTask
}
dependencies {
// ant-commons-net의 maven pom.xml이 잘못돼 있어서 ant-commons-net의 추가 의존성을 직접 지정해줬다.
ftpAntTask("org.apache.ant:ant-commons-net:1.8.4") {
module("commons-net:commons-net:1.4.1") {
dependencies "oro:oro:2.0.8:jar"
}
}
}
task ftp << {
ant {
taskdef(name: 'ftp',
classname: 'org.apache.tools.ant.taskdefs.optional.net.FTP',
classpath: configurations.ftpAntTask.asPath) // 여기서 추가된 클래스패스 사용!
ftp(server: "ftp.apache.org", userid: "anonymous", password: "me@myorg.com") {
fileset(dir: "htdocs/manual")
}
}
}
===== apply =====
''apply from: 'some.gradle'''로 외부 Gradle 스크립트를 적용할 수 있다. 이때 파일 대신 URL을 적어도 된다. [[http://mrhaki.blogspot.kr/2012/10/gradle-goodness-init-script-for-adding.html|인트라넷 URL을 통해 전사 공통 자바 소스 스타일 검사기능 추가하는 방법]]
apply from: 'http://intranet/source/quality.gradle'
이 방식을 사용할 경우 [[ci:jenkins|Jenkins]]등의 CI에서 동시 빌드시에 외부 리소스에 대한 락이 걸리는 상황이 발생한다.[[http://issues.gradle.org/browse/GRADLE-2795|[GRADLE-2795] Gradle locks the global script cache during the entire build, causing subsequent builds to fail if scripts change]]
아래와 유사한 오류가 발생할 것이다.
A problem occurred evaluating script.
Could not open buildscript class cache for script 'http://.../?p=build-core;a=blob_plain;f=repository-utils.gradle;hb=HEAD' (/home/build/.gradle/caches/1.6/scripts/_p_build_core_a_blob_plain_f_r_4n9gdhqrjd4inp4c6jive7ql9c/DefaultScript/buildscript).
Timeout waiting to lock buildscript class cache for script 'http://.../?p=build-core;a=blob_plain;f=repository-utils.gradle;hb=HEAD' (/home/build/.gradle/caches/1.6/scripts/_p_build_core_a_blob_plain_f_r_4n9gdhqrjd4inp4c6jive7ql9c/DefaultScript/buildscript).
It is currently in use by another Gradle instance.
Owner PID: unknown
Our PID: 15314
Owner Operation: unknown
Our operation:
Lock file: /home/build/.gradle/caches/1.6/scripts/_p_build_core_a_blob_plain_f_r_4n9gdhqrjd4inp4c6jive7ql9c/DefaultScript/buildscript/cache.properties.lock
이 때 해결책은 빌드 스크립트 URL의 맨 뒤에 Jenkins Job의 이름을 넣어주는 것이다. 마지막의 ''**${java.net.URLEncoder.encode(System.getenv()['JOB_NAME'] ?: 'NOJOB', 'UTF-8')}**'' Job 마다 서로 다른 build script Cache를 생성하여 Lock 충돌이 방지된다.
apply from: "http://server/epository-utils.gradle?jn=${java.net.URLEncoder.encode(System.getenv()['JOB_NAME'] ?: 'NOJOB', 'UTF-8')}"
''JOB_NAME'' 대신 ''Math.random()''사용시 계속해서 빌드 스크립트 캐시가 서로 다른이름으로 생성되어 파일 갯수가 증가하게 된다.
''/home/[username]/.gradle/caches/[version]/scripts/*'' 디렉토리를 cron 등으로 주기적으로 정리해줘야한다. 아래는 하루에 한 번씩 어제날짜의 캐시 디렉토리를 삭제하는 Unix script.
find /home/[user]/.gradle/caches/*/scripts -maxdepth 1 -mindepth 1 -mtime +1 -type d -exec rm -rf {} \;