원문 - Implementing Gradle plugins


  • 플러그인 작성을 위해 플러그인 개발 플러그인 사용(Using the Plugin Development plugin for writing plugins)
  • 커스텀 태스크 타입 작성 및 사용(Prefer writing and using custom task types)
  • 증분 태스크의 이점(Benefiting from incremental tasks)
  • DSL과 유사한 API 모델링(Modeling DSL-like APIs)
  • 플러그인 런타임 동작을 구성하기 위해 사용자 입력 캡처(Capturing user input to configure plugin runtime behavior)
  • DSL 구성 컨테이너 선언(Declaring a DSL configuration container)
  • 플러그인에 반응(Reacting to plugins)
  • 플러그인에 대한 기본 의존성 제공(Providing default dependencies for plugins)
  • 적절한 플러그인 식별자 할당(Assigning appropriate plugin identifiers)
  • 그레이들의 다양한 버전에 대한 변형 플러그인 제공(Providing multiple variants of a plugin for different Gradle versions)
  • 다중 변형 플러그인의 버전별 변형 사용(Using version-specific variants of multi-variant plugins)

그레이들 플러그인 구현(Implementing Gradle plugins)

플러그인 코드 개발은 고급 빌드 작성자의 일상적인 활동이다. 일반적으로 플러그인 구현 개발, 원하는 기능을 실행하기 위한 커스텀 태스크 타입 생성, 선언적이고 표현적인 DSL을 노출하여 최종 사용자가 런타임 동작을 구성할 수 있도록 하는 태스크가 포함된다. 이 장에서는 더 나은 플러그인 개발자가 되기 위한 확립된 관행과 소비자가 플러그인에 최대한 접근하고 유용하게 만드는 방법을 배우게 된다.

이 장에서는 다음 내용들이 있다.

  • 소프트웨어 엔지니어링 실무에 대한 기본 이해
  • 프로젝트 구성, 태스크 생성 및 구성은 물론 그레이들 빌드 생명주기와 같은 그레이들 기본 지식
  • 자바 코드 개발에 대한 실무 지식

플러그인 작성을 위해 플러그인 개발 플러그인 사용(Using the Plugin Development plugin for writing plugins)

그레이들 플러그인 프로젝트를 설정하려면 상용구 코드가 최대한 적게 필요하다. 자바 그레이들 플러그인 개발 플러그인은 이러한 문제를 지원한다. 시작하려면 build.gradle 파일에 다음 코드를 추가하자.

build.gradle.kts

plugins {
    `java-gradle-plugin`
}

gradlePlugin {
    plugins {
        create("simplePlugin") {
            id = "org.example.greeting"
            implementationClass = "org.example.GreetingPlugin"
        }
    }
}

build.gradle

plugins {
    id 'java-gradle-plugin'
}

gradlePlugin {
    plugins {
        simplePlugin {
            id = 'org.example.greeting'
            implementationClass = 'org.example.GreetingPlugin'
        }
    }
}

플러그인을 적용(applying)하면, 필요한 플러그인이 적용되고 관련 의존성이 추가된다. 또한 바이너리 아티팩트를 그레이들 플러그인 포털에 게시하기 전에 플러그인 메타데이터를 검증하는 데도 도움이 된다. 모든 플러그인 프로젝트는 이 플러그인을 적용해야 한다.

커스텀 태스크 타입 작성 및 사용(Prefer writing and using custom task types)

그레이들 태스크는 임시(ad-hoc) 태스크, 하나 이상의 액션이 포함된 DefaultTask 타입의 간단한 태스크 또는 커스텀 태스크 타입을 사용하고 프로퍼티스의 도움으로 구성 가능성을 노출하는 고급 태스크로 정의할 수 있다. 일반적으로 커스텀 태스크는 재사용성, 유지 보수성, 구성 가능성 및 테스트 가능성을 위한 수단을 제공한다. 플러그인의 일부로 태스크를 제공할 때도 동일한 원칙이 적용된다. 항상 임시 태스크보다 커스텀 태스크 타입이 선호된다. 플러그인 소비자는 빌드 스크립트에 더 많은 태스크을 추가하려는 경우 기존 태스크 타입을 재사용할 수도 있다.

커스텀 태스크 타입을 제공하여 HTTP 호출을 수행으로 바이너리 리포지터리에서 최신 버전의 의존성을 분석하는 플러그인을 구현했다고 가정해 보자. 커스텀 태스크는 HTTP를 통한 통신과 XML 또는 JSON과 같은 기계가 읽을 수 있는 타입으로 응답을 처리하는 플러그인에 의해 제공된다.

