LoginSignup
2
2

Gradle学習

Last updated at Posted at 2023-05-18

自分の今のレベルでは情報をうまく体系化できなかったため、この記事は今後少しづつレベルアップして改善させて行けたら良いなと思っています。

Groovyの文法については以下にまとめました。

タスク

Gradleで行う処理はタスクと言う。Gradleにおいてタスクは非常に重要な概念。

Gradleはビルドツールとして知られるが、ビルド作業も数多くあるタスクの内の一つに過ぎず、例えば、$ gradle build$ gradleという「タスクを実行するコマンド」によって、buildという名前の「タスクを実行している」に過ぎない。

タスクはプラグインによって後から追加することができる。

タスクの定義

タスクを定義するには、TaskContainerクラスのregister()を使用する。

TaskContainerオブジェクトは、Projectオブジェクトのtasksプロパティを通じてアクセスする。

build.gradleに記述するスクリプトは、実際にはProjectオブジェクトに委譲される(イメージとしては、スクリプトが関数としてProjectオブジェクトに渡され、Projectオブジェクトが呼び出すような形。実際にはもう少し複雑で、スクリプトがまるごとProjectのプロパティに格納されたりするわけではない。ただ、委譲される = 「スクリプトがProjectオブジェクトの持つプロパティやメソッドにアクセスできるようになること」と理解する。)ため、Projectオブジェクトが持つプロパティであるtasksにアクセスするには、build.gradle内で、ただ単にtasksと記述すれば良い。

build.gradle
// 方法①(推奨) : タスクの定義が遅延される
tasks.register('hi') {
    doLast {
        println 'hi'
    }
}
build.gradle
// 方法② : Build script(build.gradle)が評価された時点で、タスクも即座に定義される
task hi {
    doFirst {
        println 'hi'
    }
}
build.gradle
// 廃止された記法
task hi << {            // 演算子 << は leftShift() と同じ
    println 'hi'
}

<<を使用した記法は、Gradle 5.0で廃止された。

タスクの実行

$ gradle タスク名で、指定したタスクが実行できる。

タスク名には省略記法もある。aaaBbbCccDddタスクは$ gradle aBCDで実行できる。

$ gradle hi

> Task :hi
hi

$ gradleコマンドは、カレントディレクトリに配置されたbuild.gradleをGradleのプロジェクトとして認識する。

そのため、マルチプロジェクトにおいては
【プロジェクトのルートディレクトリで実行】
ルートディレクトリのbuild.gradleがプロジェクトとして認識される
【サブプロジェクトのディレクトリで実行】
サブディレクトリのbuild.gradleがプロジェクトが認識される

タスクの編集(アクションの追加)

タスクは定義した後にも処理(アクション)を追加できる。TaskContainerクラスのnamed()を使う。

build.gradle
// タスクの定義
tasks.register('myTask') {
    doLast {
        println 'タスクの定義'
    }
}
build.gradle
// アクションの追加
tasks.named('myTask') {
    doFirst {
        println '1回目のdoFirst()'
    }
}
// アクションの追加
tasks.named('myTask') {
    doFirst {
        println '2回目のdoFirst()'
    }
}

doFirst()を2回実行すると、後に追加したほうが、早いタイミングで実行される。

$ gradle greet

> Task :myTask
2回目のdoFirst()
1回目のdoFirst()
タスクの定義

型を指定したタスク定義

Gradleにはあらかじめアクションが定義されたタスクが存在する。また、タスクはプラグインが提供するものもある。公式サイトのDSL Reference では Task types と言う表現になっている。

これらのタスクを利用したいときは、利用したいタスクの型を指定して、自分のタスクを定義する。利用したいタスク型が必要とするパラメータは定義の中で設定する。

たとえば、Copyタスクは、「ファイルやディレクトリをコピーする」というアクションが定義されたタスクであり、利用する場合に、コピー元のパス・コピー先のパスを設定する必要がある。下のコードでは、その設定をfrom()や、into()で行っている。タスクの型を指定してタスク定義する場合、アクション自体はすでに定義されているため、doFirst()doLast()によってアクションを定義する必要は無い。

build.gradle
// 型(Copy)を指定
tasks.register('copy', Copy) {
    from(file('srcDir'))
    into(buildDir)
}

組み込みタスク(build-in tasks)

Gradleにあらかじめ定義されたタスクを組み込みタスクと言う。

