virtual-dom のイベント処理

仮想 DOM を扱うための JavaScript 用ライブラリ virtual-dom では ev-xxx (例. ev-click) でイベント処理を扱えるようになっていますが、実際に機能させるには dom-delegator が必要なようです。

virtual-dom は bower install が可能で Web ブラウザ上で直接使えるファイル (dist/virtual-dom.js) を用意してくれていますが、今のところ dom-delegator はそうなっていません。(dom-delegator は npm install する)

名称 bower install Web ブラウザで直接使用
virtual-dom
dom-delegator × ×

そこで、今回は下記ツールを用いて virtual-dom で ev-click が機能するようにしてみます。

事前準備

Gulp・Bower・Browserify をインストールします。

インストール例
> npm install -g gulp bower browserify

virtual-dom のイベント処理サンプル

virtual-dom で ev-click を実施するサンプルを作成します。

ソースは http://github.com/fits/try_samples/tree/master/blog/20160107/

(1) virtual-dom インストール

virtual-dom を Bower でインストールします。

bower init と virtual-dom インストール例
> bower init
・・・

> bower install virtual-dom --save
・・・

(2) dom-delegator インストール

dom-delegator は npm でインストールします。 (dom-delegator は Bower でインストールできないため ※)

※ dom-delegator の git リポジトリを bower install する事は可能ですが、
   その場合は依存ライブラリを手動でインストールする事になります
npm init と dom-delegator インストール例
> npm init
・・・

> npm install dom-delegator --save
・・・

(3) gulp-flatten インストール

Gulp でファイルをコピーする際にディレクトリ構成をフラット化するため gulp-flatten パッケージをインストールしておきます。

gulp-flatten インストール例
> npm install gulp-flatten --save-dev

Gulp で以下のように bower_components/virtual-dom/dist/virtual-dom.js を js ディレクトリへコピーすると、js/virtual-dom/dist/virtual-dom.js が作られます。

Gulp によるコピー例
gulp.task('js-copy', () => {
   gulp.src('bower_components/*/dist/*.js')
        .pipe(gulp.dest('js'));
});

今回は、js/virtual-dom/dist/virtual-dom.js では無く js/virtual-dom.js へコピーしたかったので gulp-flatten を使いました。

(4) Gulp ビルド定義作成と実行

以下のような処理を行う gulpfile.js を作成します

  • (a) bower_components/virtual-dom/dist/virtual-dom.js を js/virtual-dom.js へコピー
  • (b) dom-delegator を Browserify で処理して js/dom-delegator.js へ出力 (Web ブラウザ上で DOMDelegator という名称で扱えるように standalone も設定)
gulpfile.js
var fs = require('fs');

var gulp = require('gulp');
var browserify = require('browserify');

var flatten = require('gulp-flatten');

// (a)
gulp.task('js-copy', () => {
    gulp.src('bower_components/*/dist/*.js')
        .pipe(flatten()) // gulp-flatten でディレクトリをフラット化
        .pipe(gulp.dest('js'));
});

// (b)
gulp.task('browserify', () => {
    browserify({
        require: 'dom-delegator',
        standalone: 'DOMDelegator'
    }).bundle().pipe(fs.createWriteStream('js/dom-delegator.js'));

});

gulp.task('default', ['js-copy', 'browserify']);

js ディレクトリを手動で作成した後、gulp コマンドを実行します。

gulp 実行例
> mkdir js
> gulp

[00:19:17] Using gulpfile ・・・\virtual-dom\gulpfile.js
[00:19:17] Starting 'js-copy'...
[00:19:17] Finished 'js-copy' after 15 ms
[00:19:17] Starting 'browserify'...
[00:19:17] Finished 'browserify' after 17 ms
[00:19:17] Starting 'default'...
[00:19:17] Finished 'default' after 6.16 μs

これで下記のファイルが作成されます。

  • js/virtual-dom.js
  • js/dom-delegator.js

(5) virtual-dom の ev-click サンプル作成と実行

Gulp で生成した js ファイルを使い virtual-dom で ev-click を扱う単純なサンプルを作成し動作確認してみます。

virtual-dom は概ね以下のようにして使います。

  • (1) virtualDom.diff 関数で 2つの VNode (VirtualNode) の差分を VPatch (VirtualPatch) として抽出
  • (2) virtualDom.patch 関数で VPatch の内容を実際の DOM へ反映
