torutkのブログ

ソフトウェア・エンジニアのブログ

JavaアプリケーションのJPMSモジュール化をGradleで

Javaアプリケーションをモジュール化し、Gradleでビルドするようにした

先日のブログの続きです。

torutk.hatenablog.jp

いろいろな問題点に直面してしまいました。その後の紆余曲折を書いていきます。

使用するIDE

Gradleプロジェクトとして作ったディレクトリは、直接NetBeans IDEIntelliJ IDEAで開くことができます。そこで、Gradle化にあたっては、IDE固有のプロジェクト設定ファイルは設けずにGradleのビルド定義だけを置くことにします。リポジトリに置くソースファイルは文字コードUTF-8とします。

NetBeans IDEの注意点

Windows日本語版上のNetBeans IDE 12.0で文字コードUTF-8のソースファイルを持つGradleプロジェクトを開くと文字化けします。回避策はNetBeans IDEの起動オプションで-J-Dfile.encoding=UTF-8を指定します。ただし、JDK 9~14ではこのオプションを指定するとJavaVMのバグによりクラッシュするので、NetBeans IDEJDK 8で動かします。NetBeans IDEJDK 8で動かしても、プロジェクトのビルドは任意のJDKバージョンを設定できます。

IntelliJ IDEAは問題なく

Windows日本語版上のIntelliJ IDEA 2020で文字コードUTF-8のソースファイルを持つGradleプロジェクトを開くと、UTF-8文字コードを認識し文字化けは発生しません。IntelliJ IDEAの内蔵JDKが独自のパッチを当ててクラッシュを避けているようです。

プロジェクトの構造

JPMS/Gradle 対応前の構造

JPMS/Gradle化する前の EarthGadgetアプリケーションのソースコード構造の概要は次です。

EarthGadget
  +- nbproject/                   <-- NetBeans IDEのプロジェクト定義等が格納
  +- src/                         <-- EarthGadgetアプリケーションのソースコード(画像含む)
  +- build.xml                    <-- NetBeans IDEのAntビルド定義
  +- javafx-gadgetsupport/        <-- Gitサブモジュールで取り込んだライブラリリポジトリ

EarthGadget はライブラリとして別リポジトリにあるjavafx-gadgetsupportを使用しています。javafx-gadgetsupport を GitのサブモジュールとしてEarthGadgetのプロジェクトツリー配下に配置し、一緒にビルドする構造としています。EarthGadgetをビルドすると依存関係からjavafx-gadgetsupportをビルドしてバイナリを生成します。

JPMS/Gradle 対応の構造

Gradle化する際には、EarthGadgetをビルドすると依存関係からjavafx-gadgetsupportをビルドするようにする仕組みを維持したいのでその方法を探りました。

独立したGradleプロジェクトとして構成すると、順番にビルドする必要が生じますが、マルチプロジェクトとして構成すると、依存関係をプロジェクトに対して設定できます。

次はマルチプロジェクトのディレクトリ構成です。チュートリアル、ユーザーマニュアルでは、ルートプロジェクトの下に実際のプロジェクトを並べる構成が記載されています。それにならうと、

EarthGadget
  +-- settings.gradle               <-- サブプロジェクト(earthgadget, javafx-gadgetsupport)を定義
  +-- earthgadget/                  <-- EarthGadgetアプリケーションのGradleサブプロジェクト
  +-- javafx-gadgetsupport/         <-- Gitサブモジュールで取り込んだJavaFX GadgetSupportライブラリのGradleサブプロジェクト

という構成です。これでGradle化してビルドができました。 次は、元の構造に近い形でマルチプロジェクトのディレクトリが構成できないかと試してみました。

EarthGadget
  +-- settings.gradle               <-- サブプロジェクト(javafx-gadgetsupport)を定義
  +-- build.gradle                  <-- EarthGadgetアプリケーションのビルド定義
  +-- src/                          <-- EarthGadgetアプリケーションのソースコード(画像含む)
  +-- javafx-gadgetsupport/         <-- Gitサブモジュールで取り込んだJavaFX GadgetSupportライブラリのGradleサブプロジェクト    

マルチプロジェクト構成のルートプロジェクトにEarthGadgetアプリケーションのビルド定義とソース一式を配置、サブプロジェクトにjavafx-gadgetsupportライブラリを配置します。

移行作業

gradle化作業をgitの新たなブランチで行う

JPMS対応およびGradle対応を、gitの新しいブランチ(migrate_to_gradle)を設けてその上で作業します。

  • Gradle化前のEarthGadgetプロジェクトをgithubからクローン
D:\work> git clone --recursive https://github.com/torutk/EarthGadget.git
Cloning into 'EarthGadget'...
remote: Enumerating objects: 79, done.
remote: Total 79 (delta 0), reused 0 (delta 0), pack-reused 79
Unpacking objects: 100% (79/79), 412.56 KiB | 625.00 KiB/s, done.
Submodule 'javafx-gadgetsupport' (https://github.com/torutk/javafx-gadgetsupport.git) registered for path 'javafx-gadgetsupport'
Cloning into 'D:/work/EarthGadget/javafx-gadgetsupport'...
remote: Enumerating objects: 18, done.
remote: Counting objects: 100% (18/18), done.
remote: Compressing objects: 100% (17/17), done.
remote: Total 74 (delta 0), reused 13 (delta 0), pack-reused 56
Submodule path 'javafx-gadgetsupport': checked out 'd7cbb2d2d9605769d40769133a55ef5a60049261'
D:\work>
  • Gradle化の作業を行うブランチをmasterから新規作成
D:\work> cd EarthGadget
D:\work\EarthGadget> git checkout -b migrate_to_gradle
Switched to a new branch 'migrate_to_gradle'

D:\work\EarthGadget> git branch
  master
* migrate_to_gradle

D:\work\EarthGadget> cd javafx-gadgetsupport

D:\work\EarthGadget\javafx-gadgetsupport> git checkout -b migrate_to_gradle
Switched to a new branch 'migrate_to_gradle'
 
D:\work\EarthGadget\javafx-gadgetsupport> git branch
  master
* migrate_to_gradle

D:\work\EarthGadget\javafx-gadgetsupport> 
javafx-gadgetsupportライブラリのGradle対応

Gradleプロジェクトとするには、gradle initコマンドで必要なファイル群を作成します。その際、種類をbasicにしておくと、雛形のソースコードやそのコードを使いサンプルのライブラリへの依存がないビルド定義ファイルが生成されます。

  • gradle init でGradleプロジェクトの作成
D:\work\EarthGadget\javafx-gadgetsupport> gradle init --type basic --dsl groovy --project-name javafx-gadgetsupport
  • NetBeans固有のプロジェクト定義を削除
D:\work\EarthGadget\javafx-gadgetsupport> git rm -r build.xml nbproject
rm 'build.xml'
rm 'nbproject/build-impl.xml'
rm 'nbproject/genfiles.properties'
rm 'nbproject/project.properties'
rm 'nbproject/project.xml'
  • Javaのライブラリを生成するGradleビルド定義を記述
plugins {
    id 'java-library'
}

java {
    sourceCompatibility = JavaVersion.VERSION_11
    targetCompatibility = JavaVersion.VERSION_11
    modularity.inferModulePath = true
}

sourceSets {
    main {
        java {
            srcDirs = ['src']
        }
    }
}

tasks.withType(JavaCompile) {
    options.encoding = 'UTF-8'
}

ライブラリをビルドするので、プラグインjava-libraryを指定します。 Javaソースコードのバージョンとコンパイル後のクラスファイルのバージョンをJava SE 11に指定します。 JPMSモジュール対応の設定をします。 ソースファイルのディレクトリは、gradleのデフォルトであるsrc/main/java以下ではなく、src以下にあることを設定します。 ソースファイルの文字コードUTF-8であることを指定します。

  • module-info.java をsrc直下に新規作成
module com.torutk.gadget.support {
    requires javafx.controls;
    requires transitive java.prefs;
    exports com.torutk.gadget.support;
}

モジュール名は、代表公開パッケージと同じ名前としています。 requiresにはこのライブラリが使用するライブラリを提供するモジュールを指定しています。 このライブラリのAPIでは、Java Preferences APIの型を使っていますが、ライブラリ利用者はAPIの利用箇所にほぼ限定してJava Preferences APIを使うので、transitiveを指定し、ライブラリ利用者のモジュール定義にjava.prefsモジュールを指定しなくてもよいようにしています。

EarthGadgetアプリケーションのGradle対応
  • gradle init でGradleプロジェクトの作成
D:\work\EarthGadget> gradle init --type basic --dsl groovy --project-name EarthGadget
  • NetBeans固有のプロジェクト定義を削除
D:\work\EarthGadget> git rm -r build.xml nbproject
rm 'build.xml'
rm 'nbproject/build-impl.xml'
rm 'nbproject/genfiles.properties'
rm 'nbproject/project.properties'
rm 'nbproject/project.xml'
  • settings.gradle にjavafx-gadgetsupportライブラリのサブプロジェクトを指定
rootProject.name = 'EarthGadget'
include 'javafx-gadgetsupport'
  • Javaのアプリケーションを生成するGradleビルド定義を記述
plugins {
    id 'application'
}

java {
    sourceCompatibility = JavaVersion.VERSION_11
    targetCompatibility = JavaVersion.VERSION_11
    modularity.inferModulePath = true
}

dependencies {
    implementation project(':javafx-gadgetsupport')
}

sourceSets {
    main {
        java {
            srcDirs = ['src']
        }
        resources {
            srcDirs = ['src']
        }
    }
}

tasks.withType(JavaCompile) {
    options.encoding = 'UTF-8'
}

application {
    mainModule = 'com.torutk.gadget.earth'
    mainClass = 'com.torutk.gadget.earth.EarthGadgetApp'
}

アプリケーションをビルドするので、プラグインにapplicationを指定します。 Javaソースコードのバージョンとコンパイル後のクラスファイルのバージョンをJava SE 11に指定します。 JPMSモジュール対応の設定をします。 依存関係でjavafx-gadgetsupportを指定します。マルチプロジェクトでは依存関係に別のサブプロジェクトを指定することができます。 ソースファイルのディレクトリは、gradleのデフォルトであるsrc/main/java以下ではなく、src以下にあることを設定します。また、リソースファイルのディレクトリも、gradleのデフォルトであるsrc/main/resources以下ではなく、src以下にあることを設定します。 ソースファイルの文字コードUTF-8であることを指定します。 アプリケーションとしてJPMSのメインモジュールと、メインクラスを指定します。

  • module-info.java をsrc直下に新規作成
module com.torutk.gadget.earth {
    requires com.torutk.gadget.support;
    requires javafx.controls;
    opens com.torutk.gadget.earth to javafx.graphics;
}

モジュール名は、代表パッケージと同じ名前としています。 requiresにはこのアプリケーションが使用するライブラリを提供するモジュールを指定しています。 JavaFXのアプリケーションでは、アプリケーション側で作成したクラスに対してJavaFXライブラリ側からリフレクションをかけるので、opensでJavaFXライブラリから実行時にアクセスすることを許可しています。

ビルドと実行
  • ビルド
D:\work\EarthGadget> gradle build
  :
  • 実行
D:\work\EarthGadget>gradle run

WindowsインストーラMSI)の作成