LatestArtifactVersion.java

package org.myorg;

import org.gradle.api.DefaultTask;
import org.gradle.api.provider.Property;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.TaskAction;

abstract public class LatestArtifactVersion extends DefaultTask {

    @Input
    abstract public Property<String> getCoordinates();

    @Input
    abstract public Property<String> getServerUrl();

    @TaskAction
    public void resolveLatestVersion() {
        System.out.println("Retrieving artifact " + getCoordinates().get() + " from " + getServerUrl().get());
        // HTTP 호출을 발행하고 응답을 파싱한다.
    }
}

이제 태스크의 최종 사용자는 다양한 구성을 사용하여 해당 타입의 여러 태스크를 쉽게 생성할 수 있다. 복잡한 로직은 커스텀 태스크 구현체에 완전히 숨겨져 있다.

build.gradle.kts

tasks.register<LatestArtifactVersion>("latestVersionMavenCentral") {
    coordinates = "commons-lang:commons-lang"
    serverUrl = "http://repo1.maven.org/maven2"
}

tasks.register<LatestArtifactVersion>("latestVersionInhouseRepo") {
    coordinates = "commons-lang:commons-lang"
    serverUrl = "http://repo1.myorg.org/maven2"
}

build.gradle

tasks.register('latestVersionMavenCentral', LatestArtifactVersion) {
    coordinates = 'commons-lang:commons-lang'
    serverUrl = 'http://repo1.maven.org/maven2'
}

tasks.register('latestVersionInhouseRepo', LatestArtifactVersion) {
    coordinates = 'commons-lang:commons-lang'
    serverUrl = 'http://repo1.myorg.org/maven2'
}

증분 태스크의 이점(Benefiting from incremental tasks)

그레이들은 선언된 입력 및 출력을 사용하여 태스크가 최신 상태이면서 태스크를 수행해야 하는지 확인한다. 입력이나 출력이 변경되지 않은 경우 그레이들은 해당 태스크를 건너뛸 수 있다. 그레이들은 이 메커니즘을 증분 빌드 지원이라고 부른다. 증분 빌드 지원의 장점은 빌드 성능을 크게 향상시킬 수 있다는 것이다.

그레이들 플러그인이 커스텀 태스크 타입을 도입하는 것은 매우 일반적이다. 플러그인 개발자로서 이는 입력(input) 또는 출력(output) 어노테이션으로 태스크의 모든 프로퍼티스에 주석을 달아야 함을 의미한다. 최신 확인을 실행하기 위해 모든 태스크에 정보를 갖추는 것이 좋다. 기억하자: 최신 확인이 제대로 작동하려면 태스크에 입력과 출력을 모두 정의해야 한다.

설명을 위해 다음 샘플 태스크를 보자. 태스크는 출력 디렉토리에 지정된 수의 파일을 생성한다. 해당 파일에 기록된 텍스트는 String 프로퍼티에 의해 제공된다.

Let’s consider the following sample task for illustration. The task generates a given number of files in an output directory. The text written to those files is provided by a String property.

Generate.java

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;

import org.gradle.api.DefaultTask;
import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.provider.Property;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.OutputDirectory;
import org.gradle.api.tasks.TaskAction;

public abstract class Generate extends DefaultTask {

    @Input
    abstract public Property<Integer> getFileCount();

    @Input
    abstract public Property<String> getContent();

    @OutputDirectory
    abstract public RegularFileProperty getGeneratedFileDir();

    @TaskAction
    public void perform() throws IOException {
        for (int i = 1; i <= getFileCount().get(); i++) {
            writeFile(new File(getGeneratedFileDir().get().getAsFile(), i + ".txt"), getContent().get());
        }
    }

    private void writeFile(File destination, String content) throws IOException {
        BufferedWriter output = null;
        try {
            output = new BufferedWriter(new FileWriter(destination));
            output.write(content);
        } finally {
            if (output != null) {
                output.close();
            }
        }
    }
}

이 가이드의 첫 번째 절에서는 플러그인 개발 플러그인에 대해 설명한다. 프로젝트에 플러그인을 적용하면 얻을 수 있는 추가적인 이점으로, verifyPlugins 태스크는 커스텀 태스크 타입 구현에 정의된 모든 공용 프로퍼티에 대한 기존 입력/출력 어노테이션을 자동으로 확인한다.

DSL과 유사한 API 모델링(Modeling DSL-like APIs)