index.html
<!DOCTYPE html>
<html>
<script src="js/virtual-dom.js"></script>
<script src="js/dom-delegator.js"></script>
<body>
    <h1>virtual-dom click sample</h1>

    <div id="ct"></div>

    <script>
        (function() {
            // ev-xxx イベントの有効化 (dom-delegator)
            new DOMDelegator();

            var h = virtualDom.h;
            var create = virtualDom.create;
            var diff = virtualDom.diff;
            var patch = virtualDom.patch;

            var index = 1;
            var tree = h();

            var render = function(v) {
                // 仮想 DOM の作成
                return h('div', [
                    h('button', 
                        {
                            // click イベントの設定
                            'ev-click': function(ev) { update(index++) }
                        },
                        ['countUp']
                    ),
                    h('br'),
                    'counter: ' + String(v)
                ]);
            }

            var update = function(v) {
                var newTree = render(v);

                // tree と newTree の差分を実際の DOM へ反映
                patch(document.getElementById('ct'), diff(tree, newTree));

                tree = newTree;
            };

            update(index++);
        })();

    </script>
</body>
</html>

Web ブラウザで実行し countUp ボタンを押下すると counter の数値がカウントアップする事を確認できました。

Web ブラウザ表示例

f:id:fits:20160107023648p:plain

virtual-dom イベント処理の仕組み

最後に、virtual-dom と dom-delegator がどのようにイベント処理を行っているのか簡単に書いておきます。

まず、VNode の作成時に ev- で始まるプロパティは EvHook へ変換されます。(virtual-hyperscript/index.js の transformProperties)

patch 関数で実際の DOM へ反映する際に、対象の DOM ノードへ特殊なプロパティ __EV_STORE_KEY@7 ※ を追加して EvHook の内容 (イベント名とイベント処理関数) を設定します。 (ev-store の機能)

※ プロパティ名は ev-store の index.js で定義されており、
   ev-store 7.0.0 では __EV_STORE_KEY@7 となっています
__EV_STORE_KEY@7 が設定されている DOM ノード例

f:id:fits:20160107023717p:plain

dom-delegator は "blur", "change", "click" 等の多数のイベントを document.documentElement で listen するように処理し、イベント発生時に対象 DOM ノードの __EV_STORE_KEY@7 プロパティの内容をチェックして、該当するイベントが設定されていればその処理関数を実行します。

つまり、上記のようなイベント処理を自前で行えば dom-delegator を使わなくても ev-click を機能させる事が可能です。

index2.html (dom-delegator を使わずに ev-click を有効化する例)
<!DOCTYPE html>
<html>
<script src="js/virtual-dom.js"></script>
<body>
    <h1>virtual-dom click sample2</h1>

    <div id="ct"></div>

    <script>
        (function() {
            // dom-delegator の代用処理
            document.addEventListener('click', function(ev) {
                var store = ev.target['__EV_STORE_KEY@7'];

                if (store && store[ev.type]) {
                    // 該当するイベント処理関数の実行
                    store[ev.type](ev);
                }
            }, true);

            var h = virtualDom.h;
            ・・・
            var render = function(v) {
                return h('div', [
                    h('button', 
                        {
                            'ev-click': function(ev) { update(index++) }
                        },
                        ['countUp']
                    ),
                    h('br'),
                    'counter: ' + String(v)
                ]);
            }
            ・・・
        })();

    </script>
</body>
</html>

pulp を使った PureScript の開発

PureScript 用のビルドツールpulp があります。

pulp を使えば PureScript v0.7 から多少面倒になったビルドや実行が比較的容易になります。

pulp インストール

Node.js の npm で purescript (コンパイラ) と pulp をインストールします。

pulp インストール例
> npm install -g purescript pulp

今回インストールした PureScript コンパイラpulp のバージョンは以下の通りです。

なお、PureScript コンパイラに関しては https://github.com/purescript/purescript/releases/ から各 OS 用のバイナリを直接取得する方法もあります。

npm でインストールしたものも実際は node_modules/purescript/vendor ディレクトリへ配置された各 OS 用のバイナリファイル (例. psc.exe) を使っているようです。

pulp を使った開発

今回作成したソースは http://github.com/fits/try_samples/tree/master/blog/20160105/

プロジェクトの作成

任意のディレクトリ内で pulp init を実行すると、必要最小限のファイルが生成されます。

その際に Bower を使って PureScript の主要ライブラリ (以下) を自動的に取得しますので、git コマンドを使えるようにしておく必要があります。

  • purescript-console
  • purescript-eff
  • purescript-prelude
プロジェクト作成例
> pulp init

* Generating project skeleton in ・・・
bower cached        git://github.com/purescript/purescript-console.git#0.1.1
bower validate      0.1.1 against git://github.com/purescript/purescript-console.git#^0.1.0
bower cached        git://github.com/purescript/purescript-eff.git#0.1.2
bower validate      0.1.2 against git://github.com/purescript/purescript-eff.git#^0.1.0
bower cached        git://github.com/purescript/purescript-prelude.git#0.1.3
bower validate      0.1.3 against git://github.com/purescript/purescript-prelude.git#^0.1.0
bower install       purescript-console#0.1.1
bower install       purescript-eff#0.1.2
bower install       purescript-prelude#0.1.3