EarthGadgetアプリケーションには、javapackagerコマンドを使ってWindowsインストーラーを作成するバッチファイルを含めていました。しかしjavapackagerコマンドはJDK 11で削除されてしまいました。

javapackagerコマンドの後継として、JDK 14からjpackageコマンドが含まれるようになりました(incubator段階)。今回は、JDK 14のjpackageコマンドを使ってWindowsインストーラー(MSI)を生成するようバッチファイルを修正します。

ツール種類 ツール
JDK Liberica JDK 14
MSIインストーラー作成ツール WiX Toolset 3.10
@echo off

%JAVA_HOME%\bin\jpackage ^
--type msi ^
--win-upgrade-uuid 38d49c58-102e-486b-bfac-8a0f6e796a93 ^
--win-menu ^
--win-menu-group "Tiny Gadgets" ^
--win-shortcut ^
--app-version 0.2.2 ^
--description "Earth 3D rounding on desktop" ^
--name "EarthGadget" ^
--dest build\installer ^
--vendor Takahashi ^
--module-path build\libs;javafx-gadgetsupport\build\libs ^
--module com.torutk.gadget.earth/com.torutk.gadget.earth.EarthGadgetApp ^
--verbose
オプション 意味 指定例
--type インストーラー種類を指定 msi
--win-upgrade-uuid バージョンアップ時に、既にインストール済みの前のバージョンがあればそれを置き換えるための識別子
--win-menu スタートのメニューに追加する
--win-menu-group スタートメニューのフォルダ名
--win-shortcut デスクトップにショートカットを作成
--app-version インストーラに設定するバージョン番号
--description 説明文字列
--name アプリケーション名、実行ファイルの基底名
--dest 作成したインストーラファイルの格納先
--vendor 作成者・組織名
--module-path インストーラに含めるモジュールの置かれているディレクトリのリスト
--module 実行するメインモジュール名
  • --module では、MainClass属性を持つモジュールであればモジュール名のみ指定、MainClass属性を持たないモジュールであればモジュール名/メインクラス名を指定
  • インストーラー実行時にインストールディレクトリを選択可能とするには、--win-dir-chooser オプションを指定
  • インストーラーのインストール先をシステム共通の場所(C:\Program Files 以下)ではなく個人のディレクトリ下とするには、--win-per-user-install オプションを指定
  • JavaVMオプションを指定したい場合は、--java-optionsに指定、インストール後の設定ファイル(<インストールディレクトリ>\app\<アプリ名>.cfg)のJavaOptions項にオプションが記載
  • jpackageコマンドが裏で実行するjlinkの生成ファイルやWiX Toolsetの設定・オブジェクトファイルを確認したい場合は、--tempで空のディレクトリを指定

バッチファイル実行時に、PATHにJDK 14、WiX Toolsetを入れておきます。

インストーラーのファイルサイズとインストール後のサイズ

インストーラーファイルの大きさは、28MBでした。 インストール後のインストールディレクトリ以下の大きさは、85MBでした。

JavaFXを同梱するLiberica JDK 14のインストールディレクトリ以下の大きさは400MB強なので、JPMSモジュール機構による必要なモジュールを抜粋したJava実行イメージの削減効果がよく出ています。

jpackage 感想

jlinkを内部で実行するので、jpackageコマンドだけでインストーラが生成されるのはよいです。

バージョンアップ時に、既にインストール済みの古いバージョンがある場合、アップグレードインストール(古いバージョンが削除され新しいバージョンがインストールされる)を可能にするためUUIDを渡せるのはjavapackagerの時に比べて改善されています。

Javaアプリケーションのモジュール化でGradleを使おうとしたら色々と

Javaアプリケーションをモジュール化する際の様々な障害

以前、JavaFXでちょっとしたデスクトップガジェットプログラムを作ってみました。 これは、ちょっとしたユーティリティライブラリ(次の日記)と、

torutk.hatenablog.jp

それを使ったガジェット風アプリケーションとなります。

GitHub - torutk/EarthGadget: JavaFX 3d earth rounding 他

このプログラムを、JPMS(Java Platform Module System)のモジュール対応とJDK14で導入されたjpackageを使ったインストーラー作成に対応させることにしました。

環境は次です。

項目 内容 備考
OS Windows 10 バージョン1909 Pro 64bit 日本語版
IDE Apache NetBeans IDE 12.0 on Oracle JDK 11.0.7 Oracle JDKは、OTNライセンス(開発用途)の下無償利用
ビルド・実行Java Liberica JDK 14.0.1 JavaFX同梱のFull版
使用リポジトリ GitHub ソースファイルは文字コードUTF-8、改行コードLFで管理

メインクラスを持つモジュール対応JARファイルを生成する

メインクラスを持つモジュール対応JARファイルは、モジュール定義(module-info.class)にMainClass属性を埋め込む必要があります。この埋め込みはjarコマンドで--main-classオプションで指定した場合に行われます。

ガジェット風アプリケーションのビルド環境は、NetBeans IDEでAntをビルドツールとして使うプロジェクトです。 ところが、現時点でAntにはメインクラスを持つモジュール対応JARファイルを作る機能がありません。

自分でメインクラスを持つモジュール対応JARファイルを生成するタスクを書くか、Ant以外の別なビルドツールに移行するかを検討しました。

自分でモジュール対応JARファイルを生成するタスクをAntで書くというのは、今後NetBeansでプロジェクトを作成するときに毎回Antのビルドファイルに手を入れることになるので望ましい姿ではありません。

Ant以外のビルドツールとしては、mavenかgradleかが候補に上がります。現時点ではどちらもメインクラスを持つモジュール対応JARファイルの生成に対応しています。

今回開発に使う環境は個人のプログラム作成用途で、自宅以外でも出張・旅行などの移動先や移動途中などでも使用します。移動先ではWi-Fiが使えるとは限らず、また自宅でも特にコロナ緊急事態宣言中には時折インターネット接続ができない状況があり、インターネット接続ができなくても十分使えるgradleに移行することにしました。

NetBeansのプロジェクトをGradleにする

NetBeansには、Gradleプラグインが用意されており、これをインストールします。 次に、新規プロジェクトを作成([File]メニュー > [New Project]で、[Java with Gradle] > [Java Application])します。Java wtih Gradle で選択可能なプロジェクトの種類は次の3つです。

  • Java Application
  • Java Class Library
  • Multi-Project Build

複数のモジュール対応JARを1つのNetBeansプロジェクトで管理するならMulti-Project Buildです。 今回は1プロジェクト1JARを生成するので、ガジェット風アプリケーションはJava Applicationを選択しました。

問題1 UTF-8Javaソースファイルを開くと文字化け

NetBeansのGradleプロジェクトに、GitHubから取り出したガジェット風アプリケーションのJavaソースファイルを登録し、開こうとすると次の警告がでます。