플러그인에 의해 노출된 DSL은 읽기 쉽고 이해하기 쉬워야 한다. 설명을 위해 플러그인에서 제공하는 익스텐션(extension)을 고려해 보자. 현재 형태는 웹 사이트 생성을 구성하기 위한 프로퍼티스의 “일반적인” 목록을 제공한다.

build.gradle.kts

plugins {
    id("org.myorg.site")
}

site {
    outputDir = layout.buildDirectory.file("mysite")
    websiteUrl = "https://gradle.org"
    vcsUrl = "https://github.com/gradle/gradle-site-plugin"
}

build.gradle

plugins {
    id 'org.myorg.site'
}

site {
    outputDir = layout.buildDirectory.file("mysite")
    websiteUrl = 'https://gradle.org'
    vcsUrl = 'https://github.com/gradle/gradle-site-plugin'
}

노출된 프로퍼티의 수가 증가함에 따라 중첩되고 표현력이 더 뛰어난 구조를 도입할 수 있다. 다음 코드 조각은 익스텐션의 일부로 customData라는 새 구성 블록을 추가한다. 해당 프로퍼티스가 무엇 의미하는지 더 잘 나타내는 것을 눈치챘을 것이다.

build.gradle.kts

plugins {
    id("org.myorg.site")
}

site {
    outputDir = layout.buildDirectory.file("mysite")

    customData {
        websiteUrl = "https://gradle.org"
        vcsUrl = "https://github.com/gradle/gradle-site-plugin"
    }
}

build.gradle

plugins {
    id 'org.myorg.site'
}

site {
    outputDir = layout.buildDirectory.file("mysite")

    customData {
        websiteUrl = 'https://gradle.org'
        vcsUrl = 'https://github.com/gradle/gradle-site-plugin'
    }
}

그러한 익스텐션의 지원 객체를 구현하는 것은 매우 쉽다. 우선, websiteUrlvcsUrl 프로퍼티스를 관리하기 위한 새로운 데이터 객체를 도입해야 한다.

CustomData.java

package org.myorg;

import org.gradle.api.provider.Property;

abstract public class CustomData {

    abstract public Property<String> getWebsiteUrl();

    abstract public Property<String> getVcsUrl();
}

익스텐션에서는 CustomData 클래스의 인스턴스와 캡처된 값을 데이터 인스턴스에 위임할 수 있는 메서드를 만들어야 한다. 기본 데이터 객체를 구성하려면 Action 타입의 파라미터를 정의한다. 다음 예제에서는 익스텐션 정의에서 Action을 사용하는 방법을 보여준다.

SiteExtension.java

package org.myorg;

import org.gradle.api.Action;
import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.model.ObjectFactory;
import org.gradle.api.tasks.Nested;

abstract public class SiteExtension {

    abstract public RegularFileProperty getOutputDir();

    @Nested
    abstract public CustomData getCustomData();

    public void customData(Action<? super CustomData> action) {
        action.execute(getCustomData());
    }
}

플러그인 런타임 동작을 구성하기 위해 사용자 입력 캡처(Capturing user input to configure plugin runtime behavior)

플러그인에는 플러그인을 적용하는 소비(consuming) 프로젝트에 대한 가정을 하는 기본 컨벤션이 함께 제공되는 경우가 많다. 예를 들어 자바 플러그인은 src/main/java 디렉토리에서 자바 소스 파일을 검색한다. 기본 컨벤션은 프로젝트 레이아웃을 간소화하는 데 도움이 되지만 커스텀 프로젝트 구조, 레거시 프로젝트 요구 사항 또는 다른 사용자의 기본 설정을 처리하기는 힘들다.

플러그인은 기본 런타임 동작을 재구성하는 방법을 공개해야 한다. 커스텀 태스크 타입 개발 및 사용 선호 장에서는 태스크 프로퍼티스에 대한 setter 메서드를 선언하여 구성 가능성을 달성하는 한 가지 방법을 설명한다. 문제에 대한 보다 정교한 해결책은 익스텐션 기능을 노출하는 것이다. 익스텐션은 그레이들 코어에 의해 노출된 DSL과 완전히 혼합되는 커스텀 DSL을 통해 사용자의 입력을 캡처한다.

다음 예제에서는 서버 URL을 캡처하기 위해 binaryRepo라는 이름의 익스텐션을 노출하는 플러그인을 적용한다.

build.gradle.kts

plugins {
    id("org.myorg.binary-repository-version")
}

binaryRepo {
    coordinates = "commons-lang:commons-lang"
    serverUrl = "http://repo2.myorg.org/maven2"
}

