Gradle 플러그인은 재사용 가능한 빌드 로직을 패키지화하여 여러 서로다른 프로젝트와 빌드에서 사용할 수 있도록 한 것이다.
플러그인은 아무 언어로 작성해도 되며 Java 바이트코드로 컴파일해서 제공하기만 하면 된다.
빌드 스크립트에 직접 플러그인 소스를 작성해도 된다. 자동으로 컴파일되어 빌드 스크립트의 클래스패스에 추가된다. 플러그인을 선언한 빌드 스크립트 외부에서는 접근할 수 없다.
추천하는 방식.
Gradle Organizing Build Logic에 나온 buildSrc
프로젝트에 넣는다. rootProjectDir/buildSrc/src/main/groovy
디렉토리 아래에 넣는 것이다. Gradle이 자동으로 컴파일, 테스트, 클래스패스 추가를 해준다.
이렇게 할 경우 모든 프로젝트의 빌드 스크립트에서 이 플러그인에 접근 가능하다.
플러그인용 독립 프로젝트를 만들고 Jar로 묶어서 배포한다.
Plugin을 구현하여 사용자 정의 플러그인을 만든다. Gradle은 플러그인 객체를 생성하고 프로젝트 객체를 인자로 하여 Plugin.apply() 메소드를 호출한다.
build.gradle
apply plugin: GreetingPlugin class GreetingPlugin implements Plugin<Project> { void apply(Project project) { project.task('hello') << { println "Hello from the GreetingPlugin" } } }
> gradle -q hello Hello from the GreetingPlugin
플러그인이 적용되는 모든 프로젝트에 대해서 플러그인 인스턴스가 각각 하나씩 생성된다.
플러그인들은 대부분 빌드 스크립트에서 설정을 해야할 필요가 있다. 이때 확장 객체(extension object)를 사용한다. Gradle의 Project는 ExtensionContainer 객체를 통해 플러그인에 전달되는 설정과 프라퍼티들을 추적할 수 있도록 해준다. 확장 컨테이너를 통해 사용자 입력을 받을 수 있다. 입력을 받으려면 확장 컨테이너의 확장 목록에 자바 빈에 호환되는 클래스를 추가하면 된다.
greeting
확장 객체를 프로젝트에 추가하기 build.gradle
apply plugin: GreetingPlugin // 확장 객체 값 사용 greeting.message = 'Hi from Gradle' class GreetingPlugin implements Plugin<Project> { void apply(Project project) { // 'greeting' 확장 객체 추가 project.extensions.create("greeting", GreetingPluginExtension) // 관련 설정을 사용하는 태스크 추가 project.task('hello') << { println project.greeting.message } } } class GreetingPluginExtension { def String message = 'Hello form GreetingPlugin' }
> gradle -q hello Hi from Gradle
이 예제에서 GreetingPluginExtension
은 message
필드를 가지고 있는 POGO이다. greeting
이라늠 이름으로 확장 객체가 플러그인에 추가 되었다. 이 객체는 동일한 이름으로 프로젝트의 프라퍼티로서 접근할 수 있다.
종종 지정할 프라퍼티가 많은 경우 확장 객체에 구성 클로저 블록을 추가하여 한번에 설정할 수 있도록 해준다.
build.gradle
apply plugin: GreetingPlugin // 확장 객체의 프라퍼티들을 한 번에 설정한다. greeting { message = 'Hi' greeter = 'Gradle' } class GreetingPlugin implements Plugin<Project> { void apply(Project project) { project.extensions.create("greeting", GreetingPluginExtension) project.task('hello') << { println "${project.greeting.message} from ${project.greeting.greeter}" } } } class GreetingPluginExtension { String message String greeter }
> gradle -q hello Hi from Gradle
클로저 블럭의 이름(greeting
)은 확장 객체의 이름과 같다. Groovy 클로저 위임 기능에 따라 클로저가 실행될 때 확장 객체의 필드는 클로저에 있는 변수와 매핑된다.
Project.file() 메소드로 파일들의 값을 최대한 늦게 결정하도록 하는 것이 좋다.
build.gradle
class GreetingToFileTask extends DefaultTask { def destination File getDestination() { project.file(destination) } @TaskAction def greet() { def file = getDestination() file.parentFile.mkdirs() file.write "Hello!" } } task greet(type: GreetingToFileTask) { destination = { project.greetingFile } } task sayGreeting(dependsOn: greet) << { println file(greetingFile).text } greetingFile = "$buildDir/hello.txt"
> gradle -q sayGreeting Hello!
위 예제에서는 태스크의 destination
프라퍼티를 Project.file() 메소드를 사용하여 최대한 늦게 평가하도록 하였다. 빌드 스크립트에서 태스크 선언 보다 더 뒤에 greetingFile
프라퍼티를 추가한 것을 볼 수 있다. 이 늦은 초기화 기법을 적극적으로 사용하라.
독립 프로젝트로 만들면 jar로 배포하여 다른 사람들과 공유할 수 있다. 보통 다음과 같은 최소한의 빌드 스크립트로 시작한다.
build.gradle
apply plugin: 'groovy' dependencies { compile gradleApi() groovy localGroovy() }
Gradle은 Plugin 구현체를 META-INF/gradle-plugins
에서 플러그인 이름과 같은 프라퍼티 파일을 통해 찾는다.
src/main/resources/META-INF/gradle-plugins/greeting.properties
implementation-class=org.gradle.GreetingPlugin
프라퍼티 파일의 이름이 플러그인의 이름이 된다. implementation-class
프라퍼티는 Plugin 구현 클래스를 가리킨다.
buildscript { }
블럭을 통해 클래스패스에 사용자 정의 플래그인 클래스를 추가한다. Gradle Organizing Build Logic 참조. 다음 예제는 로컬 리포지토리에 플러그인을 저장해서 사용하는 것을 보여준다.
build.gradle
buildscript { repositories { maven { url uri('../repo') } } dependencies { classpath group: 'org.gradle', name: 'customPlugin', version: '1.0-SNAPSHOT' } } apply plugin: 'greeting'
ProjectBuilder 클래스를 사용하여 Project 인스턴스를 만들어낼 수 있다. 이를 통해 플러그인 구현체를 테스트한다.
src/test/groovy/org/gradle/GreetingPluginTest.groovy
class GreetingPluginTest { @Test public void greeterPluginAddsGreetingTaskToProject() { Project project = ProjectBuilder.builder().build() project.apply plugin: 'greeting' assertTrue(project.tasks.hello instanceof GreetingTask) } }
Gradle은 빌드 언어와 잘 작동하는 객체의 컬렉션을 다루는 도우미 클래스를 제공해주고 있다.
build.gradle
apply plugin: DocumentationPlugin // DocumentaionPlugin.apply()가 실행된다. // books NamedDomainObjectContainer 값을 구성한다. apply보다 나중에 실행되지만 'books.all { }'의 행위가 다 적용된다. books { quickStart { sourceFile = file('src/docs/quick-start') } userGuide { } developerGuide { } } task books << { books.each { book -> println "$book.name -> $book.sourceFile" } } class DocumentationPlugin implements Plugin<Project> { void apply(Project project) { def books = project.container(Book) books.all { // 현재 있는, 그리고 앞으로 컬렉션에 추가될 객체를 모두 돌면서 아래 수행 sourceFile = project.file("src/docs/$name") } project.extensions.books = books } } class Book { final String name // 'name' 필드는 필수이며 상수이고 유일한 값이어야 한다. File sourceFile Book(String name) { this.name = name } }
developerGuide -> /home/user/gradle/samples/userguide/organizeBuildLogic/customPluginWithDomainObjectContainer/src/docs/developerGuide quickStart -> /home/user/gradle/samples/userguide/organizeBuildLogic/customPluginWithDomainObjectContainer/src/docs/quick-start userGuide -> /home/user/gradle/samples/userguide/organizeBuildLogic/customPluginWithDomainObjectContainer/src/docs/userGuide
Project.container()는 NamedDomainObjectContainer의 인스턴스를 생성한다. 이 클래스에는 객체를 관리하고 구성하는 편리한 메소드들이 들어있다.
project.container
메소드를 통해 사용할 객체의 타입은 항상 name
이라는 프라퍼티를 가지고 있어야 하며, 이 필드는 객체의 이름으로써 유일한 값이면서 상수여야 한다.
project.container(Class)
메소드는 객체의 새로운 인스턴스를 생성하면서 하나의 문자열을 인자로 받아 객체의 이름(name
)으로 지정하려고 시도한다.
NamedDomainObjectContainer는 DomainObjectCollection을 상속하고 있다. DomainObjectCollection.all() 메소드는 컬렉션에 현재 있는 항목들과 그 이후 추가될 항목들까지 돌면서 클로저의 내용을 실행한다. 따라서 여기서 books
선언이 플러그인 적용보다 늦게 발생했으나 books.all {}
블럭의 내용이 모두 자동으로 실행되게 된다.
플러그인 태스크 간의 의존성은 다음과 같은 형태로 정의할 수 있다.
project.task('sometask') { dependsOn(project.tasks.a, project.tasks.b) }
@Inject
애노테이션을 사용한다.@Inject
사용예를 볼 수 있다.