The file D:/work/EarthGadget/src/main/java/com/torutk/gadget/earth/EarthGadgetApp.java cannot be safely opened with encoding windows-31j. Do you want to continue opening it?

[Yes]で開くと、日本語が壊れて(文字化けして)表示されます。困りました。 ファイル先頭のコピーライト記述コメントで、「©」が「ツゥ」に化けているので、UTF-8文字コードC2 A9を、Shift JISとして解釈しています。

プロジェクトのプロパティを開いて設定を探したのですが、Antプロジェクトの設定にはあったEncoding設定が見つかりません。

この事象は、NetBeansのバグデータベースに登録されていました。

issues.apache.org

  • build.gradleに以下を追加 → 文字化けは解消せず(上記チケットにもうまくいかないと記載あり)
tasks.withType(JavaCompile) {
    options.encoding = 'UTF-8'
}
  • NetBeansの起動オプションに-J-Dfile.encoding=UTF-8を追記 → 上記チケットのコメントで回避方法として紹介されていたが、JDK 11およびJDK 14ではNetBeans起動時にJavaVMがクラッシュ
#
# A fatal error has been detected by the Java Runtime Environment:
#
#  EXCEPTION_ACCESS_VIOLATION (0xc0000005) at pc=0x00007ffb6413fd3e, pid=14828, tid=4892
#
# JRE version: Java(TM) SE Runtime Environment 18.9 (11.0.7+8) (build 11.0.7+8-LTS)
# Java VM: Java HotSpot(TM) 64-Bit Server VM 18.9 (11.0.7+8-LTS, mixed mode, tiered, compressed oops, g1 gc, windows-amd64)
# Problematic frame:
# C  [awt.dll+0x8fd3e]
#
# No core dump will be written. Minidumps are not enabled by default on client versions of Windows
#
# If you would like to submit a bug report, please visit:
#   https://bugreport.java.com/bugreport/crash.jsp
# The crash happened outside the Java Virtual Machine in native code.
# See problematic frame for where to report the bug.
#

---------------  S U M M A R Y ------------

Command Line: -Dnetbeans.importclass=org.netbeans.upgrade.AutoUpgrade -Dfile.encoding=UTF-8 -XX:+UseStringDeduplication -Xss2m -Djdk.gtk.version=2.2 -Dapple.laf.useScreenMenuBar=true 
  :(中略)
---------------  T H R E A D  ---------------

Current thread (0x0000000029be5000):  JavaThread "AWT-EventQueue-0" [_thread_in_native, id=4892, stack(0x000000002c910000,0x000000002cb10000)]

Stack: [0x000000002c910000,0x000000002cb10000],  sp=0x000000002cb0cd90,  free space=2035k
Native frames: (J=compiled Java code, j=interpreted, Vv=VM code, C=native code)
C  [awt.dll+0x8fd3e]

Java frames: (J=compiled Java code, j=interpreted, Vv=VM code)
j  sun.awt.windows.WComponentPeer._setFont(Ljava/awt/Font;)V+0 java.desktop@11.0.7
j  sun.awt.windows.WComponentPeer.setFont(Ljava/awt/Font;)V+7 java.desktop@11.0.7
j  sun.awt.windows.WWindowPeer.initialize()V+42 java.desktop@11.0.7
j  sun.awt.windows.WFramePeer.initialize()V+1 java.desktop@11.0.7
j  sun.awt.windows.WComponentPeer.<init>(Ljava/awt/Component;)V+83 java.desktop@11.0.7
j  sun.awt.windows.WCanvasPeer.<init>(Ljava/awt/Component;)V+2 java.desktop@11.0.7
j  sun.awt.windows.WPanelPeer.<init>(Ljava/awt/Component;)V+2 java.desktop@11.0.7
j  sun.awt.windows.WWindowPeer.<init>(Ljava/awt/Window;)V+2 java.desktop@11.0.7
j  sun.awt.windows.WFramePeer.<init>(Ljava/awt/Frame;)V+2 java.desktop@11.0.7
j  sun.awt.windows.WToolkit.createFrame(Ljava/awt/Frame;)Ljava/awt/peer/FramePeer;+5 java.desktop@11.0.7
j  java.awt.Frame.addNotify()V+20 java.desktop@11.0.7
j  java.awt.Window.pack()V+28 java.desktop@11.0.7
j  org.netbeans.core.startup.Splash.center(Ljava/awt/Window;)V+1
j  org.netbeans.core.startup.Splash$SplashRunner.run()V+11
 :(後略)

スプラッシュ画面を表示しようとして、awt.dll でメモリアクセス違反が発生しています。 OpenJDKのバグデータベースにはこの事象が登録されています。解決はJDK 15(2020年9月リリース予定)となっています。

bugs.openjdk.java.net

これは、Windows上のJDKGUI(awtライブラリ、Swingも内部で使用)を使ったプログラムをJVMオプション-Dfile.encoding=UTF-8を指定して実行すると発生します。UTF-8以外の指定を試すと(例えばeucjp、windows-31j)クラッシュは発生しませんでした。

ということで、現時点ではNetBeans IDEJDK 8で動かすしかなさそうです。

問題2 NetBeans IDEJDK 8で動かすと
  • nb-javac ライブラリ(プラグイン)をインストールするよう案内が出る

NetBeans IDEからjavacを扱う際に、JDK 8までは標準のjavacでは機能・制御が不十分のため、NetBeans用にパッチを当てたnb-javacを使っています。JDK 9以降でNetBeans IDEを動かす場合は不要(ない方がよい?)のため、不整合が生じるかもしれません。

  • HIDPI対応等が劣化する

JDK 9ではWindows上でHiDPIディスプレイに対応していますが、JDK 8では非対応のためツールバーのアイコンが小さすぎるといった弊害があります。ダイアログにおいて文字が欠けてしまう等の問題があります。

問題3 AntベースプロジェクトからGradleベースプロジェクトへの移行

GitHubリポジトリに登録しているNetBeansのAntプロジェクトをNetBeansのGradleプロジェクトへ移行しようとすると、

  • 新規プロジェクト作成では空のディレクトリを指定する必要がある
  • ディレクトリ構造が異なる
    • Antベースのプロジェクトでは、srcディレクトリ下にパッケージに相当するディレクトリツリー
    • Gradleベースの単一モジュールプロジェクトでは、src/main/java ディレクトリ下にパッケージに相当するディレクトリツリー
    • Gradleベースの複数モジュールプロジェクトでは、src/<モジュール名>/classesディレクトリ下にパッケージに相当するディレクトリツリー
  • Antベースのプロジェクトでは画像ファイルをソースファイルと同じディレクトリに置いてるが、Gradleベースのプロジェクトでは画像ファイルをソースファイルと同じディレクトリに置いても成果物(JARファイル)に取り込まれない

といった事態に直面します。

後の問題対応を含めると、NetBeans上からプロジェクト作成をするのではなく、コマンドラインからGradleのコマンドを叩いてプロジェクトを生成させるのがよいのではと思いました(要試行)。

問題4 NetBeansのプロジェクトプロパティ設定から設定できる項目がほとんどない

また、設定を見回って気付いたこととして、GradleプロジェクトではNetBeansのプロジェクトプロパティ設定画面から設定できる項目がほとんどありません。ライブラリの参照も表示されるだけで追加・変更はできません。

ビルドに関する諸設定は、build.gradleファイルをエディタで直接編集するようになっています。

お試しにNetBeansの新規プロジェクト作成で、GradleのJava Application種類を選択すると、次のbuild.gradleファイルが生成されます。

apply plugin: 'java'
apply plugin: 'jacoco'
apply plugin: 'application'


description = 'Gradle Java Application Sample'
    group = 'com.torutk.hello'

mainClassName = 'com.torutk.hello.HelloApp'

repositories {
    jcenter()
}

dependencies {
    testImplementation     'junit:junit:4.13'
}

description、group、mainClassNameは、新規プロジェクト作成時にウィザードから設定した内容です。 javaのソースレベルは、デフォルトではNetBeans IDEを動かしているJDKバージョンとなるので、プロジェクトで使用するレベルを明示的にビルド定義に記述します。groupは、成果物をmavenリポジトリに上げる等がないなら未定義でもよいかも。

問題5 使用するGradleバージョン

[Tools] > [Options] から、[Java] > [Gradle] で Categories欄を[Execution]にすると、GradleのバージョンやOffline設定等ができます。 NetBeans IDEにGradleプラグインを入れた後、Gradleのバージョンは6.3となっています。 そのマシンで初めてGradleプロジェクトを作成するときは、まだGradle(バージョン6.3)が存在しないのでインターネットからダウンロードします。 そのため、オフラインで作業する場合は、一度インターネットに接続した環境でダミーの新規プロジェクトを作成する等の対処が必要です。

また、JavaのJPMSモジュール対応しているのはGradleの6.4以降なので、新規プロジェクト作成後にGradleのバージョンを更新する作業が発生します。

  • Use Standard Gradle Version は6.3のみ選択可

    • いったん6.3でプロジェクト作成後、Gradleバージョンを更新するとインターネットからダウンロードが発生し、それ以降は6.5等の新しいバージョンを選択可能
  • Customで、別途マシンにインストールしたGradleディレクトリを指定することが可能