build.gradle

plugins {
    id 'org.myorg.binary-repository-version'
}

binaryRepo {
    coordinates = 'commons-lang:commons-lang'
    serverUrl = 'http://repo2.myorg.org/maven2'
}

캡처된 serverUrl 값을 사용하여 태스크를 수행하고 싶다고 가정해 보자. 대부분의 경우 노출된 익스텐션 프로퍼티는 태스크를 수행할 때 실제로 값을 사용하는 태스크 프로퍼티에 직접 매핑된다. 평가 순서 문제를 방지하려면 그레이들 4.0에 도입된 공개 API 프로퍼티을 사용해야 한다.

더 나은 아이디어를 제공하기 위해 BinaryRepositoryVersionPlugin 플러그인의 내부를 살펴보자. 플러그인은 BinaryRepositoryExtension 타입의 익스텐션을 생성하고 익스텐션 프로퍼티 serverUrl을 태스크 프로퍼티 serverUrl에 매핑한다.

BinaryRepositoryVersionPlugin.java

package org.myorg;

import org.gradle.api.Plugin;
import org.gradle.api.Project;

public class BinaryRepositoryVersionPlugin implements Plugin<Project> {
    public void apply(Project project) {
        BinaryRepositoryExtension extension = project.getExtensions().create("binaryRepo", BinaryRepositoryExtension.class);

        project.getTasks().register("latestArtifactVersion", LatestArtifactVersion.class, task -> {
            task.getCoordinates().set(extension.getCoordinates());
            task.getServerUrl().set(extension.getServerUrl());
        });
    }
}

일반 스트링 타입을 사용하는 대신 익스텐션은 Property<String> 타입으로 좌표 및 serverUrl 프로퍼티스를 정의한다. 프로퍼티스에 대한 추상 getter는 그레이들에 의해 자동으로 초기화된다. 그런 다음 해당 getter 메서드를 통해 얻은 프로퍼티 객체에서 프로퍼티 값을 변경할 수 있다.

그레이들 클래스로더는 반환 타입이 Property인 모든 getter 메서드와 함께 setter 메서드를 자동으로 주입한다. 이를 통해 개발자는 그루비 DSL에서 obj.prop.set 'foo'와 같은 코드를 obj.prop = 'foo'로 단순화할 수 있다.

BinaryRepositoryExtension.java

package org.myorg;

import org.gradle.api.provider.Property;

abstract public class BinaryRepositoryExtension {

    abstract public Property<String> getCoordinates();

    abstract public Property<String> getServerUrl();
}

태스크 프로퍼티는 프로퍼티(Property) 타입을 사용하여 serverUrl도 정의한다. 처리에 필요할 때까지(즉, 태스크 액션에 있을 때까지) 해당 값에 실제로 접근하지 않고도 프로퍼티 상태를 매핑할 수 있다.

BinaryRepositoryExtension.java

package org.myorg;

import org.gradle.api.DefaultTask;
import org.gradle.api.provider.Property;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.TaskAction;

abstract public class LatestArtifactVersion extends DefaultTask {

    @Input
    abstract public Property<String> getCoordinates();

    @Input
    abstract public Property<String> getServerUrl();

    @TaskAction
    public void resolveLatestVersion() {
        System.out.println("Retrieving artifact " + getCoordinates().get() + " from " + getServerUrl().get());
        // HTTP 호출을 발행하고 응답을 파싱한다.
    }
}

플러그인 개발자는 가능한 한 빨리 플러그인을 퍼블릭 프로퍼티 API로 마이그레이션하는 것이 좋다. 아직 그레이들 4.0을 기반으로 하지 않는 플러그인은 내부 “컨벤션 매핑” API를 계속 사용할 수 있다. “컨벤션 매핑” API는 문서화되지 않았으며 이후 버전의 그레이들에서는 제거될 수 있다.

DSL 구성 컨테이너 선언(Declaring a DSL configuration container)

때로는 사용자가 동일한 타입으로 명명된 여러 데이터 객체를 정의할 수 있는 방법을 노출하고 싶을 수도 있다. 설명을 위해 다음 빌드 스크립트를 고려해 보자.

build.gradle.kts

plugins {
    id("org.myorg.server-env")
}

environments {
    create("dev") {
        url = "http://localhost:8080"
    }

    create("staging") {
        url = "http://staging.enterprise.com"
    }

    create("production") {
        url = "http://prod.enterprise.com"
    }
}

build.gradle

