デバイス上でパッケージをコンパイル(Termux on Android)

# 序

わたしはパッケージのビルドとかAndroid開発にはあまり詳しくなく、`apt`や`yum`などでインストールできるパッケージは素直にパッケージマネージャーでインストールする派です。しかし今回、Termux上でパッケージが見つからなかったアプリをインストールしたくなり、しょうがないのでやってみました(実はこれは[辞書サーバーによる辞書環境の構築](https://qiita.com/ayatakesi/items/0d73c6359350cb909666)の後日譚であります。そのため、`dictd`という世間ではあまり馴染みのないソフトがお題になってます。)

# `GNU autotools`パッケージのコンパイル手順 on Termux

[Termuxのマニュアル](https://wiki.termux.com/wiki/Package_Management#Compiling_packages_on_device)を参照しました。以下に`GNU autotools`アプリ(`./configure && make &&make install`でインストールできるやつ)をビルドする手順を要約します。

## 環境の準備
### 必要なアプリのインストール
ビルドに必要なツールをインストールします。インストールする前に、`pkg update`と`pkg upgrade`をしといたほうが良いです。

```shell-session
$ pkg install autoconf automake bison bzip2 \
              clang cmake coreutils diffutils \
              flex gawk git grep gzip libtool \
              make patch perl sed silversearcher-ag \
              tar termux-exec wget
Hit:1 https://termux.net stable InRelease
Reading package lists... Done
Building dependency tree
Reading state information... Done
All packages are up to date.
Reading package lists... Done
Building dependency tree
Reading state information... Done
.....
```

### 環境変数の設定

以下の2行を`.bashrc`に追加します。

```shell:.bashrc
export PREFIX=/data/data/com.termux/files/usr
export LD_PRELOAD=${PREFIX}/lib/libtermux-exec.so
```

## 依存関係の把握

マニュアルには[ArchLinux package database](https://www.archlinux.org/packages/)で依存するパッケージを特定して
それからインストールするようにと記述されてます。身近にlinux機のある方は`apt-get build-dep`の類いのコマンドで調べたほうが簡単でしょう。依存するパッケージがTermuxのパッケージリポジトリで提供されていればそれを`pkg install`でインストールするだけですが、提供されていなければそれもソースからビルドする必要があります。

## ソースコードなどの修正

ソース内で
`/bin`
`/etc`
`/sbin`
`/tmp`
`/usr`
`/var`
のような、ハードコーディングされがちな標準パスの前に、先程の`$PREFIX`の値を付け加えます。「`find`で`grep`かけて...」とか、「`emacs`の`Dired`から...」とか、好きな方法で修正対象のファイルを見つけて修正します。マニュアルでは`ag`(先の必要アプリインストールの中の`silversearcher-ag`のこと)が推奨されてます。

## ビルド

### `config.sub`と`config.guess`の置き換え
この2つを置き換えないとターゲットシステムを判断できないそうです。以下のコマンドで置き換えます。

```shell-session
find . -name 'config.sub' -exec chmod u+w '{}' \; -exec cp -f "${PREFIX}/share/libtool/build-aux/config.sub" '{}' \;
find . -name 'config.guess' -exec chmod u+w '{}' \; -exec cp -f "${PREFIX}/share/libtool/build-aux/config.guess" '{}' \;
```

### `configure`に必要なオプションを指定
Termuxでビルド、インストールできるように以下のオプションを指定します。

```shell-session
$ ./configure --prefix="${PREFIX}" CC=clang CXX=clang++
```

### `make`してインストール
特に問題がなければ普通に`make -j4 && make install`

# ソースtarball入手
`dictd`は`libmaa`に依存しており、どちらも同じ開発元が提供しています。[こちら](https://sourceforge.net/projects/dict/files/)からソースを入手しました。最新版は`dictd-1.12.1.tar.gz`と`libmaa-1.3.2.tar.gz`です。

# libmaaのビルドとインストール

```shell-session
$ tar xvfz libmaa-1.3.2.tar.gz #解凍
libmaa-1.3.2
libmaa-1.3.2/doc
libmaa-1.3.2/doc/Makefile.in
libmaa-1.3.2/doc/extract.pl
libmaa-1.3.2/doc/lgrindefs
.....

$ cd libmaa-1.3.2 #ソースディレクトリへcd
```

ソース内でハードコードされているディレクトリがないか調べる。libmaaは修正必要なしでした。

```shell-session
$ ag --cc /bin 
$ ag --cc /etc 
$ ag --cc /sbin
$ ag --cc /tmp
$ ag --cc /usr
$ ag --cc /var
```

`configure`します。マニュアルに記載のない`LIBS=-llog`は、これを指定しなくても`libmaa`はビルドとインストールはできたんですけど、`dictd`の`configure`時に「`libmaa`が未定義の`__android_log_print`などを参照している」→「`libmaa`がインストールされてない(誤解)」とかでコケるので、調べた結果これを追加しました。ちなみにこの`liblog.so`はTermuxではなくAndroidシステムの`/system/lib/liblog.so`らしいです。

```shell-session
$ ./configure --prefix="${PREFIX}" CC=clang CXX=clang++ LIBS=-llog                      Configuring for libmaa 1.3.2
.
checking build system type... aarch64-unknown-linux-gnu
checking host system type... aarch64-unknown-linux-gnu
checking for gawk... gawk
checking for gcc... clang
checking whether the C compiler works... yes
.....
```

終わったら`make`してインストール

```shell-session
$ make -j4 && make install
libtool --tag=CC --mode=compile clang -o obstack.o -c -DHAVE_CONFIG_H  -g -O2 -DMAA_MAJOR=1 -DMAA_MINOR=3 -DMAA_TEENY=2 -I. -I. obstack.c
libtool --tag=CC --mode=compile clang -o xmalloc.o -c -DHAVE_CONFIG_H  -g -O2 -DMAA_MAJOR=1 -DMAA_MINOR=3 -DMAA_TEENY=2 -I. -I. xmalloc.c
libtool --tag=CC --mode=compile clang -o hash.o -c -DHAVE_CONFIG_H  -g -O2 -DMAA_MAJOR=1 -DMAA_MINOR=3 -DMAA_TEENY=2 -I. -I. hash.c
libtool --tag=CC --mode=compile clang -o set.o -c -DHAVE_CONFIG_H  -g -O2 -DMAA_MAJOR=1 -DMAA_MINOR=3 -DMAA_TEENY=2 -I. -I. set.c
libtool: compile:  clang -c -DHAVE_CONFIG_H -g -O2 -DMAA_MAJOR=1 -DMAA_MINOR=3 -DMAA_TEENY=2 -I. -I. hash.c  -fPIC -DPIC -o .libs/hash.o
.....
```

# dictdのビルドとインストール

```shell-session
$ tar xvfz dictd-1.12.1.tar.gz #解凍
dictd-1.12.1
dictd-1.12.1/doc
dictd-1.12.1/doc/Makefile.in
dictd-1.12.1/doc/dicf.ms
dictd-1.12.1/doc/rfc.ms
.....
$ cd dictd-1.12.1 #ソースディレクトリへcd
```

ソース内でハードコードされているディレクトリがないか調べる。`parse.c`と`dictd.c`が要修正です。

```shell-session
$ ag --cc /bin
$ ag --cc /etc
$ ag --cc /sbin
$ ag --cc /tmp
$ ag --cc /usr
parse.c
94:                                 "/usr/lib/cpp",
95:                                 "/usr/ccs/lib/cpp", /*Solaris */
96:                                 "/usr/lang/cpp",
$ ag --cc /var
dictd.c
78:const char *pidFile     = "/var/run/dictd.pid";
```

`parse.c`を修正した結果です。

```diff:parse.c.patch
--- parse.c~	2008-12-08 01:50:05.000000000 +0900
+++ parse.c	2018-01-23 05:51:09.760006028 +0900
@@ -90,10 +90,10 @@ void prs_file( const char *filename )
    char              *buffer;
    const char        **pt;
    static const char *cpp = NULL;
-   static const char *cpps[] = { "/lib/cpp",
-                                 "/usr/lib/cpp",
-                                 "/usr/ccs/lib/cpp",	/* Solaris */
-                                 "/usr/lang/cpp",
+   static const char *cpps[] = { "/data/data/com.termux/files/usr/lib/cpp",
+                                 "/data/data/com.termux/files/usr/usr/lib/cpp",
+                                 "/data/data/com.termux/files/usr/usr/ccs/lib/cpp",	/* Solaris */
+                                 "/data/data/com.termux/files/usr/usr/lang/cpp",
                                  0 };
    static const char *extra_options = "";
    FILE              *tmp;

```

そしてこちらが`dictd.c`の修正後

```diff:dictd.c.patch                                       
--- dictd.c~	2011-01-10 01:53:27.000000000 +0900
+++ dictd.c	2018-01-23 05:51:01.530006030 +0900
@@ -75,7 +75,7 @@ int                logOptions   = 0;
 const char         *logFile     = NULL;
 int logFile_set; /* 1 if set by command line option */
 
-const char *pidFile     = "/var/run/dictd.pid";
+const char *pidFile     = "/data/data/com.termux/files/usr/var/run/dictd.pid";
 int pidFile_set; /* 1 if set by command line option */
 
 const char         *daemon_service     = DICT_DEFAULT_SERVICE;

```

ここまでは、マニュアルに記載されていたTermuxでのビルド方法を踏襲しました。しかし、これだけではやはり上手くビルド、またはビルドできても正常に動作させることができなかったので、その対応を書きます。

## `Makefile.in`の修正
`libtool`というのは`mode=compile`のときはソースディレクトリに`.libs`というディレクトリを作成してそこにオブジェクトファイル`*.o`をコンパイルし、ソースディレクトリのルートには、(多分)それらのオブジェクトファイルを参照する`*.lo`というファイルを作成するようです。そして`mode=link`のときはルートディレクトリの`*.lo`を参照する感じなのでしょうか。自分も他の`Makefile`とかルールなどと比較して推測してるんで、あやふやで申し訳ないんですが。以下が修正後です。

```diff:Makefile.in.patch
diff -u -r ../dictd-1.12.1.orig/Makefile.in ./Makefile.in
--- ../dictd-1.12.1.orig/Makefile.in	2011-03-07 02:52:54.000000000 +0900
+++ ./Makefile.in	2018-02-08 18:50:15.790039388 +0900
@@ -123,7 +123,7 @@
 
 %: %.o
 	$(LIBTOOL) --tag=CC --mode=link $(CC) -o $@ -static \
-		$^ $(OBJS) $(LDFLAGS) -lz ${LIBS}
+		$(^:.o=.lo) $(OBJS) $(LDFLAGS) -lz ${LIBS}
 
 include $(srcdir)/deps
```

しかし同じコマンドで`mode=compile`のときは勝手に`.libs`なるディレクトリを作成して、`mode=link`のときはルートの`*.lo`を参照するのはいいとしても、ユーザーが毎回いちいち`:.o=.lo`とか記述しなきゃいけないのは何か釈然としません。こーゆーものなのでしょうか？(だとしたら`dictd`の`Makefile.in`のバグということになりますが、他のプラットフォームで検証してないので、よく解りません)。

## `net.c`の修正

ここまでの対応で無事にビルド、インストールできたのですが、実行すると`Can't get "tcp" protocol entry`というエラーで起動してくれません。[ググった結果](https://stackoverflow.com/questions/10331699/getprotobyname-error-iptables)、Androidでは`/etc/protocols`の内容をハードコーディングしなくてはならないようです。以下はその対応です。

```diff:net.c.patch
--- net.c~	2010-08-22 02:55:40.000000000 +0900
+++ net.c	2018-01-23 07:20:49.070005208 +0900
@@ -65,7 +65,9 @@ int net_connect_tcp( const char *host, c
 {
    struct hostent     *hostEntry;
    struct servent     *serviceEntry;
+   /*
    struct protoent    *protocolEntry;
+   */
    struct sockaddr_in ssin;
    int                s;
    int                hosts = 0;
@@ -79,8 +81,10 @@ int net_connect_tcp( const char *host, c
    } else if (!(ssin.sin_port = htons(atoi(service))))
       return NET_NOSERVICE;
 
+   /*
    if (!(protocolEntry = getprotobyname("tcp")))
       return NET_NOPROTOCOL;
+   */
    
    if ((hostEntry = gethostbyname(host))) {
       ++hosts;
@@ -92,7 +96,7 @@ int net_connect_tcp( const char *host, c
 	 memcpy( &ssin.sin_addr.s_addr, *current, hostEntry->h_length );
 	 PRINTF(DBG_VERBOSE,
 		("Trying %s (%s)\n",host,inet_ntoa(ssin.sin_addr)));
-	 if ((s = socket(PF_INET, SOCK_STREAM, protocolEntry->p_proto)) < 0)
+	 if ((s = socket(PF_INET, SOCK_STREAM, 6)) < 0)
 	    err_fatal_errno( __func__, "Can't open socket on port %d\n",
 			     ntohs(ssin.sin_port) );      
 	 if (connect(s, (struct sockaddr *)&ssin, sizeof(ssin)) >= 0)
@@ -100,7 +104,7 @@ int net_connect_tcp( const char *host, c
 	 close(s);
       }
    } else {
-      if ((s = socket(PF_INET, SOCK_STREAM, protocolEntry->p_proto)) < 0)
+      if ((s = socket(PF_INET, SOCK_STREAM, 6)) < 0)
 	 err_fatal_errno( __func__, "Can't open socket on port %d\n",
 			  ntohs(ssin.sin_port) );
       if (connect(s, (struct sockaddr *)&ssin, sizeof(ssin)) >= 0)
@@ -117,7 +121,10 @@ int net_open_tcp (
    int queueLength)
 {
    struct servent     *serviceEntry;
+   /*
    struct protoent    *protocolEntry;
+   */
+   
    struct sockaddr_in ssin;
    int                s;
    const int          one = 1;
@@ -131,10 +138,12 @@ int net_open_tcp (
    } else if (!(ssin.sin_port = htons(atoi(service))))
       err_fatal( __func__, "Can't get \"%s\" service entry\n", service );
 
+   /*
    if (!(protocolEntry = getprotobyname("tcp")))
       err_fatal( __func__, "Can't get \"tcp\" protocol entry\n" );
+   */
    
-   if ((s = socket(PF_INET, SOCK_STREAM, protocolEntry->p_proto)) < 0)
+   if ((s = socket(PF_INET, SOCK_STREAM, 6)) < 0)
       err_fatal_errno( __func__, "Can't open socket on port %d\n",
 		       ntohs(ssin.sin_port) );
```

これでソースへの対応は終了です。マニュアルどおりにインストールします。

```shell-session
$ find . -name 'config.sub' -exec chmod u+w '{}' \; -exec cp -f "${PREFIX}/share/libtoo\
l/build-aux/config.sub" '{}' \;
$ find . -name 'config.guess' -exec chmod u+w '{}' \; -exec cp -f "${PREFIX}/share/libt\
ool/build-aux/config.guess" '{}' \;
$ ./configure --prefix="${PREFIX}" CC=clang CXX=clang++
Configuring for dict
.
checking build system type... aarch64-unknown-linux-gnu
checking host system type... aarch64-unknown-linux-gnu
checking for gcc... clang
checking whether the C compiler works... yes
.....

$ make -j4
libtool --tag=CC --mode=compile clang -c -DHAVE_CONFIG_H -g -O2 -DUSE_PLUGIN -DDICT_PLUGIN_PATH=\"/data/data/com.termux/files/usr/libexec/\" -DDICT_DICTIONARY_PATH=\"/data/data/com.termux/files/usr/share/\" -DDICT_VERSION=\"1.12.1\" -DDICT_CONFIG_PATH=\"/data/data/com.termux/files/usr/etc/\" -I. -I. net.c -o net.o
flex  -oclientscan.c clientscan.l
bison -y -dv clientparse.y; \
    cmp -s y.tab.h clientparse.h || mv y.tab.h clientparse.h; \
    cmp -s y.tab.c clientparse.c || mv y.tab.c clientparse.c; \
    rm -f y.tab.h y.tab.c
bison -y -dv clientparse.y; \
    cmp -s y.tab.h clientparse.h || mv y.tab.h clientparse.h; \
    cmp -s y.tab.c clientparse.c || mv y.tab.c clientparse.c; \
    rm -f y.tab.h y.tab.c
libtool --tag=CC --mode=compile clang -c -DHAVE_CONFIG_H -g -O2 -DUSE_PLUGIN -DDICT_PLUGIN_PATH=\"/data/data/com.termux/files/usr/libexec/\" -DDICT_DICTIONARY_PATH=\"/data/data/com.termux/files/usr/share/\" -DDICT_VERSION=\"1.12.1\" -DDICT_CONFIG_PATH=\"/data/data/com.termux/files/usr/etc/\" -I. -I. md5.c -o md5.o
.....

$ make install
libtool --tag=CC --mode=compile clang -c -DHAVE_CONFIG_H -g -O2 -DUSE_PLUGIN -DDICT_PLUGIN_PATH=\"/data/data/com.termux/files/usr/libexec/\" -DDICT_DICTIONARY_PATH=\"/data/data/com.termux/files/usr/share/\" -DDICT_VERSION=\"1.12.1\" -DDICT_CONFIG_PATH=\"/data/data/com.termux/files/usr/etc/\" -I. -I. net.c -o net.o
libtool: compile:  clang -c -DHAVE_CONFIG_H -g -O2 -DUSE_PLUGIN -DDICT_PLUGIN_PATH=\"/data/data/com.termux/files/usr/libexec/\" -DDICT_DICTIONARY_PATH=\"/data/data/com.termux/files/usr/share/\" -DDICT_VERSION=\"1.12.1\" -DDICT_CONFIG_PATH=\"/data/data/com.termux/files/usr/etc/\" -I. -I. net.c  -fPIC -DPIC -o .libs/net.o
libtool --tag=CC --mode=compile clang -c -DHAVE_CONFIG_H -g -O2 -DUSE_PLUGIN -DDICT_PLUGIN_PATH=\"/data/data/com.termux/files/usr/libexec/\" -DDICT_DICTIONARY_PATH=\"/data/data/com.termux/files/usr/share/\" -DDICT_VERSION=\"1.12.1\" -DDICT_CONFIG_PATH=\"/data/data/com.termux/files/usr/etc/\" -I. -I. clientparse.c -o clientparse.o
libtool: compile:  clang -c -DHAVE_CONFIG_H -g -O2 -DUSE_PLUGIN -DDICT_PLUGIN_PATH=\"/data/data/com.termux/files/usr/libexec/\" -DDICT_DICTIONARY_PATH=\"/data/data/com.termux/files/usr/share/\" -DDICT_VERSION=\"1.12.1\" -DDICT_CONFIG_PATH=\"/data/data/com.termux/files/usr/etc/\" -I. -I. clientparse.c  -fPIC -DPIC -o .libs/clientparse.o
.....
```

# 辞書や設定ファイルなどの準備
わたしの目的はGNURootからTermuxへの運用移行なので、そこまで書いておきます。一応ここまででTermuxでソースからビルドする、というのは終わりなので`dictd`とか興味ない人は読む必要ないです。

## 辞書ファイルの準備

GNURootで運用してた辞書ファイルを流用します。辞書変換ツール`dictfmt`は`dictd`と一緒にインストールされているので、多分テキスト形式の辞書ファイルから`dictd`形式への変換はできるでしょう。後日余力があったら検証します。

```shell-session
$ ls -l $PREFIX/share/dict/
total 87924
-rwxr--r-- 1 u0_a132 u0_a132 62649479 Jan 22 23:54 eijiro.dict
-rwxr--r-- 1 u0_a132 u0_a132 27278900 Jan 22 23:54 eijiro.index
```

## `pid`ファイル用のディレクトリ作成

`dictd.c`の修正で`/var/run/dictd.pid`を`/data/data/com.termux/files/usr/var/run/dictd.pid`に修正しましたが、`$PREFIX/var`に`run`というディレクトリがないので作成します。

```shell-session
$ mkdir $PREFIX/var/run
```

## 設定ファイルの準備

設定ファイルを準備します。コマンドにはフルパスを指定するのでどこにどんな名前で作ってもいいんですが、GNURootと合わせるため、`$PREFIX/etc`に`dictd`というディレクトリを作って、設定ファイル名も`dictd.conf`にしました。

```shell:$PREFIX/etc/dictd/dictd.conf
global {
listen_to 127.0.0.1
# bind to local interfacea only
}

# Access section here:

access {
allow localhost
allow 127.0.0.1
# this allows access only from local host
allow inetd
# this allows access from inetd server
}

# Database section here:

#include /var/lib/dictd/db.list

# User section here:
database eijiro {
data  "/data/data/com.termux/files/usr/share/dict/eijiro.dict"
index "/data/data/com.termux/files/usr/share/dict/eijiro.index" }
```

## 起動

起動します。うまく起動しないときログが役に立ったので情報を多く出力するよう指定してます。あとAndroidのsyslogがどこに吐かれるか解らなかったので、出力ファイル名も指定してます。

```shell-session
$ $PREFIX/sbin/dictd -c $PREFIX/etc/dictd/dictd.conf -v -d verbose -L $PREFIX/var/log/dictd.log
$ cat $PREFIX/var/log/dictd.log
:I: 9079 starting dictd 1.12.1/rf on Linux 3.10.94 Tue Jan 23 01:17:18 2018
:I: eijiro            1016642     27278900     62649479     62649479

$ dict -h localhost 'heavy snow'
1 definition found

From eiwa [eijiro]:

  heavy snow
  豪雪、ひどい雪、大雪、まとまった雪
```

# 結び
マニュアルを見たときは「こりゃ簡単！」と思ったんですが、結局朝までかかりました。`GNU autotools`難しいですね。おわり