[Tools] > [Options] から、[Java] > [Gradle] で Categories欄を[Execution]とし、Gradle Distribution領域で[Custom]ラジオボタンを選択、[Browse]ボタンでマシンにインストールしたGradleのディレクトリを指定します。

問題6 ライブラリの参照

ユーティリティライブラリのNetBeansプロジェクトと、それを利用するガジェット風アプリケーションのNetBeansプロジェクトで構成しています。 ガジェット風アプリケーションのbuild.gradleファイルに、どのようにユーティリティライブラリのNetBeansプロジェクト参照を記述するのかが問題でした。

Gradleでは、マルチプロジェクト構成を作ることで複数のプロジェクトをまとめてビルドすることが可能です。

しかし、シングルプロジェクト構成ではプロジェクトを参照することが難しいので、個別にビルドすることとし、ユーティリティライブラリのNetBeansプロジェクトが生成するJARファイルをガジェット風アプリケーションのNetBeansプロジェクトから参照するようにします。

ローカルのJARファイルを参照させるには、repositories {...} で flatDirによるローカルのディレクトリ指定をするか、dependencies {...} で、fileTreeで指定するといった方法があります。

ユーティリティライブラリのNetBeansプロジェクトは、gitのsubmodule機能を用いてガジェット風アプリケーションのディレクトリ下に置いているので、相対パスでライブラリを参照します。

dependencies {
    implementation fileTree(dir: "javafx-gadgetsupport/dist", includes: ['*.jar'])
}
  • javafx-gadgetsupport ライブラリプロジェクトは、Antベースのままmodule-info.javaを追加してモジュール対応しています。

次に、ライブラリ参照を従来のクラスパスではなくモジュールパスとして扱うよう定義します。

java {
    modularity.inferModulePath = true
}

依存関係で定義するライブラリがモジュール対応JARのときは、クラスパスではなくモジュールパスとして扱います。

問題7 画像ファイルがJARに取り込まれない

Antベースのプロジェクトで、ソースファイルと画像ファイルとを同一ディレクトリに置いていましたが、Gradleでビルドすると画像ファイルが生成されるJARファイルに含まれなくなりました。

これは、Gradleがデフォルトではソースファイルのディレクトリ(src/main/java以下)にあるファイルはコンパイル対象として扱い、それ以外のファイルは無視しています。リソースファイル(プロパティファイルや画像ファイルなど)は、リソースファイルのディレクトリ(src/main/resources以下)に置いたものがJARファイルに取り込まれます。

Gradleのデフォルトのソースディレクトリ構造ではなく、Antベースのソースディレクトリ構造として扱うには次の記述をします。

sourceSets {
    main {
        java {
            srcDir 'src'
        }
        resources {
            srcDir 'src'
        }
    }
}
問題8 Gradleのユーザーマニュアルは1200ページ以上もある

ドキュメントが充実していることはとても素晴らしいですが、分量が多いです。

問題9 マルチプロジェクトのビルド定義記述が

マルチプロジェクトのビルドを書こうとしますが、いくつか嵌りポイントがあって頓挫しました。

  • ルートプロジェクトのbuild.gradleに 共通の定義を記述すべく、subprojects {...} の中にplugins { ... } 形式を記述すると、Could not find method plugins() for arguments とエラーに

プラグインの指定の記述方法が2種類(apply plugin: xxx と plugins { id xxx })あり、plugins {...} はビルド定義のトップに記述しなくてはならずsubprojects{..}の中には記述できないようです。apply pluginsの書き方をする必要があります。

WSL 2でCentOS 8

WSL 2でCentOS 8を利用する

以前、WSL(WSL 1)でCentOS 7を利用するブログを記載しました。

torutk.hatenablog.jp

ブログで言及しているWSL向けCentOSの入手元には、Windows 10 2004 Update以降のWSL1/WSL2向けにCentOS 8.1 1911のPre-releaseが提供されています。

今回、Windows 10 2004 Updateを適用したPCにこのCentOS 8.1を入れて動かしてみることにします。

WSL 2の準備

WSL 2は、Hyper-Vハイパーバイザー上の仮想マシン(lightweight utility virtual machine:軽量ユーティリティVM)でLinuxカーネルを実行、そのカーネル上でLinuxディストリビューションのユーザーランドを実行します。一般的なHyper-V仮想マシンに対して非常に短時間で軽量ユーティリティVMは起動します。また、Windowsとの連携がいろいろ用意されています。

WSL 2を利用するには、Windowsの機能の有効化または無効化で次の2つを有効にしておく必要があります。

  1. LinuxWindowsサブシステム
  2. 仮想マシン プラットフォーム

また、2020年6月21日現在、Windows 10 2004 Update適用後の状態から、手動でカーネルコンポーネントの更新作業を実施する必要があります(今後はWindows Updateに取り込まれる予定)。

WSL 2 Linux カーネルの更新 | Microsoft Docs

CentOS 8を入れてみた

WSL向けCentOSの公開サイトから、CentOS 8をダウンロード Releases · yuk7/CentWSL · GitHub

ダウンロードしたアーカイブファイル内のCentOS8.exeとrootfs.tar.gz を、C:\Uesrs\<ユーザー名>\AppData\Local\Packages\CentOS8 の下に格納し、CentOS8.exeを実行

インストール後はWSL1用となっていました。

C:\> wsl -l -v
  NAME       STATE           VERSION
* CentOS8    Running         1
  • WSLのデフォルト設定でバージョンを2にセットしておくと、WSL2用でインストールされるかも。

既存のWSL1向けディストリビューションをWSL2にする

C:\> wsl --set-version CentOS8 2
変換中です。この処理には数分かかることがあります....
WSL 2 との主な違いについては、https://aka.ms/wsl2 を参照してください

完了すると、WSL2、すなわちLinuxカーネル上で動作するようになります。

ネットワークについて

WSL2のLinux側は、Windows 10上に作成されるHyper-V仮想スイッチに接続される仮想ネットワークアダプタを持つよう設定されます。このときIPアドレスはクラスBのプライベートアドレスが割り振られます。ネットワークアドレスはWindows 10の起動毎に変わります。

WSL2から外部へのネットワーク接続は、NAT経由となります。

CentOS 8の設定

今回利用したCentOS 8はかなりのミニマムセットなので、いろいろパッケージを追加インストールしていく必要があります(sudoパッケージも未インストール)。

それらは今後記載予定。

さくらVPSのOSをCentOS 8に更新、さらにRedmineを4.1に更新(続々々々)

さくらVPSのOSをCentOS 8に、Redmineを4.1に更新、の続き(5日目)

既に対処が終わっている内容ですが、以下の続きです。

torutk.hatenablog.jp

MariaDB文字コードを、utf8mb4に設定しました。これはUTF-8で1~4バイトの範囲を扱える設定です。一方、MariaDB文字コードをutf8とした場合は、UTF-8で1~3バイトの範囲(基本プレーン)しか扱えません。utf8のデータベースに、例えばUTF-8で4バイトで表現される絵文字をWikiに記載し保存しようとするとエラーとなってしまいます。

ここで、今回の更新で起きた問題は、RedmineのこれまでのデータベースはMySQLのutf8で作成したものを移行してきたことに起因するものです。具体的には、新たにMariaDB文字コードをutf8mb4に設定してもデータベース(テーブル)はutf8のままであり、UTF-8の4バイト文字を入れるとMariaDBの設定自体はutf8mb4であってもテーブルはutf8のためエラーとなるというものです。

今回、MariaDBを設定後にRedmineのデータベースを手動で作成し、その後に旧RedmineMariaDBからダンプしたデータをインポートしたので、データベースはutf8mb4ですが、Redmineの各テーブルがutf8のままとなっている状況です。

utf8からutf8mb4への変更は、UTF-8の1~3バイト文字からUTF-8の1~4バイト文字への拡張のみであるため、文字のデータそのものはまったく変更する必要はなく、単にテーブル(カラム)の文字列の文字コード設定をutf8からutf8mb4へ変更するだけとなります。

1つのテーブルについて、文字コードをutf8からutf8mb4に変更するには次のSQL文を実行します。 例えばattachmentsテーブルの文字コードをutf8mb4に変更する場合のSQL文は次となります。

> ALTER TABLE attachments CONVERT TO CHARACTER SET utf8mb4;

さて、Redmineのデータベースには、テーブルが多数(約70個)あります。 上述のコマンドを各テーブルについて一つ一つ入力して実行するのは大変です。

SQL文で頑張る方法もありそうですが(SELECT CONCAT ...)、ちゃんと動くか不安があったため、Linuxのテキストファイルに落としてから、これを実行することにしました。

~$ for t in $(mysql -uredmine -pxxxxxxxx redmine -e "show tables" -s -N); do echo "ALTER TABLE $t CONVERT TO CHARACTER SET utf8mb4;"; done > alter_tables.sql