plugins {
    id 'org.myorg.server-env'
}

environments {
    dev {
        url = 'http://localhost:8080'
    }

    staging {
        url = 'http://staging.enterprise.com'
    }

    production {
        url = 'http://prod.enterprise.com'
    }
}

플러그인에 의해 노출된 DSL은 환경을 정의하기 위한 컨테이너를 노출한다. 사용자가 구성한 각 환경은 임의적이지만 선언적으로 이름을 가지며 자체 DSL 구성 블록으로 표시된다. 위의 예제는 해당 URL을 포함하여 개발, 스테이징 및 프로덕션 환경을 인스턴스화한다.

분명히 이러한 각 환경에는 값을 캡처하기 위해 코드에 데이터 표현이 있어야 한다. 환경 이름은 변경할 수 없으며 생성자 파라미터로 전달될 수 있다. 현재 데이터 객체에 저장된 유일한 다른 파라미터는 URL이다. 아래 표시된 POJO ServerEnvironment는 이러한 요구 사항을 충족한다.

ServerEnvironment.java

package org.myorg;

import org.gradle.api.provider.Property;

abstract public class ServerEnvironment {
    private final String name;

    @javax.inject.Inject
    public ServerEnvironment(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    abstract public Property<String> getUrl();
}

그레이들은 팩토리 메소드 ObjectFactory.domainObjectContainer(Class, NamedDomainObjectFactory)를 노출하여 데이터 객체의 컨테이너를 생성한다. 메소드가 취하는 파라미터는 데이터를 나타내는 클래스다. NamedDomainObjectContainer 타입의 생성된 인스턴스는 특정 이름을 가진 익스텐션 컨테이너에 추가하여 최종 사용자에게 노출될 수 있다.

ServerEnvironmentPlugin.java

package org.myorg;

import org.gradle.api.NamedDomainObjectContainer;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.model.ObjectFactory;

public class ServerEnvironmentPlugin implements Plugin<Project> {
    @Override
    public void apply(final Project project) {
        ObjectFactory objects = project.getObjects();

        NamedDomainObjectContainer<ServerEnvironment> serverEnvironmentContainer = objects.domainObjectContainer(ServerEnvironment.class, name -> objects.newInstance(ServerEnvironment.class, name));
        project.getExtensions().add("environments", serverEnvironmentContainer);

        serverEnvironmentContainer.all(serverEnvironment -> {
            String env = serverEnvironment.getName();
            String capitalizedServerEnv = env.substring(0, 1).toUpperCase() + env.substring(1);
            String taskName = "deployTo" + capitalizedServerEnv;
            project.getTasks().register(taskName, Deploy.class, task -> task.getUrl().set(serverEnvironment.getUrl()));
        });
    }
}

플러그인이 플러그인 구현체 내에서 캡처된 값을 사후 처리하는 것은 일반적이다. 위의 예제에서는 사용자가 구성한 모든 환경에 대해 deployment 태스크가 동적으로 생성된다.

플러그인에 반응(Reacting to plugins)

빌드에서 기존 플러그인 및 태스크의 런타임 동작을 구성하는 것은 그레이들 플러그인 구현의 일반적인 패턴이다. 예를 들어 플러그인은 자바 기반 프로젝트에 적용된다고 가정하고 표준 소스 디렉토리를 자동으로 재구성할 수 있다.

InhouseStrongOpinionConventionJavaPlugin.java

import java.util.Arrays;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.plugins.JavaPlugin;
import org.gradle.api.tasks.SourceSet;
import org.gradle.api.tasks.SourceSetContainer;

public class InhouseStrongOpinionConventionJavaPlugin implements Plugin<Project> {
    public void apply(Project project) {
        project.getPlugins().apply(JavaPlugin.class);
        SourceSetContainer sourceSets = project.getExtensions().getByType(SourceSetContainer.class);
        SourceSet main = sourceSets.getByName(SourceSet.MAIN_SOURCE_SET_NAME);
        main.getJava().setSrcDirs(Arrays.asList("src"));
    }
}

이 접근 방식의 단점은 프로젝트가 자동으로 자바 플러그인을 적용하도록 강제하므로 의견을 강요한다는 것이다. 실제로 플러그인을 적용하는 프로젝트는 자바 코드를 처리하지 못할 수도 있다. 자바 플러그인을 자동으로 적용하는 대신 플러그인은 소비 프로젝트가 자바 플러그인을 적용한다는 사실에만 반응할 수 있다. 그러한 경우에만 특정 구성이 적용된다.

InhouseConventionJavaPlugin.java

import java.util.Arrays;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.plugins.JavaPlugin;
import org.gradle.api.tasks.SourceSet;
import org.gradle.api.tasks.SourceSetContainer;

public class InhouseConventionJavaPlugin implements Plugin<Project> {
    public void apply(Project project) {
        project.getPlugins().withType(JavaPlugin.class, javaPlugin -> {
            SourceSetContainer sourceSets = project.getExtensions().getByType(SourceSetContainer.class);
            SourceSet main = sourceSets.getByName(SourceSet.MAIN_SOURCE_SET_NAME);
            main.getJava().setSrcDirs(Arrays.asList("src"));
        });
    }
}

InhouseConventionWarPlugin.java

import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.tasks.bundling.War;

public class InhouseConventionWarPlugin implements Plugin<Project> {
    public void apply(Project project) {
        project.getTasks().withType(War.class).configureEach(war ->
            war.setWebXml(project.file("src/someWeb.xml")));
    }
}

플러그인에 대한 기본 의존성 제공(Providing default dependencies for plugins)

플러그인을 구현하려면 외부 의존성을 사용해야 하는 경우가 있습니다. 그레이들의 의존성 관리 메커니즘을 사용하여 아티팩트를 자동으로 다운로드하고 나중에 플러그인에 선언된 태스크 타입의 액션에서 사용할 수 있다. 최적으로, 플러그인 구현체는 사용자에게 해당 의존성의 좌표를 요청할 필요가 없다. 간단하게 기본 버전을 미리 정의할 수 있다.

예제를 살펴보자. 추가 처리를 위해 데이터가 포함된 파일을 다운로드하는 플러그인을 작성했다. 플러그인 구현체는 의존성 좌표를 사용하여 외부 의존성을 할당할 수 있는 커스텀 구성을 선언한다.

DataProcessingPlugin.java

package org.myorg;

import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.artifacts.Configuration;

public class DataProcessingPlugin implements Plugin<Project> {
    public void apply(Project project) {
        Configuration dataFiles = project.getConfigurations().create("dataFiles", c -> {
            c.setVisible(false);
            c.setCanBeConsumed(false);
            c.setCanBeResolved(true);
            c.setDescription("The data artifacts to be processed for this plugin.");
            c.defaultDependencies(d -> d.add(project.getDependencies().create("org.myorg:data:1.4.6")));
        });

        project.getTasks().withType(DataProcessing.class).configureEach(
            dataProcessing -> dataProcessing.getDataFiles().from(dataFiles));
    }
}

DataProcessing.java

package org.myorg;

import org.gradle.api.DefaultTask;
import org.gradle.api.file.ConfigurableFileCollection;
import org.gradle.api.tasks.InputFiles;
import org.gradle.api.tasks.TaskAction;

abstract public class DataProcessing extends DefaultTask {

