最近、milter managerというフリーソフトウェアを Mesonというビルドシステムに対応させる作業を行っている福田です。
その主な目的は、RubyやPython用のバインディングを自動で生成できるようにすることです。
ライブラリーがGObjectを利用している場合、 GObject Introspectionを使えば(ほぼ)自動でバインディングを生成できます。
MesonにはGObject Introspectionサポート(Meson Integration) が組み込まれているので、Mesonを使うと簡単にGObject Introspectionを利用できます。
今回は、Mesonを使ってGObject Introspection対応のビルドシステムを構築する方法を説明します。
Meson
GObject Introspection対応をするためには、次の2つビルドシステムのどちらかを使うと便利です。
- GNU Autotools (GNU Automake/GNU Autoconf/GNU Libtool)
- Meson
GObject Introspectionを開発しているGNOMEの関連プロジェクトがMesonに移行しているので、 今後のことを考えるとMesonを使うべきです。
GNU AutotoolsでGObject Introspection対応する方法については、次の記事で紹介しています。
今回の記事は、このMeson版の説明になります。
実際に対応したmilter manager(GitHub上のリポジトリー)を例に説明します。
プロジェクトトップのmeson.build
ファイル
Mesonでビルドを可能にするために、meson.build
ファイルを作成して設定します。
今回は、milter/core
やmilter/client
といった特定のサブディレクトリ毎にライブラリーをビルドしたいので、
プロジェクトのトップにmeson.build
ファイルを作成して共通の設定を行い、各サブディレクトリにもmeson.build
ファイルを作成します。
実際に作成したプロジェクトトップのmeson.build
ファイルは次になります。
以下で重要な部分を説明します。
# プロジェクトの定義:
# https://mesonbuild.com/Reference-manual_functions.html#project
# licenseの値の形式: https://spdx.org/licenses/
project('milter-manager',
'c',
license: 'LGPL-3.0-or-later',
version: '2.1.6')
# ライブラリーのバージョンなど、共通して用いる変数を定義する。
# ライブラリーのバージョンは、プロジェクトのバージョンとは別に管理する。(補足1)
api_version = '2.0'
so_version = 2
library_version = '@0@.0.0'.format(so_version)
# prefixオプションの値を取得する。(補足2)
prefix = get_option('prefix')
# ビルトインオブジェクト「meson」の関数を使い定義したプロジェクト名を参照。
# https://mesonbuild.com/Reference-manual_builtin_meson.html
# インストール先のパスについて定義している。(補足3)
project_include_sub_dir = meson.project_name()
milter_include_sub_dir = project_include_sub_dir / 'milter'
milter_include_dir = get_option('includedir') / milter_include_sub_dir
data_dir = prefix / get_option('datadir')
gir_dir = data_dir / 'gir-1.0'
# 用いるモジュールをインポート。
# 例: GNOME module: https://mesonbuild.com/Gnome-module.html
fs = import('fs')
gnome = import('gnome')
pkgconfig = import('pkgconfig')
# pcファイルの生成時に用いる。
# Mesonを使ってGObject Introspection対応しようとする別のライブラリが
# GIRファイルを探し出せるようにする。(補足4)
pkgconfig_variables = ['girdir=@0@'.format(gir_dir)]
(...)
package_platform = get_option('package_platform')
(...)
# meson-config.h.inの変数を置換してconfig.hを生成する。
# ソースコードで用いる定数などをビルド時に定義したいため。
config_h_conf = configuration_data()
(...)
config_h_conf.set_quoted('MILTER_MANAGER_PACKAGE_PLATFORM', package_platform)
config_h_conf.set_quoted('PACKAGE', meson.project_name())
config_h_conf.set_quoted('PREFIX', prefix)
config_h_conf.set_quoted('VERSION', meson.project_version())
config_h = configure_file(input: 'meson-config.h.in',
output: 'config.h',
configuration: config_h_conf)
# 後で依存関係に加えるため、config.hをdependencyにしておく。(補足5)
config = declare_dependency(compile_args: '-DHAVE_CONFIG_H',
include_directories: include_directories('.'),
sources: config_h)
# 指定したディレクトリに入り、そのディレクトリのmeson.buildを実行する。
subdir('milter/core')
(...)
補足1: ライブラリーのバージョン
# ライブラリーのバージョンなど、共通して用いる変数を定義する。
# ライブラリーのバージョンは、プロジェクトのバージョンとは別に管理する。(補足1)
api_version = '2.0'
so_version = 2
library_version = '@0@.0.0'.format(so_version)
ライブラリーのバージョンは、アプリケーションバイナリインターフェース(ABI)の互換性の観点で管理します。 アップデートによってプロジェクトのバージョンが上がっても、ABIの互換性が保たれているならばライブラリーのバージョンは変更しなくても良いです。
特に共有ライブラリーは、以下のようにシンボリックリンクとその本体で構成されます。
libmilter-core.so.2 -> libmilter-core.so.2.0.0
libmilter-core.so.2.0.0
シンボリックリンクの名前libmilter-core.so.2
には、メジャーバージョンの2
しか含まれていません。
つまり、メジャーバージョンを変更するということは、共有ライブラリーのシンボリックリンクを変更するということになります。
ABIの互換性を保てない更新があった場合のみメジャーバージョンを更新するべきです。
詳しくはライブラリーのバージョンとアプリケーションバイナリインターフェースをご覧下さい。
補足2: オプション
# prefixオプションの値を取得する。(補足2)
prefix = get_option('prefix')
get_option()でオプションの値を取得できます。
オプションには、デフォルトで定義されているビルトインオプションと、ユーザーが定義するオプションの2種類があります。 後者はmeson_options.txtというファイルを作成して定義します。
例えばprefix
はビルトインのオプションですが、package_platform
はmeson_options.txt
で定義したオプションです。
ビルトインのオプションはmeson
コマンドの引数として指定できます。
ユーザーが定義したオプションは、-D
オプションで指定します。
例えば次のように使います。
$ meson setup --prefix=/tmp/local -Dpackage_platform=debian ../milter-manager.build .
補足3: インストール先のパス
# インストール先のパスについて定義している。(補足3)
project_include_sub_dir = meson.project_name()
milter_include_sub_dir = project_include_sub_dir / 'milter'
milter_include_dir = get_option('includedir') / milter_include_sub_dir
data_dir = prefix / get_option('datadir')
gir_dir = data_dir / 'gir-1.0'
この後にインストールに用いる各関数は、デフォルトでprefix
オプションの値をインストール先として用います。
例えば、ヘッダーをインストールするinstall_headers()では
{prefix}/{includedir}
がデフォルトのインストール先になりますし、
ライブラリーをインストールするlibrary()では
{prefix}/{libdir}
がデフォルトのインストール先になります。
各関数でこのインストール先を変更できるので、あとで変更するためにここでパスを定義しています。
パスの連結は、/
演算子を使って行います。
この演算子はjoin_paths()と同じ機能を提供します。
補足4: GIRファイルとTypelibファイル
# pcファイルの生成時に用いる。
# Mesonを使ってGObject Introspection対応しようとする別のライブラリが
# GIRファイルを探し出せるようにする。(補足4)
pkgconfig_variables = ['girdir=@0@'.format(gir_dir)]
GObject Introspection対応のライブラリーをビルドするときは、GIRファイルとTypelibファイルを生成します。
GIRファイルを生成する際、依存している他のGIRファイルを参照する必要があります。
参照するのでどこにGIRファイルがあるか分からないといけません。
Mesonは依存しているライブラリーの.pcファイル
内にgirdir
変数が定義されていれば、その変数の値であるディレクトリーをGIRファイルの検索パスに追加してくれます。
このライブラリーのGIRファイルを、Mesonを使っている他のライブラリーから使いやすくするために、このライブラリーの.pcファイル
にもgirdir
変数を定義しています。
補足5: declare_dependency について
# 後で依存関係に加えるため、config.hをdependencyにしておく。(補足5)
config = declare_dependency(compile_args: '-DHAVE_CONFIG_H',
include_directories: include_directories('.'),
sources: config_h)
Mesonでライブラリーをビルドするにはlibrary()関数を使います。
library()
関数にマクロ定義やヘッダーの検索パスや依存関係を指定するには、library()
関数の各引数に個別に指定するか
Dependency objectを使って指定する方法があります。
Dependency objectを使うと複数のlibrary()
で指定内容を共有できるので、何度も指定する情報はDependency objectを使うとスッキリします。
外部でインストール済みのライブラリーであればdependency()を用い、 このプロジェクト内で作成する依存関係であればdeclare_dependency()を用います。
ここでは、compile_args
を指定することで、この依存を使ったライブラリーのビルドに指定したオプションを追加します。
milter managerでは元々GNU Autotoolsを用いており、その慣習で、HAVE_CONFIG_H
が定義済みの場合に限りconfig.h
を読み込むようにコーディングしている、
という事情があります。
そのためmilter managerでは、このconfig.h
に依存するライブラリーのビルドに、常にHAVE_CONFIG_H
オプションに付ける必要があります。
coreライブラリーのmeson.build
ファイル
milter managerのcoreライブラリー用のmeson.build
ファイルを次のように作成しました。
# ビルド対象に含めるソースファイル。
sources = files(
'milter-agent.c',
'milter-command-decoder.c',
(...)
)
# ビルド対象に含めるヘッダーファイル。
headers = files(
'milter-agent.h',
'milter-command-decoder.h',
(...)
)
# milter-version.h.inの変数を置換してmilter-version.hを生成し、headersに含める。
version_h_conf = configuration_data()
# str型のsplitメソッド:
# https://mesonbuild.com/Reference-manual_elementary_str.html#strsplit
version_components = meson.project_version().split('.')
version_h_conf.set('MILTER_MANAGER_VERSION', meson.project_version())
version_h_conf.set('MILTER_MANAGER_VERSION_MAJOR', version_components[0])
version_h_conf.set('MILTER_MANAGER_VERSION_MINOR', version_components[1])
version_h_conf.set('MILTER_MANAGER_VERSION_MICRO', version_components[2])
version_h = configure_file(input: 'milter-version.h.in',
output: 'milter-version.h',
configuration: version_h_conf)
headers += version_h
# enumのGObject用ファイルを生成する。(補足1)
enums = gnome.mkenums_simple('milter-enum-types',
body_prefix: '#include <config.h>',
identifier_prefix: 'Milter',
install_dir: milter_include_dir / 'core',
install_header: true,
sources: headers,
symbol_prefix: 'milter')
enums_h = enums[1]
# ヘッダーファイルをインストール。
install_headers(headers, subdir: milter_include_sub_dir / 'core')
install_headers(['../core.h'], subdir: milter_include_sub_dir)
headers += ['../core.h']
# 依存関係を定義する。
(...)
dependencies = [
config,
(...)
]
# ライブラリーを作成してインストール。
libmilter_core = library('milter-core',
c_args: '-DMILTER_LOG_DOMAIN="milter-core"',
sources: sources + headers + enums,
install: true,
dependencies: dependencies,
soversion: so_version,
version: library_version)
# 他のライブラリーがこのライブラリーを依存関係として使う。
# 生成したenumのGObject用ファイルのヘッダーを依存関係に含める。(補足2)
milter_core = declare_dependency(dependencies: dependencies,
link_with: libmilter_core,
sources: [enums_h])
# pcファイルを作成してインストール。
pkgconfig.generate(libmilter_core,
description: 'common milter features',
filebase: 'milter-core',
name: 'milter core library',
requires: ['gobject-2.0'],
subdirs: project_include_sub_dir,
variables: pkgconfig_variables)
# GIRファイルとTypelibファイルを生成してインストール。(補足3)
milter_core_gir = gnome.generate_gir(libmilter_core,
export_packages: 'milter-core',
extra_args: [
'--warn-all',
],
fatal_warnings: true,
header: 'milter/core.h',
identifier_prefix: 'Milter',
includes: [
'GObject-2.0',
],
install: true,
namespace: 'MilterCore',
nsversion: api_version,
sources: sources + headers + enums,
symbol_prefix: 'milter')
補足1: enumのGObject用ファイルの生成
# enumのGObject用ファイルを生成する。(補足1)
enums = gnome.mkenums_simple('milter-enum-types',
body_prefix: '#include <config.h>',
identifier_prefix: 'Milter',
install_dir: milter_include_dir / 'core',
install_header: true,
sources: headers,
symbol_prefix: 'milter')
enums_h = enums[1]
gnome.mkenums_simple() を使い、enumのGObject用のファイルを生成します。
sources
で指定したファイルで定義されているenum
をGEnum
にしてGObject対応します。
install_header: true
を指定することで、生成したヘッダーファイルをインストールしています。
このため、後のinstall_headers()
で指定しているインストールするヘッダーファイルのリストに、ここで生成したヘッダーファイルを含めていません。
ヘッダーファイルのインストール先をinstall_dir
で指定しています。
prefix
で指定したパスに対する相対パスを指定します。
一方でinstall_headers()
は{prefix}/{includedir}
に対する相対パスをsubdir
で指定しています。
identifier_prefix
には、コード上のenum
の定義において、名前空間としてprefixに付与している部分を指定します。
下の例では、MilterAgentError
のMilter
を指定します。
symbol_prefix
には、ヘッダーファイルで宣言される関数名や#define
されるenum名のprefixを指定します。
通常はidentifier_prefix
で指定した部分に対応する文字列を_
区切りの小文字で指定します。
実際に生成されるコードの例は、次のようになります。
元々のenum
定義
typedef enum
{
MILTER_AGENT_ERROR_IO_ERROR,
MILTER_AGENT_ERROR_DECODE_ERROR,
MILTER_AGENT_ERROR_NO_EVENT_LOOP_ERROR
} MilterAgentError;
生成されるmilter-enum-types.h
- ここのprefixが
identifier_prefix
やsymbol_prefix
によって決まります。
GType milter_agent_error_get_type (void);
#define MILTER_TYPE_AGENT_ERROR (milter_agent_error_get_type())
生成されるmilter-enum-types.c
GType
milter_agent_error_get_type (void)
{
static volatile gsize gtype_id = 0;
static const GEnumValue values[] = {
{ C_ENUM(MILTER_AGENT_ERROR_IO_ERROR), "MILTER_AGENT_ERROR_IO_ERROR", "io-error" },
{ C_ENUM(MILTER_AGENT_ERROR_DECODE_ERROR), "MILTER_AGENT_ERROR_DECODE_ERROR", "decode-error" },
{ C_ENUM(MILTER_AGENT_ERROR_NO_EVENT_LOOP_ERROR), "MILTER_AGENT_ERROR_NO_EVENT_LOOP_ERROR", "no-event-loop-error" },
{ 0, NULL, NULL }
};
if (g_once_init_enter (>ype_id)) {
GType new_type = g_enum_register_static (g_intern_static_string ("MilterAgentError"), values);
g_once_init_leave (>ype_id, new_type);
}
return (GType) gtype_id;
}
補足2: 生成したenumのGObject用ファイルのヘッダーを依存関係に含める
# 他のライブラリーがこのライブラリーを依存関係として使う。
# 生成したenumのGObject用ファイルのヘッダーを依存関係に含める。(補足2)
milter_core = declare_dependency(dependencies: dependencies,
link_with: libmilter_core,
sources: [enums_h])
他のライブラリーがこのライブラリーを依存関係として用いるため、dependencyを定義しておきます。
gnome.mkenums_simple()
等を使いヘッダーファイルを自動生成している場合は、sources
に自動生成したヘッダーファイルを含める必要があります。
これに依存するライブラリーのコンパイル時に、このライブラリーの各ヘッダーファイルが必要となります。 そのため、その前にヘッダーファイルの自動生成が完了している必要があります。
Cではコンパイルとリンクはフェーズが別れているため、link_with
でリンクの設定をするだけでは、
これに依存するライブラリーのコンパイル時にヘッダーファイルの生成が完了していない可能性があります。
sources
に自動生成するヘッダーファイルを指定することで、そのファイルのビルドが完了してから、これに依存するライブラリーをコンパイルできます。
補足3: GIRファイルとTypelibファイルの生成
# GIRファイルとTypelibファイルを生成してインストール。(補足2)
milter_core_gir = gnome.generate_gir(libmilter_core,
export_packages: 'milter-core',
extra_args: [
'--warn-all',
],
fatal_warnings: true,
header: 'milter/core.h',
identifier_prefix: 'Milter',
includes: [
'GObject-2.0',
],
install: true,
namespace: 'MilterCore',
nsversion: api_version,
sources: sources + headers + enums,
symbol_prefix: 'milter')
gnome.generate_gir() を使って、バインディングに用いるファイルであるGIRファイルとTypelibファイルを生成します。
fatal_warnings: true
を指定することで、警告をエラーとすることができます。
バインディングを生成するにはGObject Introspection Annotations
というアノテーションを適切に設定する必要があり、アノテーションに問題があればビルド時に警告が発生します。
この設定をすることでそのような警告をエラーとすることができます。
header: 'milter/core.h'
は、ただのドキュメント用の情報です。
namespace
で、生成するバインディングの名前空間を設定します。
identifier_prefix
とsymbol_prefix
は、gnome.mkenums_simple()
と同様に設定します。
例えば、前者をMilter
、後者をmilter
と設定することで、MilterAgent
がMilter
名前空間内のAgent
クラスとして認識されたり、
milter_xxx()
関数がxxx()
関数として認識されたりするようになります。
ビルド
例えば、以下のようにビルドできます。
$ meson setup --prefix=/tmp/local ../milter-manager.build .
$ ninja -C ../milter-manager.build
- 再起動時に自動でクリーンされる
/tmp
以下のパスをprefix
に利用することで、以前にインストールした成果物の影響を受けにくくしています。 - ビルド用のディレクトリとして、
../milter-manager.build
を利用しています。
インストールは次のように行うことができます。
$ meson install -C ../milter-manager.build
上インストールコマンドはビルドも兼ねるので、ビルドだけしたいという場合でなければ、次のようにビルド&インストールできます。
$ meson setup --prefix=/tmp/local ../milter-manager.build .
$ meson install -C ../milter-manager.build
まとめ
本記事では、Mesonを使ってGObject Introspection対応のビルドシステムを構築する方法について紹介しました。
GObject Introspectionを利用してバインディングを生成するには、 GObject Introspection Annotations というアノテーションを適切に管理する必要があります。
それについては今後の記事でご紹介します。
クリアコードではこのように業務の成果を公開することを重視しています。 業務の成果を公開する職場で働きたい人はクリアコードの採用情報をぜひご覧下さい。