purescript-console#0.1.1 bower_components\purescript-console
└── purescript-eff#0.1.2

purescript-eff#0.1.2 bower_components\purescript-eff
└── purescript-prelude#0.1.3

ディレクトリ・ファイル構成は以下のようになります。

  • bower_components
    • purescript-console
    • purescript-eff
    • purescript-prelude
  • src/Main.purs
  • test/Main.purs
  • .gitignore
  • bower.json

デフォルトで用意されている src/Main.purs の内容は以下の通りです。

src/Main.purs
module Main where

import Prelude
import Control.Monad.Eff
import Control.Monad.Eff.Console

main :: forall e. Eff (console :: CONSOLE | e) Unit
main = do
  log "Hello sailor!"

ライブラリの追加には pulp dep install <ライブラリ名> を実行します。 そうすると bower install が実施されます。

bower.json の依存パッケージ設定へエントリを追加するには --save オプションを付けます。

purescript-tuples の追加例
> pulp dep install purescript-tuples --save

・・・
bower install       purescript-control#0.3.2
bower install       purescript-invariant#0.3.0

purescript-tuples#0.4.0 bower_components\purescript-tuples
└── purescript-foldable-traversable#0.4.2

purescript-foldable-traversable#0.4.2 bower_components\purescript-foldable-traversable
├── purescript-bifunctors#0.4.0
└── purescript-maybe#0.3.5

purescript-maybe#0.3.5 bower_components\purescript-maybe
└── purescript-monoid#0.3.2

purescript-bifunctors#0.4.0 bower_components\purescript-bifunctors
└── purescript-control#0.3.2

purescript-monoid#0.3.2 bower_components\purescript-monoid
├── purescript-control#0.3.2
└── purescript-invariant#0.3.0

purescript-control#0.3.2 bower_components\purescript-control
└── purescript-prelude#0.1.3

purescript-invariant#0.3.0 bower_components\purescript-invariant
└── purescript-prelude#0.1.3

ビルドと実行

Main.purs を以下のようにタプルを使った処理に書き換えて実行してみます。

src/Main.purs
module Main where

import Prelude
import Control.Monad.Eff
import Control.Monad.Eff.Console
import Data.Tuple

main :: forall e. Eff (console :: CONSOLE | e) Unit
main = do
  let t = Tuple "two" 2
  print t
  
  let r = t >>= \x -> Tuple " + 1" (x + 1)
  print r

pulp run を実行するとビルドした後に処理を実施します。
ビルドだけを実施したい場合は pulp build を使います。

実行
> pulp run

* Building project in ・・・\20160105\purescript
psc: No files found using pattern: src/**/*.js
* Build successful.
Tuple ("two") (2)
Tuple ("two + 1") (3)

ビルド結果は output ディレクトリへ生成されます。

パッケージング

pulp browserify を実行すると Browserify を使って output ディレクトリ内のファイルをパッケージングしてくれます。

browserify によるパッケージング
> pulp browserify > sample.js

* Browserifying project in ・・・\20160105\purescript
* Project unchanged; skipping build step.
* Browserifying...

パッケージングしたファイル (sample.js) の実行結果は以下の通りです。

実行結果
> node sample.js

Tuple ("two") (2)
Tuple ("two + 1") (3)

Web ブラウザで実行する事もできます。

index.html
<!DOCTYPE html>
<html>
<script src="sample.js"></script>
</html>

f:id:fits:20160105001028p:plain

備考 - pulp を使わない場合

最後に pulp を使わない場合のビルド・実行方法も書いておきます。

まずは、Bower を使って purescript-console 等の必要なライブラリを手動でインストールします。

ライブラリのインストール例
> bower install purescript-console --save

src/Main.purs を psc コマンドでビルドするには以下のようにします。

psc によるビルド例
> psc src/Main.purs bower_components/purescript-*/src/**/*.purs --ffi bower_components/purescript-*/src/**/*.js

bower_components 内の .purs ファイルと --ffi オプションで bower_components 内の .js ファイルを指定します。

ビルド結果はデフォルトで output ディレクトリへ生成されます。(-o オプションで変更する事も可能)

実行する場合は、NODE_PATH 環境変数へ output ディレクトリを設定し、node コマンドで require('Main/index.js').main() を実行します。

実行例
> set NODE_PATH=output
> node -e "require('Main/index.js').main()"

Tuple ("two") (2)
Tuple ("two + 1") (3)

Google Cloud Print を Web API で操作 - Unirest 使用

リフレッシュトークンを使って Google Cloud Print を Web API で操作してみます。

以前、「Google アカウントで Google API を利用 - google-api-services-gmail」 では Apache HTTPClient を使いましたが、今回は Unirest を使っています。

ソースは http://github.com/fits/try_samples/tree/master/blog/20151222/