    @InputFiles
    abstract public ConfigurableFileCollection getDataFiles();

    @TaskAction
    public void process() {
        System.out.println(getDataFiles().getFiles());
    }
}

이제 이 접근 방식은 의존성을 선언할 필요가 없으므로 최종 사용자에게 매우 편리하다. 플러그인은 이미 이 구현 세부 사항에 대한 모든 지식을 제공한다. 하지만 사용자가 기본 의존성을 오바리이드하고 싶다면 어떻게 해야 할까? 플러그인은 또한 다른 의존성을 할당하는 데 사용할 수 있는 커스텀 구성을 노출한다. 사실상 기본 의존성을 덮어쓰게 된다.

build.gradle.kts

plugins {
    id("org.myorg.data-processing")
}

dependencies {
    dataFiles("org.myorg:more-data:2.6")
}

build.gradle

plugins {
    id 'org.myorg.data-processing'
}

dependencies {
    dataFiles 'org.myorg:more-data:2.6'
}

이 패턴은 태스크의 액션이 실제로 실행될 때 외부 의존성이 필요한 태스크에 적합하다는 것을 알 수 있다. 더 나아가 익스텐션 프로퍼티(예: JaCoCo 플러그인의 toolVersion)을 노출하여 외부 의존성에 사용할 버전을 추상화할 수 있다.

적절한 플러그인 식별자 할당(Assigning appropriate plugin identifiers)

설명이 포함된 플러그인 식별자를 사용하면 소비자가 프로젝트에 플러그인을 쉽게 적용할 수 있다. ID는 단일 용어로 플러그인의 목적을 반영해야 한다. 또한 유사한 기능을 가진 다른 플러그인 간의 충돌을 피하기 위해 도메인명을 추가해야 한다. 이전 절에서 코드 예제에 표시된 의존성은 그룹 ID로 org.myorg를 사용한다. 도메인명과 동일한 식별자를 사용할 수 있다.

단일 JAR 아티팩트의 일부로 여러 플러그인을 게시하는 경우 동일한 명명 컨벤션을 적용해야 한다. 이는 관련 플러그인을 함께 그룹화하는 좋은 방법이다. 식별자별로 등록할 수 있는 플러그인 수에는 제한이 없다. 설명을 위해 그레이들 안드로이드 플러그인은 두 가지 다른 플러그인을 정의한다.

클래스로 작성된 플러그인의 식별자는 플러그인 클래스가 포함된 프로젝트의 빌드 스크립트에서 정의되어야 한다. 이를 위해서는 java-gradle-plugin을 적용해야 한다.

build.gradle.kts

plugins {
    id("java-gradle-plugin")
}

gradlePlugin {
    plugins {
        create("androidApplicationPlugin") {
            id = "com.android.application"
            implementationClass = "com.android.AndroidApplicationPlugin"
        }
        create("androidLibraryPlugin") {
            id = "com.android.library"
            implementationClass = "com.android.AndroidLibraryPlugin"
        }
    }
}

build.gradle

plugins {
    id 'java-gradle-plugin'
}

gradlePlugin {
    plugins {
        androidApplicationPlugin {
            id = 'com.android.application'
            implementationClass = 'com.android.AndroidApplicationPlugin'
        }
        androidLibraryPlugin {
            id = 'com.android.library'
            implementationClass = 'com.android.AndroidLibraryPlugin'
        }
    }
}

미리 컴파일된 스크립트 플러그인의 식별자는 스크립트 플러그인의 파일명을 기반으로 자동으로 등록된다.

그레이들의 다양한 버전에 대한 변형 플러그인 제공(Providing multiple variants of a plugin for different Gradle versions)

현재 다중 변형 플러그인을 지원하려면 그레이들의 원시 변형 인식 의존성 관리 API를 사용해야 한다. 이에 대한 더 많은 편의가 향후 제공될 수 있다.

현재 추가 플러그인 변형을 구성하는 가장 편리한 방법은 자바 플러그인 중 하나를 적용하는 모든 그레이들 프로젝트에서 사용할 수 있는 개념인 기능 변형을 사용하는 것이다. 문서에 설명된 대로 기능 변형을 설계하는 데는 여러 가지 옵션이 있다. 동일한 Jar 안에 번들로 제공될 수도 있고, 각 변형이 자체 Jar와 함께 제공될 수도 있다. 여기서는 각 플러그인 변형이 어떻게 개별적으로 개발되는지 보여준다. 즉, 별도로 컴파일되어 별도의 Jar에 패키지된 별도의 소스 세트에 있다. 하지만 다른 설정도 가능하다.

다음 샘플은 그레이들 7+와 호환되는 변형을 추가하는 방법을 보여주며 ‘main’ 변형은 이전 버전과 호환된다. 이에 대한 지원은 그레이들 7에만 추가되었으므로 그레이들 버전 7 이상만 변형에 의해 명시적으로 타겟팅될 수 있다.

build.gradle.kts

val gradle7 = sourceSets.create("gradle7")
java {
    registerFeature(gradle7.name) {
        usingSourceSet(gradle7)
        capability(project.group.toString(), project.name, project.version.toString()) //................... 1.
    }
}
configurations.configureEach {
    if (isCanBeConsumed && name.startsWith(gradle7.name))  {
        attributes {
            attribute(GradlePluginApiVersion.GRADLE_PLUGIN_API_VERSION_ATTRIBUTE, //........................ 2.
                objects.named("7.0"))
        }
    }
}
tasks.named<Copy>(gradle7.processResourcesTaskName) { //.................................................... 3.
    val copyPluginDescriptors = rootSpec.addChild()
    copyPluginDescriptors.into("META-INF/gradle-plugins")
    copyPluginDescriptors.from(tasks.pluginDescriptors)
}

dependencies {
    "gradle7CompileOnly"(gradleApi()) //.................................................................... 4.
}

build.gradle

def gradle7 = sourceSets.create('gradle7')
java {
    registerFeature(gradle7.name) {
        usingSourceSet(gradle7)
        capability(project.group.toString(), project.name, project.version.toString()) //................... 1.
    }
}
configurations.configureEach {
    if (canBeConsumed && name.startsWith(gradle7.name))  {
        attributes {
            attribute(GradlePluginApiVersion.GRADLE_PLUGIN_API_VERSION_ATTRIBUTE, //........................ 2.
                      objects.named(GradlePluginApiVersion, '7.0'))
        }
    }
}
tasks.named(gradle7.processResourcesTaskName) { //.......................................................... 3.
    def copyPluginDescriptors = rootSpec.addChild()
    copyPluginDescriptors.into('META-INF/gradle-plugins')
    copyPluginDescriptors.from(tasks.pluginDescriptors)
}

dependencies {
    gradle7CompileOnly(gradleApi()) //...................................................................... 4.
}

먼저, Gradle7 플러그인 변형에 대해 별도의 소스 세트와 이를 기반으로 하는 기능 변형을 선언한다. 기능을 적절한 그레이들 플러그인 변형으로 전환하려면 몇 가지 특정 연결을 수행해야 한다.

