Jenkinsコードリーディング - JDK自動インストールの謎を追え


## TL;DR

- 自動インストールでインストールされるJDKのアーキテクチャは、各Nodeを登録するために起動するslave.jarのJREのアーキテクチャだよ
- Jenkinsのソースコードは構造のバランスがいい
- Jenkinsは週次リリースできるソフトウェアのアーキテクチャを学ぶ題材として最適

## きっかけ

Jenkinsの便利な機能に、JDKの自動インストールがある。プロジェクトに利用するJDKを設定すれば、実行するノードに自動でJDKをインストールしてくれる、という素晴らしい機能だ。WindowsやLinuxだと32bit、64bitのJDKが配布されているので、どうやってインストールするJDKを判定してるんだろう、と思ってコードを読み始めた。

## ゴール

- 自動インストールを実装しているクラスを見つけ、どのように実装されているか理解すること

## 実装クラスを見つける

実装を見つけるときは、ログを出力して実際にその操作をすると、どのクラスで実行されているかすぐにわかる。ただ、今回はテスト用のJenkinsを立ち上げるのは面倒だったので、ソースコードリポジトリの中を見てみることにした。

[JenkinsのリポジトリはGitHubで公開されている。](https://github.com/jenkinsci/jenkins)リポジトリが公開されているので、リポジトリの中を手当たり次第パッケージをたどってパッケージから全体の構造を推測してみようと試みた。しかし、hudson時代のパッケージもあったりしてどこを見たらいいのかわからない。hudson時代のパッケージが残るのは、APIの互換性維持のためだと思うので、仕方ないことだとおもう。

あきらめて、JDKのインストールなので、`JDK`でリポジトリを検索してみた。すると[hudson.tools.JDKInstaller](https://github.com/jenkinsci/jenkins/blob/7ad3f50121560b94a5caa191732eb01827a075d7/core/src/main/java/hudson/tools/JDKInstaller.java#L87)というそのものズバリが見つかった。

関連するクラスとか見ると、親クラスが`ToolInstaller `とかなので、このクラスだろう。


## で、どんな実装だった？

[インストール処理をしている部分を抜き出す。](https://github.com/jenkinsci/jenkins/blob/7ad3f50121560b94a5caa191732eb01827a075d7/core/src/main/java/hudson/tools/JDKInstaller.java#L117)

```java
    public FilePath performInstallation(ToolInstallation tool, Node node, TaskListener log) throws IOException, InterruptedException {
...
            Platform p = Platform.of(node);
            URL url = locate(log, p, CPU.of(node));

            FilePath file = expectedLocation.child(p.bundleFileName);
            file.copyFrom(url);
```

NodeのPlatformとCPUアーキテクチャを調べ、そのURLを取得し、ダウンロードする。では、このURLを計算する[locate()](https://github.com/jenkinsci/jenkins/blob/7ad3f50121560b94a5caa191732eb01827a075d7/core/src/main/java/hudson/tools/JDKInstaller.java#L411)では何をやっているのだろう？

```java
        JDKFamilyList families = JDKList.all().get(JDKList.class).toList();
        if (families.isEmpty())
            throw new IOException("JDK data is empty.");

        JDKRelease release = families.getRelease(id);
        if (release==null)
            throw new IOException("Unable to find JDK with ID="+id);

        JDKFile primary=null,secondary=null;
        for (JDKFile f : release.files) {
            String vcap = f.name.toUpperCase(Locale.ENGLISH);

            // JDK files have either 'windows', 'linux', or 'solaris' in its name, so that allows us to throw
            // away unapplicable stuff right away
            if(!platform.is(vcap))
                continue;

            switch (cpu.accept(vcap)) {
            case PRIMARY:   primary = f;break;
            case SECONDARY: secondary=f;break;
            case UNACCEPTABLE:  break;
            }
        }
```

JDKListからJDKFileを取り出し、JDKFile毎にPlatformとCPUがOKとするかチェックしている。`CPU#accept()`で`PRIMARY`と`SECONDARY`があるのは、64bitアーキテクチャのJREが動いているNodeであれば、32bitでもOKだからだろう。この[JDKList](https://github.com/jenkinsci/jenkins/blob/7ad3f50121560b94a5caa191732eb01827a075d7/core/src/main/java/hudson/tools/JDKInstaller.java#L796)はどうやって取得しているのか？

```java
    public static final class JDKList extends Downloadable
        public JDKList() {
            super(JDKInstaller.class);
        }

        public JDKFamilyList toList() throws IOException {
            JSONObject d = getData();
            if(d==null) return new JDKFamilyList();
            return (JDKFamilyList)JSONObject.toBean(d,JDKFamilyList.class);
        }
    }
```

`Downloadable`ってなんだろう？[import文を見てみる](https://github.com/jenkinsci/jenkins/blob/7ad3f50121560b94a5caa191732eb01827a075d7/core/src/main/java/hudson/tools/JDKInstaller.java#L33)と、

```java
import hudson.model.DownloadService.Downloadable;
```

となっているので、`Downloadable`は`DownloadService`の中に定義されたクラスのようだ。[DownloadService](https://github.com/jenkinsci/jenkins/blob/03647c7ce468612d10d741c852f2a8d768b92004/core/src/main/java/hudson/model/DownloadService.java)を検索し、[Downloadableの定義](https://github.com/jenkinsci/jenkins/blob/03647c7ce468612d10d741c852f2a8d768b92004/core/src/main/java/hudson/model/DownloadService.java#L206)周辺を見てみる。

```java
    /**
     * Represents a periodically updated JSON data file obtained from a remote URL.
     *
     * <p>
     * This mechanism is one of the basis of the update center, which involves fetching
     * up-to-date data file.
     *
     * @since 1.305
     */
    public static class Downloadable implements ExtensionPoint {
    ...
        /**
         *
         * @param url
         *      URL relative to {@link UpdateCenter#getDefaultBaseUrl()}.
         *      So if this string is "foo.json", the ultimate URL will be
         *      something like "http://updates.jenkins-ci.org/updates/foo.json"
         *
         *      For security and privacy reasons, we don't allow the retrieval
         *      from random locations.
         */
        public Downloadable(String id, String url, long interval) {
            this.id = id;
            this.url = url;
            this.interval = interval;
        }

        public Downloadable() {
            this.id = getClass().getName().replace('$','.');
            this.url = this.id+".json";
            this.interval = DEFAULT_INTERVAL;
        }
        
        ...
        public Downloadable(Class id) {
            this(id.getName().replace('$','.'));
        }
        ...
        public String getUrl() {
            return Jenkins.getInstance().getUpdateCenter().getDefaultBaseUrl()+"updates/"+url;
        }        
    }
```

JavaDocにちゃんと書いてある。Downloadableのパラメータ`url`は`URL relative to {@link UpdateCenter#getDefaultBaseUrl()}.`と書かれている。`urlはUpdateCenter#getDefaultBaseUrl()からの相対パス`です。もし`foo.json`という文字列であれば、アクセスするURLは`http://updates.jenkins-ci.org/updates/foo.json`のようになる。

さっきの`JDKList`のコンストラクタは`this(JDKInstaller.class)`なので、下記のURLからJSONを取得する。

http://updates.jenkins-ci.org/updates/hudson.tools.JDKInstaller.json

JSONP形式のJSONが取得できるはずだ。

### Q.UpdateCenter#getDefaultBaseUrl()ってほんとに`http://updates.jenkins-ci.org/`なの？

そういう時はJenkinsのスクリプトコンソールを開いて試してみればいい。スクリプトコンソールはJenkinsの管理を開こう。Groovyはスクリプト環境はJavaのコードを解釈できる。UpdateCenterはJenkinsのインスタンスから取得する。下記のように書くといい。

![Kobito.gJ6ZXz.png](https://qiita-image-store.s3.amazonaws.com/0/4244/a2552c42-d31c-aecb-6e07-a1bdef9a06b5.png "Kobito.gJ6ZXz.png")


### JDKInstallerの中のドメインモデル

ところで、JDKInstallerの中で`Platform`や`CPU`というクラスが定義されている。Jenkinsはクラスの定義がモデルと対応していて、感銘を受けた。JDKInstaller#JDKListの周辺クラスは下記のとおりだ。

![Kobito.XFpVPe.png](https://qiita-image-store.s3.amazonaws.com/0/4244/aec688c8-ac5a-82de-bbcc-fd7809c8d0f1.png "Kobito.XFpVPe.png")

個々のクラスが小さいとファイル管理が大変になる。しかし、ネストクラスとして定義することでファイル管理の大変さを解決しつつ、概念をうまくモデル化し、コードに落としこんでいる。

そういう意味でも大変勉強になった。