使用できるタスクは$ gradle tasksで確認できる。

$ gradle tasks

Build Setup tasks
-----------------
init - Initializes a new Gradle build.
wrapper - Generates Gradle wrapper files.
...以下省略

タスクルール

タスクを動的に定義する方法。

同じ処理をパラメータだけ変えて実行したい場合に利用すると、似た定義を何度もしなくて済む。

タスクルールはProjectオブジェクトのtasksプロパティが参照するTaskContainer型オブジェクトのaddRules()を使用する(project.tasks.addRule())。

addRules()の第一引数に渡す文字列は$ gradle tasksコマンドを実行した時の表示文になる。第二引数にはクロージャーを渡す。クロージャーには、タスク実行時のタスク名が渡される。

build.gradle
tasks.addRule('Pattern: say<WORD>: Say something.') { taskName ->
    if(taskName.startsWith('say')){
        tasks.register(taskName){
            String word = taskName.substring('say'.size())
            println "The word you specified is ... $word"
        }
    }
}

タスクルールが追加されたことを確認。

$ gradle tasks

Build Setup tasks
-----------------
init - Initializes a new Gradle build.
...省略

Rules
-----
Pattern: say<WORD>: Say something.

実際に実行してみる。

$ gradle sayHello

The word you specified is ... Hello

タスクの依存関係(Task Graphs)

タスクには依存関係がある。task Graphs と言う。

例えば、タスクの実行順を A → B に制御したい、みたいなもの。依存関係を定義しておけば、実行時にGradleが制御してくれる。(依存関係の別に実行順を制御する方法もあり、正確には依存関係は実行順を制御するためのものではない)

依存関係が重複しても、Gradleはそれぞれのタスクを1度しか実行しない。
tasks_graph.png
$ gradle buildを実行すると、コンソールにいろんなタスク名が表示されるのは、依存している複数のタスクが自動的に実行されているため(:タスク名で表示される)。

循環する依存関係は定義できない。

build.gradle
tasks.register('first') {
    doLast {
        println 'first task'
    }
}

tasks.register('second') {
    dependsOn tasks.first // 依存関係を追加
    doLast {
        println 'second task'
    }
}

tasks.register('third') {
    dependsOn tasks.second // 依存関係を追加
    doLast {
        println 'third task'
    }
}
$ gradle third

> Task :first
first task

> Task :second
second task

> Task :third
third task

$ gradle secondではthirdしか指定していないが、依存関係をGradleが解決してくれる。

タスクの実行結果

タスクの実行結果はラベルによって表現される。

これらのラベルはタスク実行後に自動的にコンソールに出力される。同じコマンドを2回実行すると、UP-TO-DATEをよく目にする。

  • ラベルなし or EXECUTED : 実行済
  • UP-TO-DATE : 変更なし
  • FROM-CACHE : 前回のキャッシュあり
  • NO-SOURCE : 対象が無い

タスクの上書き

既存のタスクは上書きすることができる。

試しにjavaプラグインが提供するbuildタスクを上書きしてみる。

build.gradle
apply plugin: 'java'

Map<String, ?> args = [
    overwrite: true,
    action: {
        println 'Build is overwritten.'
    }
]
task(args, 'build')
$ gradle build

> Task :build
Build is overwritten.

組み込みタスクは、この方法で上書きすることができない。

プロジェクトの作成

Gradleでは(変更することもできるが)、プロジェクトのフォルダ構成を基本的にはMavenの規約に合わせる必要がある。規約とはソースコードはsrc/main/java/に格納するなどのルールや初期設定のこと。Mavenの規約と同じなので、MavenからGradleへの移行はしやすいと言われたりする。

$ gradle initコマンドでは規約に沿ったフォルダ構成を自動で作成できる。

$ gradle init

DSL

DSLとは、Domain-Specific Language(特定のドメイン向け言語)の略称で、特定の問題領域に特化したプログラミング言語のことを指す。

DSLは、一般的なプログラミング言語とは異なり、特定のドメインでの問題解決に最適化されており、そのドメインにおける表現力が高いことが特徴。

DSLには大きく2種類ある。

  • Embedded DSL(内部DSL) : 既存の言語を使用して作成されたDSL
  • Srandalone DSL(外部DSL) : 新たな文法を持つ言語によって作成されたDSL