あとはこのファイルをMariaDBに読み込ませて実行させます。

~$ mysql -uredmine -pxxxxxxxx redmine < alter_tables.sql

IntelliJ IDEAでGradleを使ってJPMSプロジェクトの作成

IntelliJ IDEAでGradleを使ってJPMS(モジュールシステム)対応のプロジェクトを作成

JavaのビルドツールGradleが、まもなくリリースされるバージョン6.4で、Java SE 9で導入されたJava Platform Module System(略称JPMS)に対応します。Java SE 9がリリースされた2017年9月から3年近くが経過し、ようやくGradleがJPMS対応されます。

今回は、IntelliJ IDEA上でGradleをビルドツールに使うJPMS対応のプロジェクトを作成する経緯を記します。

開発環境は次です。

項目 内容
OS Windows 10 Pro 64bit 日本語版
JDK Liberica JDK 14 full 64bit
IntelliJ IDEA Community 2020.1.1
Gradle 6.4 RC-4

Liberica JDK 14のインストール

Liberica JDKの提供元 Bell Software社から JDK 14.0.1 Full versionをダウンロードします。

https://bell-sw.com/

bellsoft-jdk14.0.1+8-windows-amd64-full.zip

Full versionはJavaFXを同梱したJDKとなっています。Windows用のLiberica JDKMSIインストーラー形式とZIP形式とが用意されています。開発環境では各種各バージョンのJDKを多数揃える(同居させる)ので、zip版をダウンロードし展開するのがよいでしょう。

IntelliJ IDEA Community版

IntelliJ IDEA Community版(無料)をインストールします。 https://www.jetbrains.com/ja-jp/idea/download/

Gradle 6.4 RC4のインストール

Gradle Build Tool - Releases を開き、[release candidates]リンクを辿り、Install Manually項の[Binary-only]をクリックすると、Gradle 6.4 RC4のバイナリzipファイルをダウンロードします。

gradle-6.4-rc-4-bin.zip

このファイルをマシン上の適切な場所(C:\Program Files\Java\gradle-6.4-rc-4)に展開しました。

IntelliJ IDEAでJDKの設定

プロジェクト作成前に、IntelliJ IDEA上でLiberica JDK 14 full のJDK定義を作成しておきます。

  • [File]メニュー > [Project Structure] で「Project Structure」画面を開き、左側ペインで[Program Settings] > [SDKs]を選択、中側ペインの[+]をクリックし、[Add JDK]を選択して「Select Home Directory for JDK」画面でJDKのインストールディレクトリを指定します。

  • 「Select Home Directory for JDK」画面の右側ペインでName欄に指定したJDKに適切な名称を記入します(例:Liberica JDK 14 full)。

IntelliJ IDEAでJavaアプリケーション(単一モジュール)プロジェクト

JPMSモジュールを指定して実行するアプリケーションを作成します。今回はJPMSの定義、ビルド、実行に着目するため、依存ライブラリは使用しません。

プロジェクトの作成

IntelliJ IDEA上でGradleを使うプロジェクトを新規作成

まず、IntelliJ IDEAのプロジェクト作成機能でGradleのプロジェクトを作成します。

IntelliJ IDEAを起動、新規プロジェクト(Gradle)を作成します。なお、これで作成したプロジェクトはGradleのバージョンが古い(IntelliJ IDEA 2020.1の場合、Gradle 6.1)ので、後の手順で新しいGradleを使うよう設定を変更します。

  • 「New Project」画面の左側ペインで[Gradle]を選択し、右側ペインでProject SDK欄に前の手順で定義したJDK名(例:Liberica JDK 14 full)を選択します。
  • Additional Libraries and Frameworks欄は、[Java]にチェックを付けて、[Next]ボタンをクリックします。
  • Name欄にプロジェクト名(例:HelloJavaFx)を記入、Location欄にプロジェクトの基点ディレクト1を記入
  • Artifact Coordinatesの左端にある▼印をクリックし、詳細設定項目を展開します。
  • GroupId欄に、成果物のグループ識別子(通常代表モジュールのパッケージ名逆順、例:com.torutk.hello)を記入します。
  • ArtifactId欄に、成果物の名前(デフォルトではプロジェクト名が展開済み)を記入します。
  • Version欄に、バージョン名を記入します。
    f:id:torutk:20200505224804p:plain
    IntelliJ IDEA新規プロジェクト(Gradle)の成果物定義画面
Gradleプロジェクトのディレクトリ構成

IntelliJ IDEAで作成したGradleプロジェクトのディレクトリ・ファイル構成は次です。

HelloJavaFx
│  build.gradle
│  gradlew
│  gradlew.bat
│  settings.gradle
│
├─.gradle
│  ├─6.1
│  │  │  gc.properties
│  │  │
│  │  ├─executionHistory
│  │  │      executionHistory.bin
│  │  │      executionHistory.lock
│  │  │
│  │  ├─fileChanges
│  │  │      last-build.bin
│  │  │
│  │  ├─fileHashes
│  │  │      fileHashes.bin
│  │  │      fileHashes.lock
│  │  │
│  │  └─vcsMetadata-1
│  ├─buildOutputCleanup
│  │      buildOutputCleanup.lock
│  │      cache.properties
│  │      outputFiles.bin
│  │
│  ├─checksums
│  │      checksums.lock
│  │
│  └─vcs-1
│          gc.properties
│
├─.idea
│      .gitignore
│      compiler.xml
│      gradle.xml
│      misc.xml
│      workspace.xml
│
├─gradle
│  └─wrapper
│          gradle-wrapper.jar
│          gradle-wrapper.properties
│
└─src
    ├─main
    │  ├─java
    │  └─resources
    └─test
        ├─java
        └─resources
使用するGradleをIntelliJ IDEA同梱のものから別途インストールしたものへ変更

IntelliJ IDEAの[File]メニュー > [Settings]で「Settings」画面を開き、左側ペインで[Buid, Execution, Deployment] > [Build Tools] > [Gradle]を選択、右側ペインで[Gradle Projects]領域内のUse Gradle from欄をデフォルトの'gradle-wrapper.properties' fileからSpecified locationに変更、右隣の欄にGradle 6.4 RC4ディレクトリパスを設定します。

その下のGradle JVM欄は、プロジェクトで使用するJVM(例:Liberica JDK 14 full)を選択します。

[OK]ボタンを押すと、Gradleの設定ファイルとのSyncが実行されます。

  • 既存のGradleプロジェクトでGradleのバージョンを更新する場合、通常は gradle wrapper --gradle-version 6.4-rc-4 と実行して指定バージョンへ更新するようです。
build.gradleファイルの編集

IntelliJ IDEAが生成したbuild.gradle(次に示す)は、JPMS対応前のバージョン用の定義です。

plugins {
    id 'java'
}

group 'com.torutk.hello'
version '0.1.0'

repositories {
    mavenCentral()
}

dependencies {
    testCompile group: 'junit', name: 'junit', version: '4.12'
}

Gradle 6.4のJPMS対応のドキュメント(以下のURL)を参照し、修正をします。 Gradle 6.4-rc-4 Release Notes

GradleプラグインJava一般用からアプリケーション用に変更します。

  plugins {
-     id 'java'
+     id 'application'
  }

実行クラスを含む実行可能モジュールとメインクラスを定義します。

+ application {
+     mainModule = 'com.torutk.hello'
+     mainClass = 'com.torutk.hello.MessageBoard'
+ }

モジュールパス推論をセットします。

  version '0.1.0'

+ java {
+     modularity.inferModulePath = true
+ }

リポジトリ定義、依存関係定義は今回使用しないので削除しておきます。

build.gradle

修正後のbuild.gradleファイルの全体象は次です。

plugins {
    id 'application'
}

group 'com.torutk.hello'
version '0.1.0'

java {
    modularity.inferModulePath = true
}

application {
    mainModule = 'com.torutk.hello'
    mainClass = 'com.torutk.hello.MessageBoard'
}

モジュール定義

src\main\javaディレクトリの下にモジュール定義(module-info.java)を作成します。

HelloJavaFx
└─src
    ├─main
    │  ├─java
    │  │  │  module-info.java

モジュール定義の内容は次です。

module com.torutk.hello {
    requires javafx.graphics;
    opens com.torutk.hello to javafx.graphics;
}
  • 作成するアプリケーションのモジュール名をcom.torutk.helloと定義
  • アプリケーションモジュールcom.torutk.helloは、javafx.graphicsモジュールを使用(requiresの依存関係)
  • このアプリケーションモジュールに含むcom.torutk.helloパッケージは、javafx.graphicsモジュールに対して実行時にのみアクセスを許可(リフレクションでアクセス可能とする)

メインクラスの作成

src\main\javaディレクトリの下に、パッケージcom.torutk.helloに対応するディレクトリを作成し、その中にメインクラスを配置します。

└─src
    ├─main
    │  ├─java
    │  │  │  module-info.java
    │  │  │
    │  │  └─com
    │  │      └─torutk
    │  │          └─hello
    │  │                  MessageBoard.java