はじめに

事前準備としてリフレッシュトークンを取得しておきます。

今回は、「Google アカウントで・・・」 にて発行したクライアント ID をそのまま使用します。

(1) コードの取得

下記パラメータを付け https://accounts.google.com/o/oauth2/auth へ Web ブラウザ等でアクセスし API の利用を許可する事でコードを取得します。

パラメータ
response_type code
client_id クライアント ID の client_id 値(※1)
scope https://www.googleapis.com/auth/cloudprint
redirect_uri oob (※2)
(※1)クライアント ID 発行時にダウンロードした JSON ファイルの client_id 値

(※2)リダイレクトしない場合の設定。
       何らかのアプリケーション内から実行する際は
       それに合わせたリダイレクト先を指定する

例えば、以下のような URL へアクセスします。

URL 例
https://accounts.google.com/o/oauth2/auth?redirect_uri=oob&response_type=code&client_id=xxx.apps.googleusercontent.com&scope=https://www.googleapis.com/auth/cloudprint

Google アカウントでログインすると以下のような画面が表示されますので、「許可」ボタンを押下します。

f:id:fits:20151221233721p:plain

コードが表示されるのでコピーしておきます。

f:id:fits:20151221233746p:plain

(2) リフレッシュトークンの取得

次に、下記パラメータを https://www.googleapis.com/oauth2/v3/token へ POST し、リフレッシュトークンを含んだ JSON を取得します。

パラメータ
code (1) で取得したコード
client_id クライアント ID の client_id 値
client_secret クライアント ID の client_secret 値
grant_type authorization_code
redirect_uri oob (※1)
(※1)(1) の場合と同様です。

       ただし、以下のスクリプトでは
       urn:ietf:wg:oauth:2.0:oob を設定しています

Groovy スクリプト化すると以下のようになります。

get_refresh-token.groovy
@Grab('com.mashape.unirest:unirest-java:1.4.9')
import com.mashape.unirest.http.Unirest
import groovy.json.JsonSlurper

addShutdownHook {
    Unirest.shutdown()
}

def json = new JsonSlurper()

def conf = json.parse(new File(args[0])).installed
def code = args[1]

def res = Unirest.post('https://www.googleapis.com/oauth2/v3/token')
    .field('code', code)
    .field('client_id', conf.client_id)
    .field('client_secret', conf.client_secret)
    .field('grant_type', 'authorization_code')
    .field('redirect_uri', conf.redirect_uris[0])
    .asJson()

println res.body

クライアント ID 発行時にダウンロードした JSON (ここでは client_secret.json) を第1引数、(1) で取得したコードを第2引数へ指定して実行します。

取得したリフレッシュトークン JSON は後で使うので保存しておきます。 (ここでは token.json へ保存)

実行例
> groovy get_refresh-token.groovy client_secret.json 4/9ic・・・ > token.json

プリンタ一覧の取得

リフレッシュトークンを使う場合は、以下のような手順で API を呼び出します。

  1. リフレッシュトークンからアクセストークンを取得
  2. アクセストークンを使って API を実行

リフレッシュトークンからアクセストークン取得

アクセストークンを取得するには、下記パラメータを https://www.googleapis.com/oauth2/v3/token へ POST します。

パラメータ
client_id クライアント ID の client_id 値
client_secret クライアント ID の client_secret 値
grant_type refresh_token
refresh_token リフレッシュトークンの値 (※1)
(※1)リフレッシュトークン取得で保存した JSON の refresh_token 値

この処理は共通的に使用するため、Groovy の BaseScript として定義しました。

TokenScript.groovy
import com.mashape.unirest.http.Unirest
import groovy.json.JsonSlurper

abstract class TokenScript extends Script {
    // アクセストークンの取得
    def accessToken(String clientId, String clientSecret, String refreshToken) {
        def res = Unirest.post('https://www.googleapis.com/oauth2/v3/token')
            .field('client_id', clientId)
            .field('client_secret', clientSecret)
            .field('grant_type', 'refresh_token')
            .field('refresh_token', refreshToken)
            .asJson()

        res.body.object
    }
}

API 実行 (プリンタの一覧取得)

アクセストークンは HTTP ヘッダーへ Authorization: Bearer <アクセストークン> のように設定して使います。(例. Authorization: Bearer ya26.pw・・・)

クラウドプリンタ一覧は https://www.google.com/cloudprint/search へ GET すれば取得できます。 (q や type パラメータを付けて条件検索することも可能)

get_printers.groovy
@Grab('com.mashape.unirest:unirest-java:1.4.9')
import com.mashape.unirest.http.Unirest
import groovy.json.JsonSlurper
import groovy.transform.BaseScript

// TokenScript を BaseScript へ設定
@BaseScript TokenScript baseScript