GradleのスクリプトはGroovyの内部DSLによって記述するため、文法はあくまでもGroovyの文法で理解できる。

Gradleのスクリプト

Gradleのスクリプトには以下の種類がある。

  • Build script
  • Init script
  • Settings script

これらのscriptは、3つそれぞれに応じたオブジェクトに委譲される。この時、scriptが委譲されるオブジェクトを delegate object と言う。

そのためscriptの中で、delegate object のプロパティやメソッドを使用することができる。突然、宣言されていないメソッドが出てきたように感じるのはこのため。

例えば、Build script (build.gradle)はProject型の delegate object に委譲される。そのため、build.gradle内ではProject型のプロパティとメソッドが使用できる。

さらに3種類のscriptはScriptインターフェースも実装しているため、Scriptインターフェースで定義されたプロパティ、メソッドも使用することができる。

これらの情報は DSL Reference という形でGradleの公式サイトに掲載されている。

スクリプトに使用されるファイル

スクリプト ファイル名 役割や機能
Init script init.gradle ユーザー情報設定、環境設定
Settings script settings.gradle マルチプロジェクトにおけるプロジェクト定義
Build script build.gradle ビルド作業の定義
プロパティファイル gradle.properties ビルド実行時に読み込まれる

Init Script

  • ファイル名 : init.gradle
  • 配置先 : 環境変数を使用する。
    • <GRADLE_HOME>/init.d/ディレクトリ
    • <GRADLE_USER_HOME>/init.d/ディレクトリ
  • 実行されるタイミング : ビルドの最初
  • delegate objectの型 : Gradle
  • 補足
    • 必須のファイルでは無い
    • Gradle自体の設定を行うもの。Build scriptで使用するライブラリの取得先やバージョンしておいて、複数のプロジェクトで使いまわしたりする。いろんなプロジェクトで特定のライブラリを毎回設定するのが面倒な場合などに使用するものだと思う。そのため、Init Scriptは環境変数で配置先を保持して、プロジェクトディレクトリには配置しない。

Settings script

  • ファイル名 : settings.gradle
  • 配置先 : ルートプロジェクトの直下 ※他の場所にも置ける
  • 実行されるタイミング : ビルド前
  • delegate objectの型 : Settings
  • 補足
    • 必須のファイルでは無い。マルチプロジェクトでは必須
    • マルチプロジェクトにおける、サブプロジェクトの定義

Build script

  • ファイル名 : build.gradle
  • 配置先 :
  • 実行されるタイミング : ビルド時
  • delegate objectの型 : Project

スクリプトの実行される仕組み

スクリプトを構成する要素

  • Statement
  • Build script block
  • 変数

Statement

Groovyのstatement(文)。

一般的にstatementとはセミコロンで終わる1行の命令。if文の場合は1行で終わらないこともある。
文(statement)に対して、式(expression)は値を評価するコードで、値を返す。言語によって微妙に違うようなので、深入りはしない。

Gradleのスクリプトは delegate object に委譲されて実行されるため、statementでは delegate object のプロパティやメソッドが使用できる。

Build script block

Gradle独自の概念。実体はクロージャーを引数として受け取るメソッド。

Groovyのメソッドは、最後の引数がクロージャーの時は、()の外にクロージャーを出すことができる(※Gradleを理解するためのGroovy学習)。これを利用して Build script block という独自の文法を作り出している(DSL)。

Build script block に処理を記述するということは、delegate object が提供するAPIが引数として受け取るクロージャーを定義しているということになる。

例えば、build.gradledpendenciesブロックに処理を記述することを考える。

build.gradle
dependencies {
    
}
  • build.gradledelegate object であるProjectオブジェクトに委譲される
  • Projectオブジェクトにはdependencies()というメソッドがある
  • つまり、dependencies()delegate object のAPI
  • dependencies()dependencies(Closure closure)という定義になっていて、引数の最後がクロージャーなので、クロージャーを()の外に出すことができる
  • つまり上記のコードは、dependencies(Closure closure)を実行していて、{}closure部分のメソッドの引数

さらに、dependencies()に渡したclosureは、別のオブジェクトへ委譲される。この delegate object からクロージャーを受け取るオブジェクトをドメインオブジェクトという。

具体的には、dependencies()に渡したクロージャーは、DependencyHandlerオブジェクトへ委譲される。

