torutkのブログ

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

ソフトウェア開発環境が含む範囲

ソフトウェア開発環境が含む範囲

本記事は、製品としてソフトウェアを作る場合で、複数の開発者が共同して作業する、企業でのビジネスを想定しています。プログラミング言語は、コンパイラ型でオブジェクト指向プログラミング機能を想定しています。

逆に、サービスの提供が目的でソフトウェア自体が製品ではない場合、開発規模が小さい(ごく少人数の開発で阿吽の呼吸が通じる)場合、既にある製品やサービスを活用し、ちょっとしたアドオンやカスタマイズで実現する場合、などは本記事の想定外となります。

開発環境と一言でいうとどこまでを含むのか

ソフトウェアの開発環境を整備しようとしたら、何を含めるのかについて同じ職場でもコンセンサスがなかなか得られていません。そんなときは両極端な思考をしてみて検討の幅を持たせて適切なところを探ります。

最小限の開発環境

まず、片方の極端な思考として、もっとも少ない開発環境を考えます。ミニマムではソースコードを実行体にコンパイル(ビルド)するのに必要なコンパイラがあればいいでしょう。作業効率も問わなけらば、ソースコードの記述は何でもよくて(メモ帳でもWordPadでも)、ソースファイルは日時のフォルダに整理して保存し、ソースファイルをコンパイルして実行体を生成します。

最大限の開発環境

続いて、もう片方の極端な思考として、もっとも多い開発環境を考えます。

プログラミング作業が用意になるよう統合開発環境IDE:Integrated Develop Environment)を用意します。開発者がそれぞれバラバラなソースコードを記述しないようコーディング規約を策定し、IDEに可能な限り規約に合わせた設定を行い、それを全員に配布し適用します。プロジェクトで定めたファイル先頭コメント(ライセンス記述やプロジェクト記述等)やコメントの雛形等をファイル新規作成時に自動で展開できるようにIDEに設定しておきます。設定を展開するのに必要であればIDEプラグインを作って全員に配布します。

ソースコードは構成管理ツールの下に集約し、ソースコードの作成、修正、履歴管理、リリース管理の方法を構成管理規約を策定し全員に配布します。要件(仕様)からのトレーサビリティを確保するため、要件から展開したタスクをタスク管理システムに登録し、タスク番号をブランチ名としてソースコードをコミットし管理するなども構成管理規約に含めます。

ソースコードの品質を評価する作業も開発の一部として開発環境に組み込みます。品質の代表的な評価手段としてレビュー(インスペクション)とテストがあります。レビューはプロセスやライブラリの外部インタフェース(プロセス間通信、ファイル等のリソースアクセス、APIなど)、プログラムの構造(パッケージ/クラス構造、ライブラリ構成)、ドキュメントコメントから生成する設計情報、各段階のテスト仕様、ソースコードを対象とするので、レビューの実施体制、レビュー基準、レビュー記録などの手順、計画、管理方法などを策定します。

ソースコードに対して静的解析ツールを適用して、予め定義した検査ルールに逸脱する箇所を検出します。検査ルールは、命名規約からネストの深さ、パッケージやクラスの構成(クラス数、メソッド数、ファンイン/ファンアウト数、他)、循環複雑度、凝集性、APIの呼び出し方法、バグパターンなどを定義しておきます。組み込み方も、ある程度開発が進んでから一括して検証するだけでなく、開発者がコードを書いている最中に検証をしておくのが望ましいので、IDEへ組み込めるようにします。

ユニットテストツールを適用して、小さいモジュール単位(クラス/メソッド等)で機能の確認をします。その際、カバレッジツールや動的解析ツールを併用してテストの検証範囲や計算機リソース(CPU、メモリ、ディスク、ネットワーク等)の使用状況を評価します。ユニットテストで不可欠なモックの作成をツールを使って効率化するならモックツールも適用します。

開発されたソースコードをまとめてビルドしテスト環境や納品用のリリース媒体を作るビルド・リリース環境も用意します。ビルドしたソースコードのレビジョンとリリースバージョンの対応付け、バグ管理ツールとの対応(そのリリースで修正されたバグ、既知のバグなど)などを行って、リリースノートへの記載などができるようにします。このビルド環境にも静的解析ツールを組み込んでの評価、ドキュメントコメントからのドキュメント生成などができるようにしておきます。