メインクラスの最初の雛形ソースコードは次です。

package com.torutk.hello;

import javafx.application.Application;
import javafx.stage.Stage;

public class MessageBoard extends Application {

    @Override
    public void start(Stage primaryStage) throws Exception {
        primaryStage.show();
    }
}
  • ここでは空の(真っ白な)ウィンドウを1つ表示するだけのメインクラスを記述

ビルド

では、早速ビルドしてみます。

IntelliJ IDEAのメニューからビルド

[Build]メニュー > [Build Project] を実行します。

23:40:16: Executing tasks ':classes :testClasses'...

> Task :compileJava
> Task :processResources NO-SOURCE
> Task :classes
> Task :compileTestJava NO-SOURCE
> Task :processTestResources NO-SOURCE
> Task :testClasses UP-TO-DATE

BUILD SUCCESSFUL in 1s
1 actionable task: 1 executed
23:40:18: Tasks execution finished ':classes :testClasses'.

メニューからビルドを実行すると、Gradleのタスクのうち classes と testClasses (とその依存タスク)だけを実行しています。 ビルドの結果生成されるディレクトリ・ファイルは次です。クラスファイルのみ生成されています。

├─build
│  ├─classes
│  │  └─java
│  │      └─main
│  │          │  module-info.class
│  │          │
│  │          └─com
│  │              └─torutk
│  │                  └─hello
│  │                          MessageBoard.class
│  │
│  ├─generated
│  │  └─sources
│  │      ├─annotationProcessor
│  │      │  └─java
│  │      │      └─main
│  │      └─headers
│  │          └─java
│  │              └─main
│  └─tmp
│      └─compileJava
Gradleのbuildタスク実行

Gradleペインの中から[Tasks] > [build] > [build] を実行します。 f:id:torutk:20200506234836p:plain

Build結果出力は次です。

23:44:40: Executing task 'build'...

> Task :compileJava
> Task :processResources NO-SOURCE
> Task :classes
> Task :jar
> Task :startScripts
> Task :distTar
> Task :distZip
> Task :assemble
> Task :compileTestJava NO-SOURCE
> Task :processTestResources NO-SOURCE
> Task :testClasses UP-TO-DATE
> Task :test NO-SOURCE
> Task :check UP-TO-DATE
> Task :build

BUILD SUCCESSFUL in 504ms
5 actionable tasks: 5 executed
23:44:41: Task execution finished 'build'.

先ほどのclasses、testClassesタスク以外にも、多数のタスクが実行されているのが分かります。 ビルド結果は次です。

├─build
│  ├─classes
│  │  └─java
│  │      └─main
│  │          │  module-info.class
│  │          │
│  │          └─com
│  │              └─torutk
│  │                  └─hello
│  │                          MessageBoard.class
│  │
│  ├─distributions
│  │      HelloJavaFx-0.1.0.tar
│  │      HelloJavaFx-0.1.0.zip
│  │
│  ├─generated
│  │  └─sources
│  │      ├─annotationProcessor
│  │      │  └─java
│  │      │      └─main
│  │      └─headers
│  │          └─java
│  │              └─main
│  ├─libs
│  │      HelloJavaFx-0.1.0.jar
│  │
│  ├─scripts
│  │      HelloJavaFx
│  │      HelloJavaFx.bat
│  │
│  └─tmp
│      ├─compileJava
│      └─jar
│              MANIFEST.MF

クラスファイルだけでなく、tar、zipファイル、jarファイル、それからシェルスクリプト/バッチファイルなども生成されています。

  • distributions のtar/zipファイルには、アプリケーションのjarファイルと実行用スクリプト/バッチファイルが含まれています。
  • libsのアプリケーションjarファイルは、実行可能JARファイルでかつ実行可能モジュールJARファイルとなっています。
コマンドプロンプトからjavaコマンドで実行

Liberica JDK 14にパスを通したコマンドプロンプトからアプリケーションを実行します。

D:\work\HelloJavaFx> java -p build\libs -m com.torutk.hello
  • モジュールJARファイルのあるディレクトリ(build\libs)を-pオプションで指定
  • 実行可能モジュール名(com.torutk.hello)を-mオプションで指定
配布イメージファイルを展開し中のバッチファイルから実行

build\distributions\HelloJavaFx-0.1.0.zip を適当な場所に展開します。

Liberica JDK 14にパスを通したコマンドプロンプトから、上述の展開した中にあるbin\HelloJavaFx.batを実行します。


  1. ここで指定したディレクトリの下に、gradle、srcなどのディレクトリおよびbuild.gradle、gladlewなどのファイル群が生成されます

さくらVPSのOSをCentOS 8に更新、さらにRedmineを4.1に更新(続々々)

さくらVPSのOSをCentOS 8に、Redmineを4.1に更新、の続き(4日目)

先日の続きです。

torutk.hatenablog.jp

Redmineログのローテーション

SELinux下でlogrotateを使ってRedmineのログ・ローテーションを実現するには設定がそれなりに手間なので、Railsの機能を使ってログローテーションを設定します。

Redmineインストールディレクトリのconfig/additional_environment.rb にログ設定を記述

config.logger = Logger.new('log/production.log', 30, 10 * 1024 * 1024)
  • 一つ目の引数は、ログファイルのパスを指定します。Railsアプリケーションのルートディレクトリからの相対パスで指定可。
  • 二つ目の引数は、ローテートするときに残す世代数(ファイル数)です。
  • 三つ目の引数は、ログファイルの容量がこの閾値に達したらローテーションを実施する値です。

gitサーバー

このマシンに共有gitリポジトリを設けて、Redmineから参照し、またHTTP経由でリポジトリのクローンとプッシュをできるようにします。

HTTPからのアクセスにおいて、HTTP用にアカウントを作成し運用管理するのは負担が大きいですから、RedmineのアカウントでHTTPの認証を行うようにします。Redmineには現時点ではApache HTTPD用の認証モジュールが含まれていますがNginxには対応していません。

Feature #7061: Use redmine.pm in nginx - Redmine