addShutdownHook {
    Unirest.shutdown()
}

def json = new JsonSlurper()

def conf = json.parse(new File(args[0])).installed
def token = json.parse(new File(args[1]))

// アクセストークン取得
def newToken = accessToken(
    conf.client_id,
    conf.client_secret,
    token.refresh_token
)

// API の実行
def printers = Unirest.get('https://www.google.com/cloudprint/search')
    .header('Authorization', "${newToken.token_type} ${newToken.access_token}")
    .asJson()

println printers.body

実行結果は以下の通りです。
まだプリンタを登録していないためデフォルトの 「ドライブに保存」 しかありません。

実行結果 (出力結果は加工済、実際は改行・字下げは無し)
> groovy get_printers.groovy client_secret.json token.json

{
    "request":{・・・},
    "printers":[{
        "isTosAccepted":false,
        "displayName":"ドライブに保存",
        "capsHash":"",
        "description":"ドキュメントを Google ドライブで PDF として保存します",
        "updateTime":"1370287324050",
        "ownerId":"cloudprinting@gmail.com",
        "type":"DRIVE",
        "tags":["save","docs","pdf","google","__google__drive_enabled"],
        "proxy":"google-wide",
        "ownerName":"Cloud Print",
        "createTime":"1311368403894",
        "defaultDisplayName":"ドライブに保存",
        "connectionStatus":"ONLINE",
        "name":"Save to Google Docs",
        "id":"__google__docs",
        "accessTime":"1316132041869",
        "status":""
    }],
    "success":true,
    ・・・
}

また、https://www.google.com/cloudprint/list?proxy=<プロキシ> へ GET すると、指定プロキシへ属するプリンタ一覧を取得できます。

印刷処理 (Google Drive へ保存)

最後に、「ドライブに保存」 プリンタへファイルを印刷してみます。 (実際は Google Drive へファイル保存)

印刷は、下記の必須パラメータ(他にもパラメータあり)を multipart/form-datahttps://www.google.com/cloudprint/submit へ POST します。

パラメータ
printerid プリンタ ID (※1)
title 印刷タイトル
ticket 印刷設定 (※2)
content 印刷するファイルの内容
(※1)今回は __google__docs を使用
(※2)CJT (Cloud Job Ticket) フォーマットで印刷オプションなどを指定
submit_file.groovy
@Grab('com.mashape.unirest:unirest-java:1.4.9')
import com.mashape.unirest.http.Unirest
import groovy.json.JsonSlurper
import groovy.json.JsonBuilder
import groovy.transform.BaseScript

@BaseScript TokenScript baseScript

addShutdownHook {
    Unirest.shutdown()
}

def ticketBuilder = new JsonBuilder()

// ticket の内容
ticketBuilder (
    version: '1.0',
    print: {}
)

def json = new JsonSlurper()

def conf = json.parse(new File(args[0])).installed
def token = json.parse(new File(args[1]))
def file = new File(args[2])

// アクセストークンの取得
def newToken = accessToken(
    conf.client_id,
    conf.client_secret,
    token.refresh_token
)

// ticket の JSON 文字列化
def ticket = ticketBuilder.toString()

// 印刷
def res = Unirest.post('https://www.google.com/cloudprint/submit')
    .header('Authorization', "${newToken.token_type} ${newToken.access_token}")
    .field('printerid', '__google__docs')
    .field('title', file.name)
    .field('ticket', ticket)
    .field('content', file)
    .asJson()

println res.body

PDF ファイルを POST した結果は以下の通りです。
Google Drive で確認すると sample1.pdf ファイルが保存されていました。

実行結果 (出力結果は加工済、実際は改行・字下げは無し)
> groovy submit_file.groovy client_secret.json token.json data/sample1.pdf

{
    "request":{・・・},
    "success":true,"xsrf_token":"AIp・・・",
    "message":"印刷ジョブが追加されました。",
    "job":{
        "ticketUrl":"https://www.google.com/cloudprint/ticket?format=xps&output=xml&jobid=0db・・・",
        "printerName":"",
        "errorCode":"",
        ・・・
        "title":"sample1.pdf",
        "tags":["^own"],
        ・・・
        "printerid":"__google__docs",
        ・・・
        "contentType":"application/pdf",
        "status":"DONE"
    }
}

また、試しに以下のような拡張子のファイルを POST してみたところ、自動的に PDF へ変換され Google Drive へ保存されました。 (例えば sample1.html は sample1.html.pdf で保存)

  • .html
  • .docx
  • .odt

Android エミュレータの hosts ファイルを編集

Android エミュレータの hosts ファイルを編集する方法です。
以下のような方法が考えられます。

  • (a) shell で hosts へ行を追加
  • (b) pull と push で hosts を変更

(a) shell で hosts へ行を追加

行を追加するだけなら以下のように shell を使うだけです。