テストや運用中に発生したバグを管理し、バグの修正コードを記述するために、バグ管理ツールを導入し、バグ管理規約を作成します。バグ管理ツールは構成管理ツールとの紐づけが最低限必要です。

  • IDEとその設定(プラグイン含む)
  • コーディング規約
  • 構成管理ツール
  • 構成管理規約
  • 要件管理ツール(またはタスク管理ツール)
  • レビュー管理ツール
  • レビュー規約
  • 静的解析ツール
  • ユニットテストツール
  • カバレッジツール
  • 動的解析ツール
  • テスト規約(ユニットテスト
  • ビルドツール(自動ビルド)
  • リリースツール(必要ならインストール媒体作成を含む)
  • ドキュメント生成ツール
  • バグ管理ツール
  • バグ管理規約

ソフトウェア開発環境が対象とするもの

ソフトウェア開発環境が対象とするものをソースコードとするのか、製品の媒体(バイナリ、インストーラー等)とするのかもブレがありそうです。

ソースコードまでを範囲とする場合、各開発者が自分の環境でソースコードの作成をすることが主作業で、作業の必要上コンパイルする、ユニットレベルのテストをする、デバッグをする、といった範囲が開発環境となります。ソースコードをテスト環境や運用環境に展開する、あるいはインストール用媒体を生成する、などはおざなりに付く程度です。

実行体までを範囲とする場合は、テスト環境や運用環境に展開する、あるいはインストール用媒体を生成するまでが開発環境となります。

品質活動をどこまで開発環境に取り込むかによって、対象が増減します。

規約とツール

IDEEclipseを使います。ソースコード管理にGitLabを使います。テストはJUnitを使います。ビルドにはJenkinsを使います。バグ管理にはJIRAを使います。とツールだけを決めても開発現場は混乱します。ファイルの改行コード、エンコーディングがバラバラ、ビルドツールはIDE固有の人、Ant使う人、maven使う人などでビルド困難、命名規約がバラバラ、パッケージ単位もバラバラ(やたら細かい人、数十クラスを1つのパッケージにする人)、コメント記述バラバラ、ユニットテストの書き方、メンテナンスがバラバラ、などなど。

しまらないまとめ

ソフトウェア開発環境はピンキリですが、品質保証活動を取り入れた製品の開発を行う場合は、言わば開発対象のビジネス業務を丸ごと一つ業務設計するような決め事が必要になります。これを、ソフトウェアをエンジニアリングすることと言いたいのですが、伝わらないですねぇ。

でも、それをしなないとカオスの渦に飲み込まれてしまいます。

JDK 9のjavac "--release"オプションについて

はじめに

The Java Module System 2月1日開催の Java読書会「The Java Module Sysmem」を読む会(第4回)において、コラム枠にJDK 9で導入されたjavacコマンドのオプション--releaseの記載がありました。 --releaseオプションは、開発(コンパイル)に使用するJDKのバージョンよりも古いJava SEバージョンで実行可能なクラスファイルをコンパイルするために指定します。そして、JDK 8までの-source、-targetおよび-bootclasspathオプションを置き換えるものです。

JDK 8までの指定

開発(コンパイル)に使用するJDKバージョンより古いバージョンのJavaで実行可能なバイトコード(クラスファイル)を生成するために、javacには以前から次のオプションが用意されています。

  • -source
  • -target

コンパイルする際に適用するJava言語仕様のバージョンとコンパイルしたクラスファイルを実行可能なバージョンを指定することができます。例えば、JDK 11を開発環境に使っていて、Java SE 8で実行可能なクラスファイルを生成したい場合などに使用します。

ただし、この2つのオプションだけでは、-targetで指定したバージョンより新しいバージョンで追加されたAPIを使ったソースコードコンパイルしてもエラー、警告等は出ません。ですから、Java SE 8より後に追加されたAPIを呼び出すコードを-source 8 -target 8とオプションを指定してJDK 11のjavacコマンドでコンパイルしてもエラーにはならず、生成されたクラスファイルをJava SE 8で実行するとAPIが存在しないため実行時エラーとなってしまいます。

D:\work> javac -source 8 -target 8 Hello.java
警告:[options] ブートストラップ・クラスパスが-source 8と一緒に設定されていません
警告1個

D:\work> dir /b
Hello.class
Hello.java

D:\work>

後で述べる-bootclasspathが指定されていないと警告が出ますが、コンパイルは通ってクラスファイルは生成されています。

  • JDK 8で実行するとエラー
D:\work> java Hello
Exception in thread "main" java.lang.NoSuchMethodError: java.lang.Runtime.version()Ljava/lang/Runtime$Version;
        at Hello.main(Hello.java:3)

そこで、新しいバージョンのJDKを使用して-sourceオプションと-targetオプションを指定し、それより古いバージョンのJava SEで実行できるクラスファイルを生成するには、実行時エラーとなる新しいAPIの使用をコンパイル時にチェックできるよう-bootclasspathオプションを追加します。-bootclasspathには、実行したいバージョンのJDKに含まれるAPIのクラスファイル群(rt.jar)を指定します。

D:\work> javac -source 8 -target 8 -bootclasspath "C:\Program Files\Java\jdk1.8.0\jre\lib\rt.jar"
ShowRuntimeVersion.java:3: エラー: シンボルを見つけられません
        System.out.println("Runtime Version = " + Runtime.version());
                                                         ^
  シンボル:   メソッド version()
  場所: クラス Runtime
エラー1個

-bttoclasspathで実行したいバージョンのJDKAPI(rt.jar)を指定することにより、そのrt.jarに含まれないAPIを使用したソースコードコンパイルエラーとなります。

-source、-target、-bootclasspath の問題点

  • 指定するオプションが3つもあり、それぞれ整合した指定を記述しなくてはならない
  • 開発に使用するJDKバージョンの他に実行時のJDKバージョンをインストールしなくてはならない
  • -bootclasspathの指定で絶対パスが登場するので扱いが煩雑に(開発環境に制約)

JDK 9からの--releaseオプション(JEP 247)

JDK 9からは、新たな--releaseオプションが追加されました。

openjdk.java.net

これは、従来の-source、-target、および-bootclasspathの3つのオプションに替わり、一括で指定できるようにしたとともに、開発に使用するJDKバージョンだけあればよく、実行したいバージョンのJDKはインストール不要(したがって絶対パス指定も不要)という優れもののオプションです。

D:\work> javac --release 8 Hello.java
ShowRuntimeVersion.java:3: エラー: シンボルを見つけられません
        System.out.println("Runtime Version = " + Runtime.version());
                                                         ^
  シンボル:   メソッド version()
  場所: クラス Runtime
エラー1個

D:\work>

なぜ指定したバージョンのJavaにはないAPIがエラーとできるのか

--releaseオプションの仕様を定義したJEP 247によると、ct.symファイルに情報があるとのことです。

JDK 11の場合次の場所にct.symがありました。

<JDK 11インストール基点ディレクトリ>
  +-- bin
  +-- include
  +-- jre
  +-- lib
  |     +-- ct. sym

ct.symはZIP形式アーカイブなので中を見ると次の様にフォルダが並んでいます。

META-INF
6
678
7
76
78
8
87
876
8769
8769A
879
879A
89
89A
9
9-modules
9A
A
A-modules
A789
A9
B

このフォルダ名は、JDKのバージョンを示しているようです。Aは10、Bは11に対応していると思われます。いくつかフォルダの下を探ってみます。

  • 6 フォルダの下のファイル構成(一部のみ展開)
6
+-- org
+-- javax
+-- java
|     +-- util
|     +-- sql
|     +-- security
|     +-- nio
|     |     +-- channels
|     |     |     +-- spi
|     |     |     +-- SocketChannel.sig
|     |     |     :
|     |     +-- CharBuffer.sig
|     +-- net
|     +-- lang
|     +-- io
|     +-- beans
|     +-- awt
|     +-- applet
+--com

全てではないですが、パッケージ名に対応したフォルダとクラス名に拡張子.sigを付けたファイルがツリー構造で存在しています。この拡張子.sigのファイルには、クラス名とメソッドシグニチャが含まれています。javacは、このct.symファイルを参照してコンパイルをしています。

  • 9-modulesフォルダの下のファイル構成(一部のみ展開)
9-modules
+-- java.activation
+-- java.base
|     +-- module-info.sig
+-- java.compiler
+-- java.corba
+-- java.datatransfer
:

こちらには、モジュール定義が格納されています。

  • 9フォルダの下のファイル構成(一部のみ展開)
9
+-- com
+-- java
|     +-- awt
|     +-- io
|     +-- lang
|     |     +-- Math.sig
|     |     +-- Module.sig
|     |     +-- Runtime$Version.sig
|     |     +-- Runtime.sig
|     |     +-- SecurityManager.sig
|     |     +-- StackWalker$StackFrame.sig
|     +-- security
|     +-- time
|     +-- util
+-- javafx
+-- javax
+--jdk
+--sun

JDK 11のct.symには、バージョンとシグニチャの関係が含まれているので、--releaseオプションで指定したバージョンで使用可能なAPIがチェックできるようです。

なお、ct.symの内部構造については仕様が公開されていないので実装依存情報となっている模様です。

「The Java Module System」を読む会これまでのまとめ(その2) #javareading

Java読書会「The Java Module System」を読む会(第1~3回)のまとめ(その2)

torutk.hatenablog.jp

の続きです。

第2章 Anatomy of a modular application

モジュール化したアプリケーションのディレクトリ構成の例
  +- libs    (サードパーティの依存物)
  +- mods    (コンパイル&パッケージ化したモジュール)
  +- monitor   (アプリケーションモジュールの1つ)
  |     +- src
  |     |    +- main
  |     |         +- java  (モジュールのソースコードを含む)
  |     |              +- monitor
  |     |              +- module-info.java  (モジュールの宣言)
  |     +- target
  |          +- classes         (モジュールのコンパイル結果を格納)
  +- monitor.observer (モジュール)
  |     +- src
  • 慣例で、modsフォルダーに生成したモジュールを格納
モジュール化するアプリケーションの作成の流れ
  1. アプリケーションをモジュールに分解する(関心事の分離戦略がよい)
  2. ディレクトリ構造の作成
  3. モジュール宣言
  4. 別モジュールへの依存関係の宣言
  5. モジュール公開APIの定義
  6. モジュールグラフの可視化
  7. コンパイルとパッケージ化
他のモジュールに依存のないモジュールのビルド
$ java -d monitor.observer/target/classes ${source-files}
$ jar --create --file mods/monitor.observer.jar
 -C monitor.observer/target/classes .
他のモジュールに依存のあるモジュールのビルド
$ java --module-path mods
 -d monitor.observer.alpha/target/classes ${source-files}
$ jar --create --file mods/monitor.observer.alpha.jar
 -C monitor.observer.alpha/target/classes .

依存するモジュールのあるフォルダを --module-pathオプションで指定します。

2つのフォルダにあるモジュールに依存のあるモジュールのビルド

アプリケーションのモジュールとサードパーティのモジュールとに依存のあるモジュールをビルドします。 --module-pathオプションで複数のフォルダを指定可能です。

$ javac --module-path mods:libs
 -d monitor.rest/target/classes ${source-files}
$ jar --create --file mods/monitor.rest.jar
 -C monitor.rest/target/classes .
メインメソッドを持つクラスを含むモジュールのビルド
$ javac --module-path mods
 -d monitor/target/classes ${source-files}
$ jar --create --file mods/monitor.jar
 --main-class monitor.Monitor
 -C monitor/target/classes .
アプリケーションの実行
$ java --module-path mods:libs --module monitor

モジュールパスでモジュールを収容するフォルダを指定、メインを持つモジュールの名前を指定して実行します。

第3章 Defining modules and their properties

2つのモジュールファイルフォーマット
  • JMOD
    JDKを構成するモジュールはJMODフォーマットで提供されています。アプリケーション開発者にはJMODファイルを生成する機能は提供されていません
  • モジュラーJAR 通常のJARファイル内のルートディレクトリにモジュール記述子module-info.classが含まれているものです。

モジュラーJARをクラスパスに置くと、モジュール定義が無視され従来のJARファイルとして扱われます。

モジュール宣言(module-info.java
module モジュール名 {
    requires モジュール名;
    exports パッケージ名;
}

モジュール名とパッケージ名の両方が登場し、命名規則が類似しているので混乱しがちです。

ルートモジュールからの依存性解析結果に含まれないモジュールは使用できません。 そこで、javaコマンドのオプション --add-modules や --add-reads で補うことができます。

  • --add-modulesは、指定したモジュール群を、ルートモジュールとして扱う
  • --add-readsは、指定したモジュールから、指定したターゲットモジュール群へrequiresなしでもアクセス可能とする

モジュール宣言(module-info.java)をコンパイルして生成されるクラスmodule-info.classは、モジュール記述子と呼びます。

モジュール名を付けるには次が重要です。

  • グローバルユニーク
  • 安定

ベストな命名は、ドメイン名の逆順です。これはパッケージ名の命名と同じルールです。そのため、モジュールに含まれるパッケージのパッケージ名のプレフィックスをモジュール名にするとよいです。

モジュールの種類
  • アプリケーションモジュール
    JDK以外のモジュール。モジュールパス上に置き、モジュラーJAR形式
  • イニシャルモジュール
    コンパイル開始対象モジュール、またはmainメソッドを持つモジュール
  • ルートモジュール
    JPMSが依存関係解決にとりかかる対象モジュールで、イニシャルモジュールはルートモジュールでもある
  • プラットフォームモジュール
    JDKを構成するモジュール。JMODファイルで提供
  • インキュベーターモジュール
    非標準なプラットフォームモジュール。jdk.incubator.*
  • システムモジュール
    プラットフォームモジュールのサブセットで実行イメージ用に抜粋したものと、jlinkが追加したアプリケーションモジュールから構成。
    java --list-modules で一覧されるもの。
  • オブザーバブルモジュール
    現在のランタイムにあるすべてのプラットフォームモジュールと、コマンドラインで指定したアプリケーションモジュール
  • ベースモジュール
    java.base
  • 明示的なモジュール
    プラットフォームモジュールと、モジュール記述子(module-info.class)を持つアプリケーションモジュール
  • 自動モジュール
    モジュール記述子を持たない、名前が指定されたモジュール(モジュールパスに置いたプレーンJAR)
  • 名前付きモジュール
    明示的なモジュールと自動モジュールの総称。モジュール記述子で定義した名前またはJPMSが推論した名前
  • 名前なしモジュール
    クラスパス上にあるコンテンツ

このうち、自動モジュールと名前なしモジュールはマイグレーションの過程で使用します。

requiresするのは直接利用しているモジュールのみでよい
monitor.persistence
        ↓
monitor.statistics
        ↓
monitor.observer

このモジュール依存関係の場合、monitor.persistenceはmonitor.statisticsをrequiresすればよく、monitor.observerのrequiresは不要です。 また、monitor.persistenceをコンパイルする場合、monitor.statisticsモジュールが存在すればよく、monitor.observerモジュールは存在しなくても構いません。ただし、実行時にはすべてのモジュールが必要です。

循環参照が生じるモジュール依存関係はエラー

「The Java Module System」を読む会これまでのまとめ(その1)

Java読書会「The Java Module System」を読む会(第1~3回)のまとめ(その1) #javareading

Java読書会BOFでは、2019年11月から「The Java Module Systems」(洋書)を読み進めています。

The Java Module System

The Java Module System

  • 作者:Nicolai Parlog
  • 出版社/メーカー: Manning Publications
  • 発売日: 2019/07/23
  • メディア: ペーパーバック

Java読書会は毎月1回実施しており、2020年1月11日に第3回を実施しました。ここまで読んだ書籍の内容を復習のために簡単にまとめてみます。

第1章 First piece of the puzzle

この本の第1章では、なぜモジュールシステムが必要になったのか、これまでのクラスパスおよびJARファイルで構成していた場合で生じる問題を述べ、モジュールシステムが解決しようとしていることはどういうことかを述べています。

これまでのクラスパスおよびJARファイルの問題
  • JAR地獄→JARファイルの過不足
    • 単にJARファイルが足りない
    • 同一FQCNが複数のJARに含まれる(同じライブラリの別バージョン、ファットJAR、ライブラリの分離独立や改名、など)

Google Guavaライブラリを例に、アプリケーションが利用する複数のライブラリでそれぞれ別のバージョンのGuavaライブラリを利用している場合、クラスパス上で先に検出したバージョンのクラスが使われてしまう問題(本来使いたかったクラスがシャドウ)が発生することが解説されています。この問題はコンパイル時・起動時にはチェックができず、良くて実行時にクラッシュ、悪ければ誤動作(動作継続)となってしまう点で厄介です。

  • publicな型は同じアプリケーション内からは自由にアクセスできてしまう
    • ライブラリのメンテナーが内部用に設けたpublicな型を変更したら、アプリケーションから使われていて思わぬ影響が
    • セキュリティチェックが困難

sun.misc.unsafe の例が紹介されています。sun.misc.safeは内部用に設けられ、名前からもJDK外から利用しないことを主張しているのですが、Netty、PowerMock、Neo4J、Hadoop、Hazelcastなどが使っており、結果たくさんのアプリケーションから依存されるパッケージとなっています。

  • 多数のクラス、JARファイルで構成されるアプリケーションの起動が遅い

クラスの検索にすべてのJARをなめるので遅いとあります。

読書会の場では、mavenやgradleのようなビルドツールでも、JARの過不足問題を解決できるとの意見が出ていました。

モジュール(JPMS:Java Platform Module System)の導入

Java 9(2017年9月リリース)で導入されたモジュール(Java Platform Module System、略してJPMS)は、モジュールの同一性とモジュール間の依存関係を扱えるようにしたものです。

  • 基本特性
    • グローバルユニークな名前を持つ
    • 他のモジュールへの依存関係を宣言する
    • 公開するパッケージの構成を制御して明確なAPIを持たせる

書籍では、グローバルユニークな名前として、パッケージ名の命名規約に類似してドメイン名の逆順を推奨しています。

  • JPMSのゴール
    • 信頼性のある構成
    • 強いカプセル化
    • 自動的なセキュリティと保守性向上
    • 起動時間の改善
    • スケーラブルなJavaプラットフォーム

特に最初の2つ(信頼性のある構成、強いカプセル化)は2大ゴールとされています。 JavaVM起動時にモジュールの依存関係を検証し、モジュールの不足や競合があればエラーとします。 モジュールの境界を越えてアクセスできるのは、公開すると宣言されたパッケージの中のpublicな型のみで、リフレクションでもこの制約を回避できません。

逆に、JPMSのゴールに含まれていないのは以下です。

  • バージョン制御
  • 中央リポジトリでのモジュール管理
  • 動的なモジュールグラフ
OpenJDK のモジュール

OpenJDK自身は、100のモジュールから成り、これらはplatform modulesに分類されます。 platform modulesは、効率の良いJMOD形式のファイルで提供されます。platform modules以外のモジュールは、モジュラーJARファイルで提供されます。

java --list-modules

で、OpenJDKのモジュール一覧が表示されます。

java --describe-module <モジュール名>

でモジュールの説明が表示されます。

第2章は次回

Githubのブランチ管理方針~Redmineプラグインの場合

Gitのブランチ管理方針~Redmineプラグインの場合

Redmineプラグインのコード管理の背景

RedmineのGlossary Pluginをフォークして細々とRedmineバージョンアップに対応しています。

https://github.com/torutk/redmine_glossary

Redmineプラグインのコードを管理する上で、ターゲットとなるRedmineバージョンを明確にしておくことはとても重要なことです。 というのは、Redmine本体がバージョンアップする際に、RedmineがベースとしているフレームワークであるRuby on Railsをメジャーバージョンアップした場合、プラグインにも大きな変更が必要になるからです。

Ruby on Railsはバージョンアップに対して後方互換性を考慮することはあまりなく、APIの廃止、変更がばしばしと入ってきます。そのため、RedmineプラグインRuby on Railsのメジャーバージョンアップに対応させるには、プラグインのコードにAPIの廃止と新しいAPIへの移行を行うなどの修正を入れていきます。

Redmine 3.4(Ruby on Rails 4.2)からRedmine 4.0(Ruby on Rails 5.2)へのバージョンアップに際してプラグインの修正が必要となった非互換性については一昨年に次の記事にまとめました。

Redmine 4.0(Rails 5.2)に用語集(Glossary)プラグインを対応させる - Qiita

これらの修正では、同じコードでRedmineの旧バージョンと新バージョンに対応することが困難であり、あきらめてRedmine 4.0向けとしました。もし、ひとつのコードで複数のRedmineバージョンに対応するためには、if文でRuby on Railsのバージョンをチェックして新旧の双方のコードを記載するなど、そうとうに煩雑なコードになりそうでした。

そこで、一つのコードで複数のRedmineバージョンに対応するのをあきらめて、Redmineの各メジャーバージョンに合わせてプラグインのコードを分けることにします。ただし、プラグインのコードを分けて管理するには、複数のブランチでリリースを管理することになります。

いままでは、ブランチ管理方針を考えることなく、その場その場で適当にブランチを作成していましたが、これからは混乱しないようにブランチ管理方針を決めてコードを管理していきたいと思います。

Gitのブランチ管理方針を調べる

Git でよく見かけるブランチ方針「A successful Git branching model」は、masterブランチにリリースしたバージョンのタグを打って管理し、releaseブランチはリリース作業中の一時的なブランチとし、リリース準備が完了するとreleaseブランチからmasterブランチにマージを行い、リリースタグを削除します。この場合、新しいバージョン(例:Ver.2.0.0)をリリースした後に、前のバージョン(例:Ver.1.4)の修正をして前のバージョンのマイナーバージョンアップ(例:Ver.1.4.1)をすると、masterへの反映ができなくなってしまいます。

この「A successful Git branching model」に基づいていると思われる「Git flow」というワークフローでは、オプションとしてsupportブランチを設け、master以外に古いバージョンを管理する場合に使用するようになっています。 supportブランチは、リリース後の管理をするためのブランチなので、リリース準備をするreleaseブランチとは別に設け、releaseブランチで準備が完了しmasterへマージをした後に、masterからsupportをブランチさせて使用します。

以下はGit flowで使われるブランチとその概要です。

  • master リリースしているコードの最新。常に1つ。直コミット厳禁でリリースブランチあるいはhotfixブランチからマージによってのみ更新。
  • develop/xxx 開発中のコード。複数のdevelopブランチが並走することがあり、スラッシュに続いてバージョン名を記した命名をする。直コミットも可(1回のコミットで完了しない変更はfeatureブランチを設ける)。
  • feature/xxx 複数のコミットで完了する大掛かりな開発はいったんfeatureブランチを設けてそこへコミットし、完了後にdevelopにマージする。スラッシュに続いて開発コード名(あるいは目指すバージョン番号、開発チケット番号等)を記した命名をする。
  • release/xxx リリース準備をするコード。スラッシュに続いてリリース番号を記した命名をする。リリース準備が完了したらmasterにマージする。masterにはリリース番号のタグを付与する。masterにマージ後そのreleaseブランチは削除する。developに反映したい場合は、masterからdevelopへマージする。
  • hotfix/xxx masterに素早く変更を加えたい場合に、masterからブランチし、masterおよび必要があればdevelopへマージする。xxxはバージョン番号(マイナーバージョンあるいはマイクロバージョン番号を更新したもの)など。
  • support/xxx 過去のバージョンのサポート。リリースしたmasterからブランチし、サポート期間中維持する。

Redmineのバージョンと利用状況

プラグインの保守対象バージョンを判断するには、Redmineの各バージョンがどの程度使われているかを知りたいところです。 年に2回実施されているRedmine.tokyo勉強会では、使用しているRedmineのバージョンなどが参加者アンケート集計結果として公開されています。

Redmine.tokyo 17 questionnaire

2019年11月の場合では、Redmine 4.0が25%弱、3.4が40%弱、3.3~3.0が20%弱、2.6~2.0が10%弱といった分布です(目分量)。

勉強会参加者なので、比較的新しいバージョンを使う動機の大きい人たちという点を考慮に入れても、Redmine 2.0以降のバージョンは現時点では対象と考えておくのがよさそうです。

Redmineのバージョン、Ruby on Railsのバージョン、Rubyのバージョンの関係は次となります。

Redmineバージョン Ruby on Railsバージョン Rubyバージョン
4.0, 4.1 5.2 2.2.2, 2.3, 2.4, 2.5
3.4 4.2 1.9.2, 1.9.3, 2.0, 2.1, 2.2, 2.3, 2.4
3.3 4.2 1.9.2, 1.9.3, 2.0, 2.1, 2.2, 2.3
3.0 4.2 1.9.2, 1.9.3, 2.0, 2.1, 2.2
2.5.2 3.2 1.8.7, 1.9.2, 1.9.3, 2.0, 2.1
2.3.0 3.2 1.8.7, 1.9.2, 1.9.3, 2.0

そこで、supportブランチの下には

| ブランチ名 | 対応Redmine | |++|++| |support/2.x | Redmine 2.3, 2.5 |support/3.x | Redmine 3.0, 3.3, 3.4 | |support/4.0or1/based_original | Redmine 4.0, 4.1 |

の3つを用意します。Redmine 4.0および4.1への対応は、これまでのコードを修正して対応するバージョンと、ゼロから作ったバージョンとがあり、これまでのコードの修正をsupport/4.0or1/based_originalブランチで行っています。ゼロから作り直しは現在developブランチで行っており、最初のリリースをmasterにマージしています。

このブランチの見直しがリポジトリに入っています。

Redmineのテキスト編集をIE上で行う際Escキーで編集内容が消える問題の対処

はじめに

自宅PCや開発PCでは、Webブラウザとして主にFirefoxを使い、たまにChromeを、稀にEdgeを使います。 先月初めに、職場が移って(現職出向から帰任)新しいPCが配布されました。業務システムの対応Webブラウザの関係か、デフォルトがIE 11となっています。IE 11でRedmineのチケットを編集していたところ、漢字変換のON/OFF操作の際に誤ってEscキーを押してしまい、なんとそれまで入力していたテキストが消失してしまいました。

IEのメニューには「元に戻す」的なメニュー項目は存在せず、ググって見つけた元に戻す操作のショートカットキーCtrl-Zでも何故か戻らず、再度入力し直す事態となりました。

IE上でHTMLのFormでテキスト編集をする際にEscキーを押すとそれまで入力した内容が消えるというのは、IEの仕様のようです。しかし、FirefoxChrome、Edgeにはその仕様はありません。

Redmineを使う際、職場では多くの人がIEを使うので、このEsc問題への対処をしたいところです。周囲に聞くと、Escの誤操作で消えるから、エディターでテキストを作成してからコピーするといった対処をしている人もいました。

IEでEscキーによるテキスト消失を避ける手段を調べてみた

調べてみると、対処の手段はHTMLページにJavaScriptでEscキー入力があるとそれを無視する処理を埋め込むというものでした。その実装の多くは、documentのkeydownイベントでキーコードがEscキーの27であればイベントをつぶすというものでした。

Redmineではどう対処するか

Redmineで各ページにJavaScriptを埋め込むぅ? Redmineの内部をいじるのか?と思いましたが、ふとView Customizeプラグインがこの対処に使えそうと思い当たりました。

View Customizeでこの件と似たような処理で、チケットの作成途中にEnterキーが押されるとチケットが登録されてしまうという問題に対処するため、特定の条件でEnterキーを無視するという例を紹介しているブログがありました。

qiita.com

この記事では、次のコードでEnterキーを抑制していました。

$(function(){

    // form内のエンターキー入力を無視
    // トラッカー・ステータス操作時のDOM操作で破棄されないようdocumentにイベントを設定
    $(document).on('keypress', '#issue-form input[type="text"]', function(event) {
        if(event.keyCode == 13) {
            return false;
        }
    })
});

これを真似て、テキスト編集箇所でキー入力があった際、Escキーであれば無視するコードを作成します。 Redmineでは、Wiki機能を持つテキスト編集領域は、HTMLのTextArea要素にclass属性で .wiki-editが指定されています。そこで、次の様に記述しView Customizeに登録しました。なお、パスは .* を指定します。

/*
 テキスト編集中にEscキーを無効にする
(IEでテキスト編集中にEscキーを押すと入力したテキストが消える問題の対処)
*/
$(function() { 
  $(document).on('keydown', '.wiki-edit', function(event) {
    if (event.keyCode == 27) {
      return false;
    }
  });
});

結果

IERedmineのテキスト編集箇所を開いてテキスト編集し、Escキーを押したところ、編集途中のテキストが消えることなく存在し続けました。

WSLでCentOSが利用できたかも?(CentOS 7)

WSLで動くLinuxディストリビューション

現在、WSLの上で動くLinuxディストリビューション(WSLではLinuxカーネルは動かさないので実際にはユーザーランド)は、マイクロソフトストア上にUbuntuOpenSUSE 他いくつか用意されていますが、CentOSはありません。普段CentOSを使っている場合、別なディストリビューションを使うとコマンド体系等が異なるので不便を感じます。

そんな折、WSLへCentOS 7を入れるブログを見かけました。

vogel.at.webry.info

ということで、この方法を追ってみたいと思いました。

方法

WSL上で動かすCentOS 7のユーザーランドを作成・公開しているGitHubリポジトリが次です。

github.com

GitHubのリリースには、バイナリのzipアーカイブCentOS.zip)とソースアーカイブが公開されています。 本日時点では、7.0.1907.1 が最新リリースです。これは、CentOS 7.6.1907 に基づくWSL用ユーザーランドです。

CentOS.zipを解凍し、その中にあるCentOS.exeを管理者権限で実行します(展開したフォルダ上で実行)。 なお、ノートPCのmicroSD上(exFAT)で実行するとエラーとなりました。参考まで。

CentOS7.exeを実行すると、コマンドプロンプトが開き、インストールが完了すると Press any key to continue... と表示されます。 何かキーをたたくとコマンドプロンプトが消えます。

Installing...
Installation Complete!
Press any key to continue...

インストール後、再度CentOS7.exeを実行すると、Bashシェルが開きます。

アンインストール(レジストリ登録の消去)は、CentOS.exe clean を実行します。

コマンドプロンプトからwslconfig /lを実行すると、CentOS7が登録されているのが分かります。

C:> wslconfig /l
Windows Subsystem for Linux ディストリビューション:
openSUSE-Leap-15 (既定)
CentOS7

初期設定

Windows上のコマンド環境Cmderへの設定

タブ機能を持つコマンドプロンプトツール Cmder でCentOS7を開けるよう、CentOS7.exeへのパスを設定したTaskを新規作成します。

ユーザー作成

デフォルトはrootユーザーとなっているので、自分用のユーザーを作成します。管理権限をsudoで実行できるようにwheelグループに登録します。

# adduser <自分用のユーザー名>
# gpasswd -a <自分用のユーザー名> wheel

sudoコマンドでパスワードなしに実行できるよう/etc/sudoersを修正します。

%wheel  ALL=(ALL)       NOPASSWD: ALL

先のCentOS7.exeを実行したときに、ここで作成したユーザーでbashが開くよう設定します。

C:\path\to\CentOS7> CentOS7 config --default-user <自分用のユーザー>

うまくいかないときは、CentOS7 config --default-user root でいったんrootユーザーに戻してbashを実行し再設定します。

SoftwareCollections.org リポジトリ設定

OS標準パッケージとは別に、RHELおよびCentOS向けに新しいバージョンのパッケージを提供するSoftware Collectionsを利用できるようにします。

$ sudo yum install centos-release-scl-rh

例えば、rubyの場合、CentOS 7標準のパッケージは ruby-2.0.0ですが、Software Collectionsで提供されるパッケージには、

  • rh-ruby22
  • rh-ruby23
  • rh-ruby24
  • rh-ruby25
  • rh-ruby26

があります。

ruby 2.6のインストール
$  sudo yum install rh-ruby26

Software Collectionsでインストールしたパッケージは、/opt下にインストールされます。rh-ruby26の場合は次になります。

/opt/rh/rh-ruby26/
  +-- enable
  +-- root/

enableファイルには、環境変数定義が記述されているので、これをbash起動時に読み込まれるよう設定します。

$ sudo ln -s /opt/rh/rh-ruby26/enable /etc/profile.d/rh-ruby26.sh

以降新たにbashを起動すると、ruby26が利用できるようになります。

~$ ruby -v
ruby 2.6.2p47 (2019-03-13 revision 67232) [x86_64-linux]

パスを通すには、/opt/rh/rh-ruby26/enable へのシンボリックリンクを、/etc/profile.d/rh-ruby26.sh として作成します。

~$ sudo ln -s /opt/rh/rh-ruby26/enable /etc/profile.d/rh-ruby26.sh
~$ bash
~$ ruby -v
ruby 2.6.2p47 (2019-03-13 revision 67232) [x86_64-linux]

Redmine プラグイン開発環境を作ってみる

Redmineプラグイン開発環境を作ってみます。前に作成した環境は次です。

Redmineプラグイン開発環境 - ソフトウェアエンジニアリング - Torutk

git

RedmineGitHubリポジトリから取得するために、gitをインストールします。 CentOS 7の標準パッケージのgitはバージョンが1.8.3と今や相当古いバージョンです。Software Collectionsでgit 2.9を取得します。

~$ sudo yum install rh-git29

環境設定を/etc/profile.d下に作成します。

~$ sudo ln -s /opt/rh/rh-git29/enable /etc/profile.d/rh-git29.sh

RedmineGitHubレポジトリからクローンしてきます。

~$ mkdir redmine
~$ cd redmine
redmine$ git clone  https://github.com/redmine/redmine.git redmine_glossary_dev
  :

sqlite開発環境

~$ sudo yum install sqlite-devel

ruby bundler

~$ sudo yum install rh-ruby26-rubygem-bundler

Redmineが必要とするgemsのインストール

redmine_glossary_dev$ bundle install --path vendor/bundler
bundle installでエラーになった対処
~$ sudo yum groupinstall "Development Tools"
  :
sudo yum install openssl-devel readline-devel zlib-devel curl-devel libyaml-devel
~$ sudo yum install rh-ruby26-ruby-devel

セッション秘密鍵の生成

redmine_glossary_dev$ bundle exec rails generate_secret_token

データベース初期化(スキーマ生成)

redmine_glossary_dev$ bundle exec rails db:migrate

Redmineの実行

redmine_glossary_dev$ bundle exec rails server

Redmineへのアクセス

同一マシン上で、Webブラウザから http://localhost:3000 にアクセスするとRedmineが表示されます。

環境変数NAMEを無効化

WSLで使用するCentOS 7のユーザー環境変数NAMEにはホスト名が定義されています。Redmineプラグインをインストールするときに影響が出るので、未定義にしておきます。

  • ~/.bashrc に次を追記
unset NAME

ファイルシステムをwslfsに更新

WSLでインストールした際のファイルシステムは、デフォルトではlxfsですが、これをWslFsに更新することができます。 更新すると何がいいのかは明確には語られていないようなので、更新して良くなると思ったら実施で・・・。

更新するには、コマンドプロンプトから以下を実行します。

C:\> wslconfig /upgrade CentOS7

更新されたかどうかは、ルートファイルシステムを表示して確認します。

~$ df -T
Filesystem     Type  1K-blocks     Used Available Use% Mounted on
rootfs         wslfs 103472168 81965432  21506736  80% /
none           tmpfs 103472168 81965432  21506736  80% /dev
  :

X Window Systemの利用

コマンドライン環境であれば、これで一通り操作できるようになりました。しかし、GUIを伴う環境も使いたくなります。 そこで、Windows OS側にXサーバー機能を入れ、Linux(WSL)側でXクライアントプログラムを実行したらその画面をWindows側に表示できるようにします。

Windows OSへXサーバー機能(VcXsrv)をインストール

Windows用のXサーバー機能には、今回VcXsrvソフトウェアをインストールしました。

sourceforge.net

インストール後、XLaunchをダブルクリックし、Xサーバーを起動します。

HiDPI画面でXの画面がぼやける

VcXsrvのアイコンを右クリックしプロパティで互換性タブを選択、[高DPI設定の変更]を押し、[高いDPIスケールの動作を上書きします。]にチェックを付け、種類は[アプリケーション]を選択。VcXsrvを再起動します。

ただし、Xのアプリケーションが小さく表示されるので、アプリケーション個別にフォントを大きくするなどして使いやすくする必要があります。

Xクライアントプログラムのインストール

~$ sudo yum install xorg-x11-apps

X Window Systemのライブラリ等を含めてインストールされます。

リモートのXサーバーに表示する場合、環境変数DISPLAYを設定します。

~$ export DISPLAY=0:0

xeyesツールを実行します。

~$ xeyes

目玉が表示されればOKです。

RubyMine Linux

RubyMineのLinux版をインストールしてみます。Windows上であらかじめダウンロードしたアーカイブファイルを読み込むことができます。

~$ cd /opt
opt$ sudo tar xvzf /mnt/c/Users/torutk/Downloads/RubyMine-2019.2.4.tar.gz
  :
opt$ cd RubyMine-2019.2.4/bin
bin$ ./rubymine.sh
OpenJDK 64-Bit Server VM warning: Option UseConcMarkSweepGC was deprecated in version 9.0 and will likely be removed in a future release.

Start Failed: Internal error. Please refer to http://jb.gg/ide/critical-startup-errors
  :(略)
Caused by: java.lang.UnsatisfiedLinkError: /opt/RubyMine-2019.2.4/jbr/lib/libawt_xawt.so: libX
tst.so.6: cannot open shared object file: No such file or directory                           

libXtst.so.6 を探す

~$ yum provides libXtst.so.6
Loaded plugins: fastestmirror
Loading mirror speeds from cached hostfile
 * base: ftp.iij.ad.jp
 * centos-sclo-rh: ftp.iij.ad.jp
 * extras: ftp.iij.ad.jp
 * updates: ftp.iij.ad.jp
libXtst-1.2.3-1.el7.i686 : X.Org X11 libXtst runtime library
Repo        : base
Matched from:
Provides    : libXtst.so.6
~$ sudo yum install libXtst

これで、RubyMingeが起動するようになりました。

Redmine をRubyMine上で実行する

  • RubyMineでRedmineを展開したディレクトリを開きます。([File]メニュー > [Open]から)
  • rubyコマンド(インタープリター)の場所を設定します。([File]メニュー > [Settings]から)
  • 初回実行時は、Railsを指定します。

日本語環境設定

日本語フォント

IPAフォント
~$ sudo yum install ipa-*-fonts
  :
VLゴシックフォント
~$ sudo yum install vlgothic-*
WindowsのフォントをVcXsrvで使う
~$ sudo ln -s /mnt/c/Windows/Fonts /usr/share/fonts
~$ sudo fc-cache -fv
  :

日本語入力

ibus-kkcを動かそうと試みるが・・・

CentOS 7標準のIbusを入れてみる。

~$ sudo yum install ibus-kkc
  :

79個のパッケージがインストールされます。 ibus-setup を実行すると、ibus-daemonが動いていないので動かすか聞いてきます。動かすを選択したがエラーで動きません。

$ ibus-setup
Gtk-Message: 14:17:48.995: GtkDialog mapped without a transient parent. This is discouraged.

(ibus-setup:2931): IBUS-WARNING **: 14:17:58.140: Unable to connect to ibus: Unexpected lack of content trying to read a line
portal is not running: GDBus.Error:org.freedesktop.DBus.Error.Spawn.ChildExited: Process org.freedesktop.portal.IBus exited with status 1
Gtk-Message: 14:18:03.295: GtkDialog mapped without a transient parent. This is discouraged.
ibus-mozcを動かそうと試みる
~$ sudo yum install ibus-mozc dbus-x11
  :

84パッケージがインストールされます。

.bash_profileに以下を追記

# Input Method
export LANG=ja_JP.UTF-8
export DefaultIMModule=ibus
export XMODIFIERS="@im=ibus"
export GTK_IM_MODULE=ibus
export QT_IM_MODULE=ibus
export IBUS_ENABLE_SYNC_MODE=1
ibus-daemon -d -x

bashを再度開いて、

~$ ibus-setup

ibus-setup を実行すると、ibus-daemonが動いていないので動かすか聞いてきます。動かすを選択したがエラーで動きません。