  1. 컴포넌트 CAV에 해당하는 암시적 기능을 변형에 할당한다.
  2. Gradle7 변형의 모든 소비 가능 구성에 그레이들 API 버전 애트리뷰트를 할당한다. 이 정보는 그레이들이 플러그인 해결 중에 선택할 변형을 결정하는 데 사용된다.
  3. 플러그인 설명자 파일이 Gradle7 변형 Jar에 추가되었는지 확인하도록 processGradle7Resources 태스크를 구성한다.
  4. 컴파일 시간 동안 API가 표시되도록 새 변형에 대한 gradleApi()에 의존성을 추가한다.

현재 플러그인을 빌드하는 데 사용하는 다른 그레이들 버전의 API에 접근할 수 있는 편리한 방법은 없다. 이상적으로는 모든 변형이 지원하는 최소 그레이들 버전의 API에 대한 의존성을 선언할 수 있어야 한다. 이는 앞으로 개선될 예정이다.

위의 코드 조각에서는 플러그인의 모든 변형이 동일한 위치에 플러그인 클래스를 가지고 있다고 가정한다. 즉, 이 절을 잘따랐고 플러그인 클래스가 org.example.GreetingPlugin인 경우 src/gradle7/java/org/example에서 해당 클래스의 두 번째 변형을 생성해야 한다.

다중 변형 플러그인의 버전별 변형 사용(Using version-specific variants of multi-variant plugins)

다중 변형 플러그인에 대한 의존성을 고려하면 그레이들은 다음 중 하나를 해결할 때 현재 그레이들 버전과 가장 잘 일치하는 변형을 자동으로 선택한다.