adb remount しておかないと /system/etc/hosts ファイルを更新できずに Read-only file system のようなエラーメッセージが表示される点に注意。 (既に adb remount 済みの場合は不要)

hosts への行追加
> adb remount
remount succeeded

> adb shell "echo 10.0.2.2 server1 >> /system/etc/hosts"

/system/etc/hosts の内容を確認してみます。

hosts の内容確認
> adb shell cat /system/etc/hosts

127.0.0.1                   localhost
10.0.2.2 server1

10.0.2.2 はエミュレータから接続する際のホスト OS の IP アドレスなので、ping してみます。

ping の実行
> adb shell ping server1

PING server1 (10.0.2.2) 56(84) bytes of data.
64 bytes from server1 (10.0.2.2): icmp_seq=1 ttl=255 time=0.000 ms
・・・

remount に関して

adb remount する事で以下のように /system が rw で再マウントされます。

adb remount 前 (ro)
/dev/block/mtdblock0 /system ext4 ro,relatime,data=ordered 0 0
adb remount 後 (rw)
/dev/block/mtdblock0 /system ext4 rw,relatime,data=ordered 0 0

(b) pull と push で hosts を変更

次は hosts ファイルを編集する方法です。

エミュレータには vi 等のエディタが用意されていないようなので、pull で hosts ファイルを取得して、ホスト OS 側で hosts ファイルを編集し、push でエミュレータへ反映します。

まずは adb pull で hosts ファイルを取得します。

hosts を pull
> adb pull /system/etc/hosts

ホスト OS のカレントディレクトリへ pull された hosts ファイルを編集します。

hosts 変更例
127.0.0.1           localhost
10.0.2.2            server1

変更した hosts を push します。
まだ一度も remount していない場合は remount する必要があります。

hosts を push
> adb remount
・・・

> adb push hosts /system/etc/

Swift のビルド環境を Docker で構築

Ubuntu をベースとした Swift のビルド環境を Docker で構築してみました。

使用したソースは http://github.com/fits/try_samples/tree/master/blog/20151207/

Docker イメージ作成

ubuntu:15.10 のイメージをベースに Swift をセットアップしました。

clang 等の必要なパッケージをインストールした後、swift-2.2-SNAPSHOT-2015-12-01-b-ubuntu15.10.tar.gz をダウンロードして解凍し、環境変数 PATH へ swift-2.2-・・・/usr/bin のパスを追加するだけです。

また、clang をインストールしただけだと swiftc の実行時に /usr/bin/ld: warning: libicuuc.so.55, needed by /swift-2.2・・・/usr/lib/swift/linux/libswiftCore.so, not found (try using -rpath or -rpath-link) のようなメッセージが出力されたので、libicu-dev もインストールするようにしています。

Dockerfile
FROM ubuntu:15.10

ENV SWIFT_PACKAGE swift-2.2-SNAPSHOT-2015-12-01-b-ubuntu15.10

RUN apt-get update
RUN apt-get -y upgrade

RUN apt-get -y install curl clang libicu-dev

# swift のパッケージをダウンロード
RUN curl https://swift.org/builds/ubuntu1510/swift-2.2-SNAPSHOT-2015-12-01-b/$SWIFT_PACKAGE.tar.gz -o $SWIFT_PACKAGE.tar.gz

RUN tar zxf $SWIFT_PACKAGE.tar.gz
RUN rm -f $SWIFT_PACKAGE.tar.gz

RUN apt-get clean

# 環境変数 PATH の設定
ENV PATH /$SWIFT_PACKAGE/usr/bin:$PATH

docker build で Docker イメージを作成します。

Docker イメージの作成
$ docker build --no-cache -t sample/swift:0.1 .

・・・
Successfully built ・・・

動作確認

作成した Docker イメージを使って以下のようなサンプルソースを実行してみます。

/vagrant/work/sample.swift
let a = 11
let b = 2

print("result = \(a * b)")

まず、sample/swift:0.1 イメージを使って Docker コンテナを起動します。

Docker コンテナの起動
$ docker run --rm -it -v /vagrant/work:/work sample/swift:0.1 bash
・・・
# cd /work

コンテナ内で sample.swift を実行します。

直接実行
# swift sample.swift

result = 22
ビルドしてから実行
# swiftc sample.swift
# ./sample

result = 22

Objective-C のビルド環境を Docker で構築

CentOS をベースとした Objective-C のビルド環境を Docker で構築してみました。

使用したソースは http://github.com/fits/try_samples/tree/master/blog/20151130/

Docker イメージ作成

今回は clang と GNUstep を使う事にします。

バージョンは古くなってしまいますが、どちらも yum でインストールするようにしました。

make をインストールしておかないと GNUstepgnustep-config コマンドが機能しない (結果が空になる) のでご注意ください。