そこで、Git(Subversionリポジトリアクセス用にApache HTTPDをインストールします。

gitリポジトリの格納ディレクトリとSELinux許可

gitリポジトリに対しては、読み込み、書き込み、およびスクリプトの実行が必要です。HTTP経由でgitリポジトリにアクセスするには、HTTPのプロセスに与えられるドメインhttpd_tから、gitリポジトリのリソースに対する読み込み(map操作含む)、書き込み、スクリプト実行の操作の許可があるリソースタイプをgitリポジトリに割当てます。

CentOS 8のデフォルトSELinux設定では、gitリポジトリを置くと想定しているディレクトリに対して次のリソースタイプが割当てられています。また、ディレクトリ内のファイルに対して許可される操作も併記します。調査コマンドについては後述します。

基点ディレクト リソースのタイプ ドメインhttpd_tからファイルへの許可された操作
/var/lib/git git_sys_content_t getattr ioctl lock map open read
/var/www/git git_content_t getattr ioctl lock map open read

これらのデフォルトSELinux設定では、書き込み操作およびスクリプトの実行操作が許可されていません。 そこで、書き込み操作と、必要なディレクトリに限定して実行操作を許可するリソースタイプを設定します。

  1. ドメインhttpd_tから/var/lib/git 以下のリソースに対して読み書き許可
  2. ドメインhttpd_tから/var/lib/git/<リポジトリ名>/hooks 以下のリソースに対して実行許可

ドメインhttpd_tから上述の許可を持つリソースタイプを調査したところ、次のタイプが適しています。

  1. git_rw_content_t
  2. git_script_exec_t

SELinuxのポリシー設定を変更します。

/var/lib/git/下のリポジトリディレクトリは、読み書きが可能なgit_rw_content_tを割当てます。デフォルトでルール定義があるので-mオプションで変更するコマンドを実行します。

# semanage fcontext -m -t git_rw_content_t '/var/lib/git(/.*)?'

リポジトリディレクトリ下のhooks/ディレクトリは、この中にあるスクリプトを実行できるようSELinuxのタイプをgit_script_exec_tに割当てます。

# semanage fcontext -a -t git_script_exec_t '/var/lib/git/[^/]+/hooks(/.*)?'

/var/lib/gitディレクトリを作成し、念のためSELinuxのリソースを再割り当てしておきます。

# mkdir /var/lib/git
# restorecon -R /var/lib/git
リソースに割り当てられるタイプの調査
# semanage fcontext -l | grpe git
  :
/var/lib/git(/.*)?     all files     system_u:object_r:git_sys_content_t:s0
/var/www/git(/.*)?  all files     system_u:object_r:git_content_t:s0
  :
ドメインhttpd_tからリソースへのアクセス可能な操作の調査

ドメインhttpd_tからタイプgit_sys_content_tへの許可を調べるコマンドの実行例を以下に示します。

$ sesearch -A -s httpd_t -t git_sys_content_t
allow httpd_t file_type:dir { getattr open search };
allow httpd_t file_type:filesystem getattr;
allow httpd_t git_sys_content_t:dir { getattr ioctl lock open read search };
allow httpd_t git_sys_content_t:file { getattr ioctl lock map open read };
allow httpd_t git_sys_content_t:lnk_file { getattr read };

ドメインhttpd_tからタイプgit_content_tへの許可を調べるコマンドの実行例を以下に示します。

$ sesearch -A -s httpd_t -t git_content_t
allow httpd_t httpd_content_type:dir { getattr ioctl lock open read search }; [ httpd_builtin_scripting ]:True
allow httpd_t httpd_content_type:dir { getattr open search };
allow httpd_t httpd_content_type:dir { getattr open search }; [ httpd_builtin_scripting ]:True
allow httpd_t httpd_content_type:dir { getattr open search }; [ httpd_builtin_scripting ]:True
allow httpd_t httpd_content_type:dir { getattr open search }; [ httpd_builtin_scripting ]:True
allow httpd_t httpd_content_type:file { getattr ioctl lock map open read };
allow httpd_t httpd_content_type:file { getattr ioctl lock open read }; [ httpd_builtin_scripting ]:True
allow httpd_t httpd_content_type:lnk_file { getattr read }; [ httpd_builtin_scripting ]:True

アトリビュートに束ねられているリソースタイプの確認方法は次です。

$ seinfo --attribute=httpd_content_type -x

Type Attributes: 1
   attribute httpd_content_type;
        apcupsd_cgi_content_t
        apcupsd_cgi_htaccess_t
           :
        git_content_t
        git_htaccess_t
        git_ra_content_t
        git_rw_content_t
        git_script_exec_t
        httpd_sys_content_t
        httpd_sys_htaccess_t
        httpd_sys_ra_content_t
        httpd_sys_rw_content_t
        httpd_sys_script_exec_t
        httpd_user_htaccess_t
        httpd_user_ra_content_t
           :
ドメインhttpd_tから、指定の操作が可能なリソースタイプを調査
$ sesearch -A -s httpd_t -c file -p write
  :
allow httpd_t git_rw_content_t:file { append create getattr ioctl link lock open read rename setattr unlink write }; [ httpd_builtin_scripting ]:True
バックアップしていたgitリポジトリの復元

/var/lib/git ディレクトリの下に、バックアップしていたgitリポジトリを展開します。 (今回はtarで固めたバックアップファイルを単に展開)

# cd /var/lib/git
# tar xzf ~/git_swe_primus-20200423.git.tgz
# ls
swe.primus.git
# restorecon -R .

展開後、SELinuxのタイプが正しく付いていないこともあるので、restoreconで反映します。 また、次の手順でApache httpdをインストールした後で、ファイルのパーミッションapacheに変更します。

Apache httpd のセットアップ

Redmineと連携するリポジトリアクセス用Webサーバーには、Apache HTTPDを使います。 既にWebサーバーにはNginxを稼働させているので、ポート番号をずらしてApache HTTPDを稼働します。

Apache HTTPDのインストール

Apache httpd は、モジュールとして提供されています。

#  dnf module list httpd
CentOS-8 - AppStream
Name        Stream       Profiles                        Summary
httpd       2.4 [d]      common [d], devel, minimal      Apache HTTP Server

ヒント: [d]efault, [e]nabled, [x]disabled, [i]nstalled

現時点ではバージョンは1つだけ存在するので、そのままモジュールをインストールします。

# dnf module install httpd
  :
Apache HTTPDの動作方式

Apache HTTPDが同時に複数のリクエストを受けて動作する際の並行処理の動作方式には複数の種類があります。Multi Processing Module: MPMと呼ばれる動作方式には、prefork、worker、eventの3種類があり、今回インストールしたCentOS 8のモジュールではデフォルトがeventになっています。 preforkは古のApache httpdからあるシングルスレッドプロセスを複数稼働させ、1リクエストを1プロセスで受ける方式、workerとeventはマルチスレッドプロセスを複数稼働させ、複数リクエストを1プロセスで受ける方式です。

  • /etc/httpd/conf.modules.d/00-mpm.conf

この設定ファイルで方式に応じたsoをLoadModuleします。

さて、Redmine認証連携のためにmod_perlを使用しますが、このmod_perlは古いので event方式で支障なく動くものなのか確証がありません。そこで今回はMPM方式をpreforkに変更します。

- #LoadModule mpm_prefork_module modules/mod_mpm_prefork.so
+ LoadModule mpm_prefork_module modules/mod_mpm_prefork.so
- LoadModule mpm_event_module modules/mod_mpm_event.so
+ #LoadModule mpm_event_module modules/mod_mpm_event.so 

また、子プロセス数等のpreforkの設定を記述するファイルを用意します。gitリポジトリへのアクセスに使うだけのHTTPDで、仮想マシンでCPU数もほとんどないので、起動プロセス数は最小限にします。

  • /etc/httpd/conf.d/mpm.conf
<IfModule mpm_prefork_module>
  # 起動時に生成する子プロセス数
  StartServers          2
  # アイドルな子プロセスの最小個数
  MinSpareServers       2
  # アイドルな子プロセスの最大個数
  MaxSpareServers       2
  # 子プロセス数の設定可能な上限
  ServerLimit           2
  # 最大同時リクエスト数
  MaxRequestWorkers     2
  # 子プロセスが稼働中に扱うリクエスト数の上限
  MaxConnectionsPerChild 4
</IfModule>
Apache HTTPDのデフォルトポート変更

既にNginxがポート80を使用しているので、Apache httpdはポートをデフォルトの80から8008等に変更します。SELinuxではhttpdプロセス(ドメインhttpd_t)がbindできるポートも制限がかけられています。

SELinuxの設定を変更することなく利用可能なポート番号の調べ方は次です。

http_port_t が対象とするポート番号を確認します。

# semanage port -l 
  :
http_port_t                    tcp      80, 81, 443, 488, 8008, 8009, 8443, 9000
  :

ドメインhttpd_t は、http_port_t に対して name_bind 操作が許されています。

$ sesearch -A -s httpd_t
  :
allow httpd_t http_port_t:tcp_socket name_bind;
  :
  • /etc/httpd/conf/httpd.conf を修正します。
-Listen 80
+Listen 8008

-ServerName
+ServerName www.torutk.com:8008
Apache httpdのデフォルト設定変更(不要な設定の削除)

/etc/httpd/conf.d/ にある設定ファイルのうち使用しないものを削除(またはリネーム)します。

# cd /etc/httpd/conf.d
# mv welcome.conf welcome.conf.orig
# mv autoindex.conf autoindex.conf.orig
# mv ssl.conf ssl.conf.orig
# mv userdir.conf userdir.conf.orig

SSLモジュールを読み込まないよう設定を変更します。

- LoadModule ssl_module modules/mod_ssl.so
+ #LoadModule ssl_module modules/mod_ssl.so
Apache httpd経由のGitリポジトリ動作確認

まずはRedmine認証連携の前に、Apache httpdの起動とGitリポジトリ動作確認をします。

  • /etc/httpd/conf.d/git-redmine.conf を新規作成します。
SetEnv GIT_PROJECT_ROOT /var/lib/git
SetEnv GIT_HTTP_EXPORT_ALL
SetEnv REMOTE_USER $REDIRECT_REMOTE_USER

ScriptAlias /git/ /usr/libexec/git-core/git-http-backend/

<LocationMatch "^/git/">
  Require all granted
</LocationMatch>

認証はしないので、Require には"all granted"を記述します。

ポート8008を一時的に開きます。

# firewall-cmd --add-port=8008/tcp

httpd自動起動設定かつ起動します。

# systemctl start --now httpd

リポジトリパーミッションを変更します。

# cd /var/lib/git
# chown -R apache:apache *

リモートからhttp経由でクローンと変更のプッシュを行い、動作確認をします。 まずクローンを実行します。

D:\work> git clone http://www.torutk.com:8008/git/swe.primus.git
Cloning into 'swe.primus'...
remote: Enumerating objects: 739, done.
remote: Counting objects: 100% (739/739), done.
remote: Compressing objects: 100% (279/279), done.
remote: Total 739 (delta 232), reused 739 (delta 232)Receiving objects: 100% (739/739), 20.23 MiB | 20.23Receiving objects: 100% (739/739), 20.67 MiB | 20.06 MiB/s, done.

Resolving deltas: 100% (232/232), done.

変更をローカルでコミット後、プッシュします。

D:\work\swe.primus>git push
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 6 threads
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 273 bytes | 136.00 KiB/s, done.
Total 3 (delta 1), reused 0 (delta 0)
To http://www.torutk.com:8008/git/swe.primus.git
   40ba3f1..6bd5f2d  master -> master
mod_perlのインストールとRedmine認証設定

Redmineの認証を利用して、Git(Apache HTTPD)の認証を行います。 Redmineに同梱されている Redmine.pm (Perlモジュール)を、httpdmod_perlの検索パスに置きます。

# mkdir -p /etc/httpd/Apache/Authn
# sudo ln -s /var/lib/redmine/extra/svn/Redmine.pm /etc/httpd/Apache/Authn/Redmine.pm

このRedmine.pmをApache HTTPDから利用するために、HTTPDmod_perl モジュールを追加する必要があります。mod_perlは少々古い仕組みで、CentOS 7以降はOS標準には含まれなくなっているので、EPELリポジトリから取得します。

EPELリポジトリ利用設定をインストールします。

# dnf install epel-release

EPELリポジトリからパッケージをインストールするのは限定的とするため、デフォルトではEPELリポジトリを無効とする設定に変更します。

  • /etc/yum.repos.d/epel.repo
  [epel]
  name=Extra Packages for Enterprise Linux $releasever - $basearch
  #baseurl=https://download.fedoraproject.org/pub/epel/$releasever/Everything/$basearch
  metalink=https://mirrors.fedoraproject.org/metalink?repo=epel-$releasever&arch=$basearch&infra=$infra&content=$contentdir
- enabled=1
+ enabled=0
  gpgcheck=1
  gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-EPEL-8

mod_perlをインストールします。

# dnf --enablerepo=epel install mod_perl
  :

perl-Digest-SHAをインストールします。

# dnf install perl-Digset-SHA
  :

/etc/httpd/conf.d/git-redmine.conf に追記

+ PerlLoadModule Apache::Authn::Redmine

  SetEnv GIT_PROJECT_ROOT /var/lib/git
  SetEnv GIT_HTTP_EXPORT_ALL
  SetEnv REMOTE_USER $REDIRECT_REMOTE_USER

  ScriptAlias /git/ /usr/libexec/git-core/git-http-backend/

  <LocationMatch "^/git/">
+   PerlAccessHandler Apache::Authn::Redmine::access_handler
+   PerlAuthenHandler Apache::Authn::Redmine::authen_handler
+   AuthType Basic
+   AuthName "Git Redmine"
+   AuthUserFile /dev/null

+   RedmineDSN "DBI:mysql:database=redmine;host=localhost"
+   RedmineDbUser "redmine"
+   RedmineDbPass "XXXXXX"
+   RedmineGitSmartHttp yes

-  Require all granted
+   Require valid-user
  </LocationMatch>

以前のApache httpd + Gitでは、AuthUserFile /dev/nullは不要だったかと思いますが、現時点ではこの指定が必要です([authn_file:error] AH01619: AuthUserFile not specified in the configurationが出る)。

この設定ファイルにはRedmineMySQLデータベースへの接続情報が記載されるので、アクセス権を厳し目に設定します。

# chmod 0400 /etc/httpd/conf.d/git-redmine.conf

Apache httpdをリロードして動作確認します。

# systemctl reload httpd
Nginxのhttps経由でのアクセス

ここまでの設定でGitリポジトリへの読み書きのアクセスができるようになりました。 しかし、httpプロトコルでポート8008にアクセスしており、認証はBasicのため平文で流れてしまいます。

せっかくサーバーにSSLサーバー証明書を設置し、セキュアなアクセスができるようになっているので、SSLでGitリポジトリにアクセスしたいところです。

既にNginxがhttps(ポート443)を押さえているので、Apache HTTPDSSL設定をすることができません。そこで、Ngixを経由してApacheにアクセスすることでSSLを利用します。

  • /etc/nginx/conf.d/redmine.conf に追記
     location ^~ /.well-known/acme-challenge/ {
         root /usr/share/nginx/html;
     }

+    location ^~ /git/ {
+        proxy_pass http://localhost:8008;
+   }

     location / {
         try_files /maintenance.html $uri/index.html $uri.html $uri @app;
     }

この設定で、Nginxがポート80もしくは443でgitへのアクセスを受けると、localhostの8008に転送します。 しかし、これだけではSELinuxでエラーになってしまいます。ドメインhttpd_tが別なプロセスにTCP接続するには許可が必要になります。

SELinuxのブール値設定で、httpd_can_network_relay を on にします。

# getsebool -a | grep httpd_can_network
httpd_can_network_connect --> off
httpd_can_network_relay --> off
# setsebool -P httpd_can_network_relay on
# getsebool httpd_can_network_relay
httpd_can_network_relay --> on

さくらVPSのOSをCentOS 8に更新、さらにRedmineを4.1に更新(続々)

さくらVPSのOSをCentOS 8に、Redmineを4.1に更新、の続きの続き

昨日の作業の続きです。 torutk.hatenablog.jp

Let's Encrypt の証明書更新

証明書更新の処理確認(エラー対処)

証明書を更新するスクリプトの実行確認(dry-run)をします。

# certbot-auto renew --dry-run
  :
Challenge failed for domain www.torutk.com
http-01 challenge for www.torutk.com
Cleaning up challenges
Attempting to renew cert (www.torutk.com) from /etc/letsencrypt/renewal/www.torutk.com.conf produced an unexpected error: Some challenges have failed.. Skipping.
All renewal attempts failed. The following certs could not be renewed:
  /etc/letsencrypt/live/www.torutk.com/fullchain.pem (failure)
  :
IMPORTANT NOTES:
 - The following errors were reported by the server:

   Domain: www.torutk.com
   Type:   unauthorized
   Detail: Invalid response from
   https://www.torutk.com/.well-known/acme-challenge/***************************************
"<!DOCTYPE
   html>\n<html>\n<head>\n  <meta charset=\"utf-8\" />\n
   <title>Redmine 404 error</title>\n  <style>\n    body {font-family:
   \"Tr"

   To fix these errors, please make sure that your domain name was
   entered correctly and the DNS A/AAAA record(s) for that domain
   contain(s) the right IP address.

Redmine環境からの移行にあたり、Redmineへのアクセスを完全https対応とし、httpでのリクエストはhttpsへリダイレクトするように設定しました。 このため、Let's Encryptの証明書更新時にドメイン確認のため http でのアクセスがredmineに転送されてしまい、上述のように404エラーとなっています。

対策は、Let's EncryptからのアクセスをRedmineへ転送せず、nginxのデフォルトのdoc rootにアクセスさせるようNginxに設定を追加します。

/etc/nginx/conf.d/redmine.conf

server {
  listen 443 ...
    :
    client_max_body_size 1G;

+    location ^~ /.well-known/acme-challenge/ {
+       root /usr/share/nginx/html;
+    }

    location / {
        try_files /maintenance.html $uri/index.html $uri.html $uri @app;
    }

最初はポート80のserver設定にlocation記述を追加しましたが、return でhttpsへリダイレクトしてしまうのは避けられず証明書更新がエラーに。そこで、ポート443のserver設定に記述することにしました。

location の記述で、^~ を追加すると、この条件に前方一致したときは後続の条件を見ずに適用するとあるので、これを指定しました。

証明書更新の定期実行(cron)

証明書更新を定期的に実行させるため、cronに登録します。

Let's Encryptの証明書の有効期限は90日で、期限30日前から次の更新を受け付けるとあるので、定期処理の間隔は最大1か月以内とします。certbot-autoは、更新対象となる期限前に実行しても証明書更新をスキップするので、毎日実行しても問題はありません。公式サイトにもcron設定の例で1日2回実行する設定が記載されています。

ただ、何か気になるので、週に1回実行することとします。

crontabに記述してもよいのですが、CentOS 8にはanacronという仕組みがあり(もっと前のバージョンからありますが)、毎時、毎日、毎週、毎月の定期処理を行うスクリプトを所定の場所に放り込んでおくとそれを実行してくれます。

今回は、毎週の処理を行う/etc/cron.weeklyディレクトリに、証明書更新のスクリプトを入れておくことにします。

/etc/cron.weekly/letsencrypt_renew.sh

#!/bin/sh
/usr/local/bin/certbot-auto renew -q
/usr/bin/systemctl reload nginx

ファイルには実行権を付けておきます。

# chmod +x /etc/cron.weekly/letsencrypt_renew.sh

テーマの設定

Redmineでは、テーマにGitmikeを使っていました。しかし、Redmine 4.1環境で sidebar_hide プラグインを使うと、サイドバーを開いたときにコンテンツ領域が本来より余分に小さくなってしまいました。(ぱっと見た目でサイドバーの幅の2倍だけ縮んで表示)

そこで、別なテーマを探してみました。

Redmine 4対応を謳っているいるテーマ
  • A1:ダウンロードにユーザー登録必要
  • OpenMind:青系、フラットでシンプル
  • PurpleMine2:紫、カラフル、サイドバーが左側かつ折り畳み可
  • RTMaterial:緑系、割とシンプル
  • MinimalFlat2:紺系、文字大き目、シンプル
  • Bleuclair:青系

Sidebar_hideプラグインとの相性をみつつ、今回はBleuclairを入れることとしました。

Bleuclairテーマ

blog.redmine.jp

Redmineのバージョン毎にブランチが用意されているので、ブランチを指定してクローンします。

~$ cd /var/lib/redmine/public/themes
themes$ git clone -b redmine4.1 https://github.com/farend/redmine_theme_farend_bleuclair.git bleuclair
  :

システム管理者でログインし、[管理] > [設定] > [表示] で、テーマ欄のドロップダウンリストから[Bleuclair]を選択します。