  • plugins {} 블록에 지정된 플러그인;
  • 빌드스크립트 클래스패스 의존성;
  • 컴파일 또는 런타임 클래스패스에 나타나는 빌드 소스(buildSrc)의 루트 프로젝트에 대한 의존성
  • 컴파일 또는 런타임 클래스패스에 나타나는 자바 그레이들 플러그인 개발 플러그인 또는 코틀린 DSL 플러그인을 적용하는 프로젝트의 의존성이다.

가장 잘 일치하는 변형은 현재 빌드의 그레이들 버전을 초과하지 않는 가장 높은 그레이들 API 버전을 대상으로 하는 변형이다.

다른 모든 경우에는 지원되는 그레이들 API 버전을 지정하지 않는 플러그인 변형이 선호된다(해당 변형이 있는 경우).

플러그인을 의존성으로 사용하는 프로젝트에서는 다른 그레이들 버전을 지원하는 플러그인 의존성의 변형을 요청할 수 있다. 이를 통해 다른 플러그인에 의존하는 다중 변형 플러그인이 해당 버전별 변형에서만 제공되는 API에 접근할 수 있다.

이 스니펫은 위에 정의된 플러그인 변형 gradle7이 다른 다중 변형 플러그인에 대한 의존성의 일치하는 변형을 사용하도록 만든다.

build.gradle.kts

configurations.configureEach {
    if (isCanBeResolved && name.startsWith(gradle7.name))  {
        attributes {
            attribute(GradlePluginApiVersion.GRADLE_PLUGIN_API_VERSION_ATTRIBUTE,
                objects.named("7.0"))
        }
    }
}

build.gradle

configurations.configureEach {
    if (canBeResolved && name.startsWith(gradle7.name))  {
        attributes {
            attribute(GradlePluginApiVersion.GRADLE_PLUGIN_API_VERSION_ATTRIBUTE,
                objects.named(GradlePluginApiVersion, '7.0'))
        }
    }
}