delegate object とドメインオブジェクトの違いは、役割の違いによるものであり、Projectオブジェクトは delegate object としての機能と、ドメインオブジェクトとして機能を併せ持つ。

Build script block
(delegate object のメソッド名)
委譲されるオブジェクト
(ドメインオブジェクト)
dependencies DependencyHandler
repositories RepositoryHandler
allprojects Project
subprojects Project
initscript ScriptHandler
buildscript ScriptHandler
configulations ConfigurationContainer
Projectオブジェクト

全てのドメインオブジェクト群を管理する。
Projectオブジェクトから、全てのドメインオブジェクトへアクセスすることができる。
build.gradleファイルと1対1の関係にある。

プロパティ 概要 readonly
name プロジェクト名 String ⚪︎
desciption プロジェクトに関する説明 String
group プロジェクトが属するグループ Object
tasks プロジェクトのタスク TaskContainer ⚪︎
dependencies プロジェクトのdependency handler DependencyHandler ⚪︎
path プロジェクトのパス
(ファイルシステムのパスではない)
ルートプロジェクトの場合、:
サブプロジェクトの場合、:child
(settings.gradleでサブプロジェクトをchildと定義したとき)
String ⚪︎
projectDir build.graleファイルが配置されている
ディレクトリまでのパス
File ⚪︎
status プロジェクトの状態
デフォルト値はrelease
ライブラリをメタデータと一緒にアップロードする時にのみ使用
Object
state プロジェクトのビルドの状態
executed,failure,upToDateなどの値があり、デバッグ時にビルドが今どのフェーズに進んでいるのかを知れる
ProjectState ⚪︎
version プロジェクトのバージョン Object
project 自分自身への参照
明示的にProjectオブジェクトを使用したい時(プロパティ名やメソッド名が被った時)に使う
Project ⚪︎
parent 親プロジェクトへの参照
シングルプロジェクトやマルチプロジェクトのルートプロジェクトではnull
Project ⚪︎
allprojects このプロジェクトとその配下のサブプロジェクト
(親プロジェクトは含まない)
Set<Project> ⚪︎
subprojects サブプロジェクトへの参照 Set<Project> ⚪︎
メソッド 引数 戻り値の型 概要 オーバーロード
apply() Closure closure void プラグインの適用 あり
copy() Closure closure WorkResult ファイルのコピー あり
file() Object path File Fileオグジェクトの取得 あり
fileTree() Object baseDir ConfigurableFileTree ファイルツリーの生成 あり
project() String path Project Projectオブジェクトの取得 あり
task() String name
Cosure closure
Task タスクの定義 あり

変数

Gradleで扱う変数には以下の種類がある。

  • ローカル変数
  • システムプロパティ
  • 拡張プロパティ
  • プロジェクトプロパティ(※Build scriptでのみ有効)
ローカル変数

スクリプトのトップレベルで宣言した変数や特定のブロックの中で宣言した変数。

Groovyの文法上は、動的な型付けをdefを用いて行い、またdefを省略することもできるが、Gradleではdefを省略することはできない。

システムプロパティ

Gradleで記述したスクリプトはGroovyのスクリプトとしてJVMで実行される。JVMにはシステムプロパティというランタイムに関するKey-Valueのペア情報が存在する。

システムプロパティはコマンドの-Dオプションで指定するか、gradle.propertiesファイルにsystemProp接頭語をつけて設定することができる。

scriptからはSystem.getProperty()で取得することができる。

gradle.properties
systemProp.hello=hi
build.gradle
tasks.register('sample'){
    doFirst {
        println System.getProperty('hello')
    }
}
$ gradle sample
> Task :sample
hi
拡張プロパティ

スクリプトからドメインオブジェクトのプロパティを追加する仕組み。extブロックを使用して設定できる。

projectNameversionなどはGradleによって自動的に定義されている。

build.gradle
ext {
    key1 = 'hello'
}

tasks.register('sample'){
    doFirst {
        println key1
    }
}
$ gradle sample

> Task :sample
hello
プロジェクトプロパティ

Build scriptで使用可能。設定方法が複数ある。

  • 設定方法
    • プロパティファイル(gradle.properties)
    • 環境変数
    • コマンドライン引数(-Pオプション)

プロパティファイルで設定する方法は以下の通り。

gradle.properties
myProp=hello
build.gradle
// 取得方法も複数ある
tasks.register('sample'){
    doFirst {
        println project.property('myProp')
        println findProperty('myProp')
        println myProp
    }
}
$ gradle sample