また、gcc-objc をインストールしておかないと、Objective-C のソースをビルドする際に必要なヘッダーファイルを参照できず、fatal error: 'inttypes.h' file not found のようなエラーが発生しました。

Dockerfile
FROM centos

RUN yum -y update

RUN yum -y install epel-release
RUN yum -y install clang make gcc-objc gnustep-base-devel

RUN yum clean all

docker build で Docker イメージを作成します。

Docker イメージの作成
$ docker build --no-cache -t sample/objc:0.1 .

・・・
Successfully built ・・・

動作確認

以下のようなサンプルソースをビルドしてみます。

/vagrant/work/Sample.h
#import <Foundation/NSObject.h>
#import <Foundation/NSString.h>

@interface Sample : NSObject {
    NSString* _name;
}

@property (nonatomic, copy) NSString* name;

- (void)log;

@end
/vagrant/work/Sample.m
#import <Foundation/Foundation.h>
#import "Sample.h"

@implementation Sample : NSObject

@synthesize name = _name;

- (void)log {
    NSLog(@"%@", _name);
}

- (void)dealloc {
    [_name release];
    [super dealloc];
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Sample* s = [[Sample new] autorelease];

        s.name = @"test";

        [s log];
    }

    return 0;
}

まずは sample/objc:0.1 イメージを使って Docker コンテナを起動します。

Docker コンテナの起動
$ docker run --rm -it -v /vagrant/work:/work sample/objc:0.1 bash

コンテナ内で clang を使ってビルドします。

コンパイラオプションに関しては以下で妥当なのかは不明ですが、一応ビルドには成功しました。 (場合によっては -objcmt-migrate-all 等の指定も必要かもしれません)

ビルド
# cd work

# clang `gnustep-config --objc-flags` `gnustep-config --objc-libs` -lgnustep-base -I /usr/lib/gcc/x86_64-redhat-linux/4.8.3/include Sample.m -o Sample

clang: warning: argument unused during compilation: '-shared-libgcc'

一応、それぞれのオプションを指定しなかった場合に発生したエラーをまとめておきます。

