Rustで動的ライブラリに依存しないLinuxコマンドを作成する方法

  • 46
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

最近、Rustでコマンドラインツールを書く機会があったのだが、完成物をローカル環境でビルドし、実際の環境で叩いてみたところlibc.solibssl.soのバージョンが異なると言われてしまい、実行することができなかった。

もちろん実行先の環境と同じ環境を用意してビルドしたり、もし実行先に不足するライブラリが存在するなら事前にインストールしておけば良いだけではある。

ただ、そのツール自体はいろいろな環境で手軽に使いたいと考えていたので、適当な環境でビルドしたコマンドバイナリをコピーするだけで利用可能な状態にするのが理想ではあった。

そのためいろいろと調べてみたところRustBookのAdvancedLinkingの節で紹介されている方法を使えば「OSはLinux (and マシンアーキテクチャが同一)」という制約はつくが、動的ライブラリへの依存がなく、かなりポータブルなバイナリが作成できそうだということが分かった。
(muslという可搬かつ静的リンク可能なlibc実装を使用する、というのが基本的なアイディアな模様。詳細はRustBookを参照)

実際に試したみたところ、いくつか躓くところはあったが、無事表題のようなコマンドを作成することができたので、ここにその手順を残しておくことにする。
(なぜか同日にあった別のアドベントカレンダーの記事を書くのにだいぶ気力が削られてしまったので、こちらはかなり手抜きです...)

サンプルプロジェクト

GETのみをサポートするHTTPクライアントコマンドを題材とする。
コマンド名はhttpc

まずはプロジェクトの作成からデフォルトコマンドのビルドまで。

# プロジェクト作成
$ cargo new httpc --bin
$ cd httpc

# 依存ライブラリに`hyper`を追加 (HTTPライブラリ)
$ vim Cargo.toml
[dependencies]  # この二行を追加
hyper="0.7.0"