> Task :sample
hello
hello
hello

プラグイン

プラグインを追加することで、使用できるタスクや、Projectオブジェクトのメソッド、プロパティが増える。

javaプラグイン

build.gradle
// binary plugins
apply plugin 'java'

// script plugins
plugins {
    id 'java'
}

ここでのjavaはプラグインIDと呼ばれる。プラグインIDの無い場合、Pluginインターフェースを実装したクラスを指定してもよい。

build.gradle
apply plugin: org.gradle.api.plugins.JavaPlugin

javaプラグインを適用すると、次のものが導入される。

  • タスク
  • ソースセット
  • プロパティ
  • メソッド
  • 規約

タスク

javaプラグインには、さまざまなタスクが用意されているため、適用すると、使用できるタスクが増える。

ソースセット

複数のソースファイルを目的別にまとめ、名前をつけて管理する仕組み。ソースセットごとにタスクを定義したり、ソースセットごとにプロパティを定義することができる。

javaプラグインにはmaintestというソースセットが含まれている。

ソースセットは追加することができる。

sourceSets {
    newSourceSet
} 

newSourceSetという名前のソースセットを追加した場合、newSourceSetCompilenewSourceSetRuntimeというConfigurationが追加される。

プロパティ

javaプラグインには、多くのプロパティが含まれる。これらはProjectオブジェクトのプロパティとして追加される。

あらかじめデフォルト値が設定されているため、値を変更したいときだけ使用する。

  • sourceCompatibility : ソースコードのJavaのバージョン
  • targetCompatibility : ランタイムのJavaのバージョン
  • archiveBaseName : アーカイブファイル(JAR)の名前
buidle.gradle
sourceCompatibility = 11
targetCompatibility = JavaVersion.VERSION_11 // enumでも指定できる
archiveBaseName = 'aaaa.jar'

(当たり前だが、プロパティによって型が異なり、全てがString型というわけではない。個人的に、Gradlenの記述を理解するには、「Groovyのオブジェクトを扱っている」という意識を忘れないことが大事な気がしている。)

ソースセットごとのプロパティ
  • name : ソースセットの名前
  • compileClasspath : コンパイル時のクラスパス
  • runtimeClasspath : 実行時のクラスパス
build.gradle
println sourceSets.main.compileClasspath
println sourceSets.['main'].compileClasspath

ソースセットごとのプロパティには、Projectオブジェクトからアクセスする。

メソッド

  • manifest() : JARファイルに含めるマニュフェスト

規約

javaプラグインを適用することで導入される約束事。

  • アプリケーションのソースファイルはsrc/main/javaディレクトリに配置する
  • リソースファイルはsrc/main/resourcesディレクトリに配置する

Toolchains

指定したJDKのバージョンがインストールされていない場合、インストールまでを行なってくれるようになる。

依存関係とConfiguration

Configurationとはプロジェクトがビルドされる際に必要なライブラリやファイルなどの依存関係を定義するための仕組み。

手作業なら、以下のような作業が必要。

  1. ライブラリのファイルをダウンロード
  2. ライブラリを配置したパスをクラスパスに追加
  3. ライブラリが依存するライブラリがあれば、1の2繰り返し

Gradleでは設定を記述しておくだけで、ライブラリをダウンロード、派生する依存関係の解決などを自動で行ってくれるようになる。

Configurationを分けて依存関係を構成するで、ビルド時のみ、実行時のみ、といった目的ごとに依存関係を管理することができる。

下は、compileというConfigurationに対して依存関係を定義している。

build.gradle
dependencies {
    compile 'org.slf4j:slf4j-api:1.7.25'
}

Configurationの定義

build.gradle
configurations {
    myConfiguration
}
build.gradle
// Configurationの定義
configurations {
    myConfiguration
}
// 依存関係の定義
dependencies {
    myConfiguration 'org.slf4j:slf4j-api:1.7.25'
}

Configurationの継承

Configurationは継承することができる

継承すると、継承元のConfigurationで定義された依存関係を取り込むことができる。

build.gradle
configurations {
    parentConf
    childConf.extendsFrom parentConf
}

dependencies {
    parentConf 'org.slf4j:slf4j-api:1.7.25' // childConfに依存関係が継承される
}

Maven Central リポジトリにおけるとの依存関係