オプション指定 指定しなかった場合のエラー例
`gnustep-config --objc-flags` 無し(指定しなくてもビルドは成功)
`gnustep-config --objc-libs` undefined reference to symbol 'objc_getProperty'
-lgnustep-base undefined reference to `NSLog'
-I /usr/lib/gcc/・・・/include fatal error: 'objc/objc.h' file not found

ビルド結果の Sample を実行してみると一応動作しました。

実行例
# ./Sample

2015-11-30 20:15:00.121 Sample[32] test

なお、clang -v の実行結果は以下のようになりました。

clang -v
# clang -v

clang version 3.4.2 (tags/RELEASE_34/dot2-final)
Target: x86_64-redhat-linux-gnu
Thread model: posix
Found candidate GCC installation: /usr/bin/../lib/gcc/x86_64-redhat-linux/4.8.2
Found candidate GCC installation: /usr/bin/../lib/gcc/x86_64-redhat-linux/4.8.3
Found candidate GCC installation: /usr/lib/gcc/x86_64-redhat-linux/4.8.2
Found candidate GCC installation: /usr/lib/gcc/x86_64-redhat-linux/4.8.3
Selected GCC installation: /usr/bin/../lib/gcc/x86_64-redhat-linux/4.8.3

twemproxy + Redis 環境を Docker で構築

twemproxy(別名 nutcracker) + Redis の環境を Docker で構築してみます。 (twemproxy は memcached・Redis 用の軽量なプロキシです)

使用した設定ファイル等は http://github.com/fits/try_samples/tree/master/blog/20151124/

Redis の Docker イメージを取得

Redis の Docker イメージは docker pull で取得する事にします。

$ docker pull redis

twemproxy の Docker イメージを構築

twemproxy の Docker イメージは、下記 Dockerfile を使って centos のイメージをベースに twemproxy のソースをビルドして作成する事にします。

Dockerfile
FROM centos

RUN yum -y update
RUN yum -y install make automake libtool git

RUN git clone https://github.com/twitter/twemproxy.git
RUN cd twemproxy && autoreconf -fvi && ./configure && make && make install
RUN rm -fr twemproxy

RUN yum clean all

上記 Dockerfile で docker build を実行すれば Docker イメージを作成できます。

Docker イメージの作成
$ docker build --no-cache -t sample/twemproxy:0.1 .

・・・
Successfully built ・・・

twemproxy のビルドで warning は出ましたが、Docker イメージの作成に成功しました。

twemproxy + Redis × 2 の実行

twemproxy と Redis 2台を個別の Docker コンテナで実行してみます。

twemproxy の設定ファイルで接続する Redis サーバーを <ホスト名 or IPアドレス>:<ポート番号>:<重み> で指定する必要があり、これが課題となります。

twemproxy 設定ファイル例
sample:
  ・・・
  redis: true
  servers:
   - 127.0.0.1:6380:1
   - 127.0.0.1:6381:1
   ・・・

今回は、起動スクリプトを使って twemproxy と Redis の Docker コンテナをまとめて起動するようにしてみました。

該当コンテナを docker start (コンテナを再起動) してみて、失敗した場合は docker run を行うようにしています。 (docker start し易いように --name=<コンテナ名> で名前を付けています)

また、コンテナ間の接続を容易にするため、docker run 時に -h <ホスト名> でホスト名も付けました。 (コンテナ名をそのままホスト名に使っています)

/vagrant/tmp/start_twemproxy (twemproxy + Redis コンテナの起動スクリプト
#!/bin/sh

INDENT="    "
CONF_DIR=/vagrant/tmp/conf
CONTAINER_CONF_DIR=/conf
TWEMPROXY_IMAGE=sample/twemproxy:0.1

CONTAINER_REDIS_LIST="redis1 redis2"
CONTAINER_TWEMPROXY=twemproxy1

REDIS_SERVERS=""

# Redis のコンテナを個々に起動
for s in $CONTAINER_REDIS_LIST
do
  if test -z `docker start $s`; then
    docker run --name=$s -h $s -d redis
  else
    echo start $s
  fi

  REDIS_SERVERS="$REDIS_SERVERS$INDENT- $s:6379:1\n"
done

# twemproxy の設定ファイルを生成
sed -e "s/#{SERVERS}/$REDIS_SERVERS/g" `dirname $0`/nutcracker.yml.tpl > $CONF_DIR/nutcracker.yml

# twemproxy のコンテナ起動
if test -z `docker start $CONTAINER_TWEMPROXY`; then
  docker run --name=$CONTAINER_TWEMPROXY -h $CONTAINER_TWEMPROXY -d -p 6379:6379 -v $CONF_DIR:$CONTAINER_CONF_DIR $TWEMPROXY_IMAGE nutcracker -c $CONTAINER_CONF_DIR/nutcracker.yml
else
  echo start $CONTAINER_TWEMPROXY
fi

twemproxy は nutcracker -c <設定ファイル> で起動しています。

また、twemproxy 設定ファイル(nutcracker.yml)は Redis サーバーの構成に合わせて下記テンプレートの #{SERVERS}sed で置き換えて生成するようにしています。

/vagrant/tmp/nutcracker.yml.tpl (twemproxy 設定ファイルのテンプレート)
sample:
  listen: 0.0.0.0:6379
  hash: fnv1a_64
  distribution: ketama
  timeout: 500
  redis: true
  servers:
#{SERVERS}

実行

それでは実行してみます。
初回起動時は docker start に失敗し docker run を実施する事になります。

twemproxy1, redis1, redis2 コンテナの実行例
$ /vagrant/tmp/start_twemproxy

Error response from daemon: no such id: redis1
Error: failed to start containers: [redis1]
bdc12db5・・・
Error response from daemon: no such id: redis2
Error: failed to start containers: [redis2]
fd4546fc・・・
Error response from daemon: no such id: twemproxy1
Error: failed to start containers: [twemproxy1]
87bb567e・・・

redis-cli で twemproxy1 へ接続する Docker コンテナを起動し (Redis の Docker イメージを使っています) 、動作確認してみます。

動作確認
$ docker run --rm -it redis redis-cli -h twemproxy1

twemproxy1:6379> set a 123
OK
twemproxy1:6379> get a
"123"

twemproxy は正常に動作しているようです。

redis1・2 へ直接接続するコンテナをそれぞれ実行し確認すると、値は redis2 の方に設定されていました。

$ docker run --rm -it redis redis-cli -h redis1
redis1:6379> keys *
(empty list or set)
redis1:6379> exit

$ docker run --rm -it redis redis-cli -h redis2
redis2:6379> keys *
1) "a"
redis2:6379> exit

備考 - Vagrant 利用時

Vagrant で起動したゲスト OS 上で Docker を実行している場合、Vagrantfile へ以下のような設定を追加すれば provision で各コンテナを起動しホスト OS から twemproxy へ接続できるようになります。

Vagrantfile
・・・
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
    ・・・
    # twemproxy 用のポートフォワード設定
    config.vm.network "forwarded_port", guest: 6379, host: 6379

    # twemproxy + redis の起動スクリプト実行
    config.vm.provision :shell, :inline => "/vagrant/tmp/start_twemproxy"
end
provision 実施例
> vagrant up --provision

・・・
==> default: Running provisioner: shell...
    default: Running: inline script
==> default: start redis1
==> default: start redis2
==> default: start twemproxy1
ホスト OS からの接続例
> redis-cli
127.0.0.1:6379> get a
"123"