# 一回ビルド (hyperの依存ライブラリがいろいろ付いてくる)
$ cargo build
    Updating registry `https://github.com/rust-lang/crates.io-index`
 Downloading cookie v0.2.2
 Downloading gcc v0.3.20
 Downloading openssl-sys v0.7.1
 Downloading log v0.3.4
 Downloading num_cpus v0.2.10
 Downloading openssl-sys-extras v0.7.1
 Downloading openssl v0.7.1
   Compiling typeable v0.1.2
   Compiling language-tags v0.2.0
   Compiling httparse v1.0.0
   Compiling lazy_static v0.1.15
   Compiling unicase v1.0.1
   Compiling matches v0.1.2
   Compiling traitobject v0.0.1
   Compiling bitflags v0.3.3
   Compiling libc v0.2.2
   Compiling winapi v0.2.5
   Compiling rustc-serialize v0.3.16
   Compiling pkg-config v0.3.6
   Compiling winapi-build v0.1.1
   Compiling log v0.3.4
   Compiling kernel32-sys v0.2.1
   Compiling advapi32-sys v0.1.2
   Compiling hpack v0.2.0
   Compiling openssl-sys v0.7.1
   Compiling time v0.1.34
   Compiling num_cpus v0.2.10
   Compiling rand v0.3.12
   Compiling gcc v0.3.20
   Compiling solicit v0.4.4
   Compiling openssl-sys-extras v0.7.1
   Compiling openssl v0.7.1
   Compiling uuid v0.1.18
   Compiling num v0.1.28
   Compiling url v0.5.0
   Compiling cookie v0.2.2
   Compiling serde v0.6.1
   Compiling mime v0.1.1
   Compiling hyper v0.7.0
   Compiling httpc v0.1.0 (file:///path/to/httpc)

$ target/debug/httpc
Hello, world!

次はmain.rsの中身。
引数で指定されたURLに対してGETリクエストを発行するだけのコマンド。

main.rs
extern crate hyper;

use std::env;
use std::process;
use std::io::Read;
use hyper::client::Client as HttpClient;

fn main() {
    let args: Vec<_> = env::args().collect();

    if args.len() != 2 {
        println!("Usage: {} HTTP_URL", args[0]);
        process::exit(1);
    }

    let url = &args[1];
    let client = HttpClient::new();
    match client.get(url).send() {
        Err(reason) => {
            println!("[ERROR] {}", reason);
            process::exit(1);
        },
        Ok(mut res) => {
            let mut buf = String::new();
            res.read_to_string(&mut buf).unwrap();
            println!("{}", buf);
        },
    }
    }

実行および依存ライブラリの確認

再度ビルドして実行。

$ cargo build
$ target/debug/httpc http://qiita.com/ | head
<!DOCTYPE html><html xmlns:og="http://ogp.me/ns#"><head><meta charset="UTF-8" /><title>Qiita - プログラマの技術情報共有サービス</title><meta content="width=device-width,initial-scale=1" name="viewport" /><meta content="Qiitaは、プログラマのための技術情報共有サービスです。 プログラミングに関するTips、ノウハウ、メモを簡単に記録 &amp;amp; 公開することができます。" name="description" /><meta content="summary" name="twitter:card" /><meta content="@Qiita" name="twitter:site" /><meta content="Qiita - プログラマの技術情報共有サービス" property="og:title" /><meta content="website" property="og:type" /><meta content="https://qiita.com/" property="og:url" /><meta content="https://cdn.qiita.com/assets/qiita-fb-a1b4a208593dbf5743d68ed2a86e63b5.png" property="og:image" /><meta content="Qiitaは、プログラマのための技術情報共有サービスです。 プログラミングに関するTips、ノウハウ、メモを簡単に記録 &amp;amp; 公開することができます。" property="og:description" /><meta content="Qiita" property="og:site_name" /><meta content="564524038" property="fb:admins" /><link href="/icons/favicons/public/production.ico?v=4" rel="shortcut icon" type="image/x-icon" /><link href="/opensearch.xml" rel="search" title="Qiita" type="application/opensearchdescription+xml" /><link href="/icons/favicons/public/apple-touch-icon.png" rel="apple-touch-icon" /><link href="//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css" rel="stylesheet" /><link rel="stylesheet" media="all" href="https://cdn.qiita.com/assets/public_vender-5494abb01d4ea7c9dcfb03b4890d2a1a.css" /><link rel="stylesheet" media="all" href="https://cdn.qiita.com/assets/application-31a8f5edb9d193f8021ac3017b5ba28c.css" /><meta name="csrf-param" content="authenticity_token" />
<meta name="csrf-token" content="TplYQVTjjZcD+my5oNyDFUWavWZbDJkxo1mNvRfQG2+Y8WekiVlQfhslBB96EMJ0BpAjS8Rec70qu4OO9qnJlA==" /></head><body class="without-js" id=""><noscript><iframe height="0" src="//www.googletagmanager.com/ns.html?id=GTM-TBQWPN" style="display:none;visibility:hidden" width="0"></iframe></noscript><script>
  document.body.className = document.body.className.replace('without-js', '') + ' with-js';

  // Application Namespace
  var Qiita = {
    startTime: Date.now(),
    controllerPath: 'public/home',
    controllerAction: 'public/home#index',
    controller: 'home',

lddコマンドで依存する動的ライブラリを確認。

$ ldd target/debug/httpc
        linux-vdso.so.1 =>  (0x00007ffe8baab000)
        libssl.so.10 => /lib64/libssl.so.10 (0x00007fb3c1232000)
        libcrypto.so.10 => /lib64/libcrypto.so.10 (0x00007fb3c0e4a000)
        libdl.so.2 => /lib64/libdl.so.2 (0x00007fb3c0c46000)
        libpthread.so.0 => /lib64/libpthread.so.0 (0x00007fb3c0a2a000)
        libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x00007fb3c0813000)
        librt.so.1 => /lib64/librt.so.1 (0x00007fb3c060b000)
        libc.so.6 => /lib64/libc.so.6 (0x00007fb3c024a000)
        /lib64/ld-linux-x86-64.so.2 (0x000056424997a000)
        libm.so.6 => /lib64/libm.so.6 (0x00007fb3bff47000)
        libgssapi_krb5.so.2 => /lib64/libgssapi_krb5.so.2 (0x00007fb3bfcfb000)
        libkrb5.so.3 => /lib64/libkrb5.so.3 (0x00007fb3bfa18000)
        libcom_err.so.2 => /lib64/libcom_err.so.2 (0x00007fb3bf813000)
        libk5crypto.so.3 => /lib64/libk5crypto.so.3 (0x00007fb3bf5e1000)
        libz.so.1 => /lib64/libz.so.1 (0x00007fb3bf3cb000)
        libkrb5support.so.0 => /lib64/libkrb5support.so.0 (0x00007fb3bf1bb000)
        libkeyutils.so.1 => /lib64/libkeyutils.so.1 (0x00007fb3befb7000)
        libresolv.so.2 => /lib64/libresolv.so.2 (0x00007fb3bed9d000)
        libselinux.so.1 => /lib64/libselinux.so.1 (0x00007fb3beb77000)
        libpcre.so.1 => /lib64/libpcre.so.1 (0x00007fb3be916000)
        liblzma.so.5 => /lib64/liblzma.so.5 (0x00007fb3be6f0000)

これらの動的な依存を完全に無くすのが今回の目的。

手順

以降の手順で、動的ライブラリへの依存がないhttpcコマンドが作成可能。
なおこの手順は最小構成でインストールしたCentOS7向けのものとなっている。

musl版rustのビルド

まずはmusl版のrustのビルドを行う。
基本的にはRustBookに書かれている内容と同様。

# ビルドに必要なパッケージ群をインストール
$ sudo yum -y install git gcc gcc-c++ tar cmake make

# muslをビルド
$ PREFIX=$PWD/musldist
$ curl http://www.musl-libc.org/releases/musl-latest.tar.gz | tar xzf -
$ cd musl-* 
$ ./configure --disable-shared --prefix=$PREFIX
$ make
$ make instal
$ cd ../

# libunwind.aだけ別にビルドする必要があるらしい
$ curl http://llvm.org/releases/3.7.0/llvm-3.7.0.src.tar.xz | tar xJf -l
$ cd llvm-3.7.0.src/projects/
$ curl http://llvm.org/releases/3.7.0/libcxxabi-3.7.0.src.tar.xz | tar xJf -
$ mv libcxxabi-3.7.0.src libcxxabi
$ curl http://llvm.org/releases/3.7.0/libunwind-3.7.0.src.tar.xz | tar xJf -
$ mv libunwind-3.7.0.src libunwind
$ mkdir libunwind/build
$ cd  libunwind/build
$ cmake -DLLVM_PATH=../../.. -DLIBUNWIND_ENABLE_SHARED=0 ..
$ make
$ cp lib/libunwind.a $PREFIX/lib/
$ cd ../../../../

# musl版rustをビルド
$ git clone https://github.com/rust-lang/rust.git muslrust
$ cd muslrust
$ ./configure --target=x86_64-unknown-linux-musl --musl-root=$PREFIX --prefix=$PREFIX
$ make
$ make install
$ export PATH=$PREFIX/bin:$PATH
$ export LD_LIBRARY_PATH=$PREFIX/lib:$LD_LIBRARY_PATH
$ cd ..

# cargoが未インストールの場合は、ついでに入れてしまう (既にあるならこの手順は不要)
$ curl https://static.rust-lang.org/cargo-dist/cargo-nightly-x86_64-unknown-linux-gnu.tar.gz | tar xzf -
$ cd cargo*
$ sudo ./install.sh
$ cd ..

ここまででmusl版rustのビルドが完了。

Pure-Rustで実装されたライブラリに関しては、デフォルトで静的リンクされるようなので、プロジェクトによっては、ここで(動的リンクを無くすための)全ての準備が整うことになる。

ただし、今回のサンプルプロジェクトではOpenSSLといった非Rustなライブラリに依存しているので、それに対応する静的ライブラリを、別途muslを使ってビルドしてあげる必要がある。

OpenSSLをmusl-gccでビルド

OpenSSLをmusl-gccを使ってビルドする。

基本的にはconfigure時に、使用するコンパイラとしてmusl版のGCCを指定すれば良い。

ただし、それだけだと何故か"undefined reference to 'rc4_md5_enc'"というエラーが出てビルドに失敗してしまった。
ソースコードを追ってみたところ、これはアセンブリで実装された関数のようなので、今回はno-asmオプションを指定してアセンブリコードが使われないようにすることで問題を回避した。
(もっと良い方法があるかもしれない)

$ curl --tlsv1 https://www.openssl.org/source/openssl-1.0.2e.tar.gz | tar xzf -
$ cd openssl*
$ ./Configure no-asm --prefix=$PREFIX os/compiler:$PREFIX/bin/musl-gcc
$ make
$ make install
$ cd ..

httpcをmusl版rustでビルド

これで準備は整ったので、最後にサンプルプロジェクトを静的リンクでビルドする。

なおhyperが依存しているRustのSSLライブラリであるrust-opensslは、デフォルトではlibssl等のライブラリを動的リンクしようとするみたいなので、以下のように環境変数で静的リンクを行うように指示する必要がある。

$ export OPENSSL_STATIC=1
$ export OPENSSL_LIB_DIR=$PREFIX/lib  # ライブラリのパスが通常とは異なる場合は、それも環境変数で指定する

ようやくhttpc自体のビルド。
--targetオプションをcargoに渡して、musl版のビルドを行うように指示する。

$ cd httpc/
$ cargo build --target=x86_64-unknown-linux-musl

# tagetを指定した場合、ビルド物は"target/x86_64-unknown-linux-musl/"以下に生成される
$ ls target/x86_64-unknown-linux-musl/debug/httpc
target/x86_64-unknown-linux-musl/debug/httpc

実行および依存ライブラリの確認

lddで確認すると、動的リンクするライブラリがなくなっていることが分かる。

$ ldd target/x86_64-unknown-linux-musl/debug/httpc
        not a dynamic executable

動作も特に問題なし。

$ target/x86_64-unknown-linux-musl/debug/httpc http://qiita.com/ | head -1
<!DOCTYPE html><html xmlns:og="http://ogp.me/ns#"><head><meta charset="UTF-8" /><title>Qiita - プログラマの技術情報共有サービス</title><meta content="width=device-width,initial-scale=1" name="viewport" /><meta content="Qiitaは、プログラマのための技術情報共有サービスです。 プログラミングに関するTips、ノウハウ、メモを簡単に記録 &amp;amp; 公開することができます。" name="description" /><meta content="summary" name="twitter:card" /><meta content="@Qiita" name="twitter:site" /><meta content="Qiita - プログラマの技術情報共有サービス" property="og:title" /><meta content="website" property="og:type" /><meta content="https://qiita.com/" property="og:url" /><meta content="https://cdn.qiita.com/assets/qiita-fb-a1b4a208593dbf5743d68ed2a86e63b5.png" property="og:image" /><meta content="Qiitaは、プログラマのための技術情報共有サービスです。 プログラミングに関するTips、ノウハウ、メモを簡単に記録 &amp;amp; 公開することができます。" property="og:description" /><meta content="Qiita" property="og:site_name" /><meta content="564524038" property="fb:admins" /><link href="/icons/favicons/public/production.ico?v=4" rel="shortcut icon" type="image/x-icon" /><link href="/opensearch.xml" rel="search" title="Qiita" type="application/opensearchdescription+xml" /><link href="/icons/favicons/public/apple-touch-icon.png" rel="apple-touch-icon" /><link href="//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css" rel="stylesheet" /><link rel="stylesheet" media="all" href="https://cdn.qiita.com/assets/public_vender-5494abb01d4ea7c9dcfb03b4890d2a1a.css" /><link rel="stylesheet" media="all" href="https://cdn.qiita.com/assets/application-31a8f5edb9d193f8021ac3017b5ba28c.css" /><meta name="csrf-param" content="authenticity_token" />

感想

実際に調べつつ試していた時には、OpenSSL周りエラーでやたらと嵌ってしまったが、分かってみれば意外と簡単に(Linux上で)ポータブルなコマンドがビルドすることができた。

やっぱりバイナリコピーだけで実行可能なコマンドが作れるのは便利。

追記: 2016-04-09

参考: 本記事に記載のビルド一式を行うためのDockerfile

追記: 2016-05-30

Taking Rust everywhere with rustupによるとrustupを使えば、以下のように数コマンドで、musl版の可搬なバイナリが生成可能な模様:

# 必要なファイル一式をダウンロードする
$ rustup target add x86_64-unknown-linux-musl

# musl版でビルド
$ cargo run --target=x86_64-unknown-linux-musl

今回の例のように外部のCライブラリに依存している場合には、そのライブラリ自体もmuslでビルドする必要があるので、あまり手間は変わらないけど、純粋なRustプロジェクトであればかなり簡単にmusl版がビルドできそう。