Maven Central リポジトリを利用するには、まずリポジトリの定義が必要。

build.gradle
repositories {
    mavenCentral()
}

Maven Central リポジトリのモジュール(ライブラリ)を特定するためには3つの情報が必要。

group: 'グループ名', name: 'モジュール名', version: 'バージョン番号'

省略記法もある。

'グループ名 : モジュール名 : バージョン番号'
build.gradle
dependencies {
	myConfiguration 'グループ名 : ライブラリ名 : バージョン番号'
}

ファイルシステムにおける依存関係

独自に作成した汎用メソッドや、複数のプロジェクトから利用したいモジュール(Jarファイル)を利用するには、以下のように記述する。

build.gradle
dependencies {
    myConfiguration file('絶対パス or 相対パス')   // 相対パスはプロジェクトのルートディレクトリが基準
}

Build scriptの中で外部ライブラリを使用したい時

build.gradle
import aaa.bbb.ccc.D // 使用したいライブラリ

buildscript {
    repositories {
        mavenCentral() // 取得先リポジトリ
    }
    dependencies {
        classpath group: 'グループ名', name: '', version: 'バージョン番号'
        classpath 'グループ名 : ライブラリ名 : バージョン番号' // 省略記法でも良い
    }
}

tasks.register('hi') {
    doFirst {
        D.method() // Build scriptの中で、ライブラリのメソッドが使える
    }
}

JARファイル

出力されるJARファイルのファイル名はデフォルトでは<baseName>-<appendix>-<version>-<classifier>.jarとなっている。

これを変更するには以下のようにする。

build.gradle
jar {
    baseName = 'aaa'
    appendix = 'bbb'
    version = 'ccc'
    classifier = 'ddd'
}

次のような方法もある。

build.gradle
jar {
    archiveName = 'aaa.jar'
}
build.gradle
jar {
    manifest {
        attributes(
            "Implementation-Title": "Gradle",
            "Implementation-Version": archiveVersion)
    }
}

ファイル操作

バリエーションが豊富で、機能も豊富。以下は多数あるAPIの内の一部。

ファイルの参照

build.gradle
File fileA = file('絶対パス or 相対パス')   // 相対パスはプロジェクトのルートディレクトリが基準

file()java.lang.Objectのメソッド。引数にはjava.net.URIjava.net.URLのインスタンスを渡しても良い。ここは、Gradleというよりは、Groovyのメソッドとしての使い方。

複数ファイルの参照

build.gradle
FileCollection fileLists = files('a.text', 'b.text')

ファイルツリー(FileTree)

build.gradle
// rootディレクトリから下の階層も含まれる
FileTree root = fileTree('root') 

// * : 0以上の文字列
FileTree htmlFiles1 = fileTree('root').include('*.html')
// ? : 任意の1文字
FileTree htmlFiles2 = fileTree('root').include('?.html')
// ** : ディレクトリ内の全ての階層含む
FileTree allHtmlFiles = fileTree('root').include('**/*.html')
// /で終わる : /以下の全ての階層のファイルを含む
FileTree allHtmlFiles2 = fileTree('root').include('sample/')

// includeの逆
FileTree withoutHtmlFile1 = fileTree('root').exclude('*.html')

// 第2引数にクロージャーを渡すこともできる
FileTree targetFiles = fileTree('root') {
    include '*.html'
    exclude '*.css'
}

// Mapを渡すこともできる
FileTree targetFiles2 = fileTree(dir: 'root', include: '*.html', exclude: '*.css')

コピー

Project.copy()を使用する方法

build.gradle
// ルートプロジェクト直下のsample.groovyをcloneディレクトリにコピー
copy {
    from 'sample.groovy' // コピー元ファイル (ディレクトリも指定できる)
    into 'clone' // コピー先のディレクトリ
}

Copy型の組み込みタスクを使用する方法

build.gradle
tasks.register('copyReport', Copy) {
    from 'aaa/bbb.text'
    into 'clone'
}

参考

プログラミングGROOVY 関谷 和愛 (著), 上原 潤二 (著), 須江 信洋 (著), 中野 靖治 (著) 技術評論社
Gradle徹底入門 次世代ビルドツールによる自動化基盤の構築 綿引 琢磨 (著), 須江 信洋 (著), 林 政利 (著), 今井 勝信 (著) 翔泳社

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2