Qt Quick 2でHello World
前回の記事でQt Creatorの設定が完了したので、Qt Quick 2のプロジェクトを作成して、Qt Creatorからネットワーク上のRaspberry Pi上で実行してみます。
プロジェクトの作成
Create Projectから「Qt Quick 2 application」を選択します。
適当に名前とパスを指定して次へ。Kitの選択で、Raspberry Pi用のKitにチェックを入れます。
ちなみに、Kitは作成済みのプロジェクトに、後から設定することもできます。
ビルドや実行などに使用するKitは、Qt Creatorの左にあるメニューから簡単に切り替えることができます。
プロジェクトの設定
実行の前に一旦プロジェクトの設定を確認します。Raspberry Pi -> RunのRemote Directoryを確認すると、「/opt/HelloWorld」などとなっています。
これはアプリのインストールパスで、Qt Creatorから実行ボタンを押した際にプログラムがコピー(転送)されるパスになります。
Raspberry PiへのアクセスはQt CreatorのDevicesで設定したアカウントでアクセスすることになりますが、そのアカウントにアクセス権が無いディレクトリの場合、転送が失敗して実行できません。
手段はいくつか考えられます。
- Devicesで設定するアカウントをrootなど、必要な権限を持ったものにする
- sshなどによって、あらかじめアクセス可能なディレクトリを用意する
- Qt Creatorのインストールパスを変える
今回は3つ目の手段をとります。
.proファイルの編集
プロジェクトの.proファイルを開くと、target.pathがコメントアウトされているのが分かります。これがインストールパスに当たるので、ここにアクセス可能なディレクトリを指定します。
# Installation path target.path = /home/pi/HelloWorld
設定後、再度プロジェクト設定を確認すると反映されているのがわかります。
この状態で再度実行すると、今度は成功し、Raspberry Pi上でプログラムが実行されます。
Raspberry Pi用にQt Creatorを設定する
前回の記事でRaspberry Pi向けにQtをビルドしましたが、今回は開発用にQt Creatorの設定をします。これも元記事はBegginer's Guideです。
Qt Creatorのインストール
インストーラーをダウンロードし、実行権限を付与して実行。ウィザードに従ってインストールします。
$ chmod +x qt-linux-opensource-5.1.1-x86-offline.run $ ./qt-linux-opensource-5.1.1-x86-offline.run
Qtをインストールしたフォルダを「creator」で検索するとアイコン画像が見つかるので、コピーしてLauncherなどでつかうとよいです。
Qt Creatorの設定
Raspberry Pi向けにクロスビルドできるように設定していきます。
ちょっとその前に...
Qtのビルド時にセットアップしたRaspberry Piのイメージをマウントします。ビルド直後であればマウントしっぱなしでしょうが、PCの再起動などでマウントは解除されます。マウントしていないと後々の設定(Qt Versions)でエラーが出ます。
$ sudo mount -o loop,offset=62914560 raspbian.img /mnt/rasp-pi-rootfs/
まあ、どちらにしろ、クロスビルドの際に必須です。慣れないうちは良く忘れて無駄に時間をつかったり...基本VM運用だとめったに再起動しないのでなおさら...
ちなみにイメージは一旦Raspberry Piでパーティションサイズを拡張後、ddでイメージ化したものを用いています。
Build & Run -> Compilers
設定はCompilersから行います。オプションを開いてBuild & RunのCompilersを選択。Add -> GCCより追加します。
コンパイラのパスにクロスコンパイラを指定します。私の場合は、「/home/ayataka/local/opt/gcc-4.7-linaro-rpi-gnueabihf/bin/arm-linux-gnueabihf-g++」。通常のGCCと区別が付くように名前をつけておくとよいです。
Build & Run -> Qt Versions
次にQt Versions。こちらもAddを押し、qmakeのパスを指定します。Raspberry Piのイメージがマウントされていないとエラーが出ます。
Devices
ここで、一旦Devicesに移って、リモートデバイスの設定をします。開発PCのQt CreatorからリモートのRaspberry Pi上で実行できるようになります。
Addを押して、Generic Linux Deviceを選択。
次の画面で、名前、IPアドレスなどを設定します。
初期設定のタイムアウトが10秒なので、接続に失敗するようなら少し伸ばして見ると良いです。
Build & Run -> Kits
最後にKitを作ります。こちらもAddより追加して設定。これまで設定してきたものを選択して行く感じですね。
Qt 5をRaspberry Pi向けにビルドする
Raspberry Pi向けにQtをビルドします。以下は元記事。
結構手順があるのですが、慣れるとそれほど手間はかかりません。DLとビルドの時間は別ですが。
Beginner's Guideは失敗時のフォローやQt Creatorの設定もあるので記事は長くなっていますが、ビルドだけ見れば、手順はそれほどでもありません。また、情報量は多く、エラーが出た場合ググるよりも先にBeginner's Guideを確認したほうがよいです。
手順
手順は次のとおり。
- ディレクトリなどの準備
- Raspberry PiのイメージのDLとマウント
- クロスコンパイラとツールの入手
- Qtソースの取得
- ビルドとインストール
- SDカードへ書き込み
ライブラリのビルドと同時に、Raspberry Piのイメージ(をマウントしたところ)にシステムを構築していきます。
ビルド
ディレクトリなどの準備
ツール類などのインストールが必要なので、ディレクトリを準備します。元記事では~/optになっていますがお好みで。
$ mkdir -p ~/local/opt #ツール用 $ mkdir -p ~/raspi/Images #イメージ用 $ mkdir -p ~/raspi/Projects #ビルド用
Raspberry PiのイメージのDLとマウント
本家から落とします。定期的に更新されていますが、最新版をDLすればよいでしょう。DLが完了したら解凍します。
$ cd ~/raspi/Images $ wget http://downloads.raspberrypi.org/images/raspbian/2013-07-26-wheezy-raspbian/2013-07-26-wheezy-raspbian.zip $ unzip 2013-07-26-wheezy-raspbian.zip
で、マウント。マウントするディレクトリはあらかじめ作ります。
$ sudo mkdir /mnt/rasp-pi-rootfs $ sudo mount -o loop,offset=62914560 2013-07-26-wheezy-raspbian.img /mnt/rasp-pi-rootfs
ここで「loop,offset=62914560」ですが、システム側のパーティションをマウントするための細工で、fdiskによって調べることができます。
$ fdisk 2013-07-26-wheezy-raspbian.img Command (m for help): p Disk 2013-07-26-wheezy-raspbian.img: 1939 MB, 1939865600 bytes 255 heads, 63 sectors/track, 235 cylinders, total 3788800 sectors Units = sectors of 1 * 512 = 512 bytes Sector size (logical/physical): 512 bytes / 512 bytes I/O size (minimum/optimal): 512 bytes / 512 bytes Disk identifier: 0x00047c7a Device Boot Start End Blocks Id System 2013-07-26-wheezy-raspbian.img1 8192 122879 57344 c W95 FAT32 (LBA) 2013-07-26-wheezy-raspbian.img2 122880 3788799 1832960 83 Linux
Sector sizeかUnitsあたりの値とStartの値より、512 * 122880 = 62914560です。変わることはなさそうですが、OSイメージは定期的に更新されるので、念のため。
クロスコンパイラとツールの入手
記事どおりDLします。元のほうはファイルが無いようなので、ミラーから落とします。
$ cd ~/local/opt $ wget http://swap.tsmt.eu/gcc-4.7-linaro-rpi-gnueabihf.tbz $ tar xf gcc-4.7-linaro-rpi-gnueabihf.tbz
名前からしてlinaro版のクロスコンパイラだと思われるので、このあたりから入れたものでも大丈夫なんじゃなかろうかとは思いますが。
また、これとは別にツール類をDLします。
$ git clone git://gitorious.org/cross-compile-tools/cross-compile-tools.git
httpの場合は「http://git.gitorious.org/cross-compile-tools/cross-compile-tools.git」です。
Qtソースの取得
gitでQtのソースを取得して、スクリプトで初期化します。
$ cd ~/raspi/Projects $ git clone git://gitorious.org/qt/qt5.git #httpはhttp://git.gitorious.org/qt/qt5.git $ cd qt5 $ ./init-repository
こちらもhttpの場合、微妙にURLが異なるので注意。
元記事ではこの後パッチを当てますが、今のリポジトリではコンフリクトを起こします。同じような修正が適用されているような...けど完全には一致して無いし...といった感じなんですが、当てなくても動作するので無視しときます。
ビルドとインストール
まず、スクリプトを実行して、環境を整えます。
$ cd ~/local/opt/cross-compile-tools $ sudo ./fixQualifiedLibraryPaths /mnt/rasp-pi-rootfs/ ~/local/opt/gcc-4.7-linaro-rpi-gnueabihf/bin/arm-linux-gnueabihf-gcc
そして、qtbaseをビルドします。まずディレクトリに入って、git branchでソースの状態を確認。stableやmasterで無い場合は、checkoutしておきます。
$ cd ~/raspi/Projects/qt5/qtbase * (no branch) stable $ git checkout stable
で、configure。長いですが、コンパイラのパスに注意して実行し、make。
$ ./configure -opengl es2 -device linux-rasp-pi-g++ -device-option CROSS_COMPILE=~/local/opt/gcc-4.7-linaro-rpi-gnueabihf/bin/arm-linux-gnueabihf- -sysroot /mnt/rasp-pi-rootfs -opensource -confirm-license -optimized-qmake -reduce-relocations -reduce-exports -release -make libs -prefix /usr/local/qt5pi $ make -j2 $ sudo make install
これで以降に必要なqmakeなどがローカルにビルド、インストールされ、Raspberry Piのイメージをマウントしたところに、ビルドしたライブラリがインストールされます。
qtbaseの後は、個別にライブラリをビルドしていきます。記事を参考に順番にビルドしていきます。なんかソースが無いやつがあったりしますが。
- qtimageformats
- qtsvg
- qtjsbackend
- qtscript
- qtxmlpatterns
- qtdeclarative
- qtsensors
- qt3d (ソースなし)
- qtgraphicaleffects
- qtjsondb (ソースなし)
- qtlocation (ソースなし)
- qtdocgallery (ソースなし)
- qtquickcontrols (Qt QuickのControlsとLayouts)
Qt 5.1から入ったQt QuickのControlsなどはqtquickcontrolsのビルドが必要なため追加でビルドします。
ビルドの手順はqmakeからのmakeですが、qtbase同様にgit branchでソースの状態を確認してからビルドを行います。
$ cd qtimageformats $ git checkout stable # stableで無い場合 $ /usr/local/qt5pi/bin/qmake . $ make -j2 $ sudo make install
SDカードへ書き込み
ビルドが完了したらイメージをSDカードに書き込みます。
$ sudo umount /dev/sdX* # sdXはdfかdmesgで調べて、適宜書き換え $ cd ~/raspi/Images $ sync;sudo umount /mnt/rasp-pi-rootfs $ sudo dd bs=1M if=2013-07-26-wheezy-raspbian.img of=/dev/sdX; sync # sdXは適宜書き換え
完了後、
SDカードの準備はこれで完了したので、後は通常通り起動します。初期の2GBだと開発時の容量が足りなくなっていくので容量は拡張したほうがよいです。
いったんRaspberry Piで起動してパーティションの容量を拡張し、PC側でddを使ってイメージを保存。そのイメージをrasp-pi-rootfsにマウントして使うようにするとよいです。
Boost.Testの利用で変な風にはまる
Boost.Testを使い始めようと試していたら、次のエラーが出て、詰まっていました。
LINK : fatal error LNK1561: エントリー ポイントを定義しなければなりません。
ソースは次のとおりの簡単なもの。
#define BOOST_TEST_MODULE unit_test #include <boost/test/unit_test.hpp> BOOST_AUTO_TEST_SUITE(suite); BOOST_AUTO_TEST_CASE(test) { BOOST_CHECK_EQUAL(2 + 2, 5); } BOOST_AUTO_TEST_SUITE_END();
そのまんま、main関数が無いよってことなんですが、不要なはずだし、念のため定義してやると今度はテストが実行されない。
同じコードでMacは問題なし。staticだdynamicだって違いがあるので、一応dllを作ってみたらそちらは問題なし。ただ、MacではNGって話はあっても、Windowsではってのは無かったので、納得いかなかったのですが、別件でVisual Studioの設定を変えたら、突然動作するようになったり。
で、改めて見比べたら、次のリンカー(のサブシステム)の設定が空に原因だったことが判明(下は修正したもの)。
ただ、これ、そもそもVisual Studioの既定のテンプレートからコンソールアプリを作った場合は問題ない設定になっています。空になっていたのは、私が自前で用意したカスタムウィザードから作った影響でして。
カスタムウィザードはデフォルトでほとんど設定が入らないので、明示的に指定する必要があるんだけど、種々の理由からすべてを設定するのは結構難しい。それで、必要最低限(と思われる)設定で取り回していたのだけれど、今回はそれが仇となりました。
Boost.Logはとりあえずこう使ってみる。
これまでの記事でいろいろ書いてきましたが、全部まとめて、とりあえずこんな設定で使ってみようと思っています。問題が出たらつどつど。
- コンソールに出力する
- Visual Studioに出力する
- ファイルに出力する
- ログファイルはローテーションの設定をする
- 日付でローテーションする
- 出力フォルダが一定容量に達したら、古いファイルを消す
- フォーマットはタブ区切りとし、Excelへの貼り付けを考慮する
#include <boost/make_shared.hpp> #include <boost/shared_ptr.hpp> #ifdef __APPLE__ #define BOOST_LOG_DYN_LINK #endif #include <boost/log/expressions.hpp> #include <boost/log/sinks.hpp> #include <boost/log/sinks/debug_output_backend.hpp> #include <boost/log/support/date_time.hpp> #include <boost/log/utility/empty_deleter.hpp> #include <boost/log/utility/setup/file.hpp> #include <boost/log/utility/setup/common_attributes.hpp> #include <boost/log/trivial.hpp> int main() { using namespace boost; namespace logging = boost::log; namespace expr = boost::log::expressions; namespace sinks = boost::log::sinks; namespace keywords = boost::log::keywords; #ifdef _WIN32 { // for Visual Studio shared_ptr<sinks::debug_output_backend> backend = make_shared<sinks::debug_output_backend>(); typedef sinks::synchronous_sink<sinks::debug_output_backend> sink_t; shared_ptr<sink_t> sink(new sink_t(backend)); sink->set_formatter( expr::format("%1%\t%2%\t%3%\n") % expr::format_date_time<posix_time::ptime>( "TimeStamp", "%H:%M:%S") % logging::trivial::severity % expr::message ); logging::core::get()->add_sink(sink); } #endif { // for std::clog shared_ptr<sinks::text_ostream_backend> backend = make_shared<sinks::text_ostream_backend>(); backend->add_stream(shared_ptr<std::ostream>( &std::clog, logging::empty_deleter())); backend->auto_flush(true); typedef sinks::synchronous_sink<sinks::text_ostream_backend> sink_t; shared_ptr<sink_t> sink(new sink_t(backend)); sink->set_formatter( expr::format("%1% [%2%] %3%") % expr::format_date_time<posix_time::ptime>( "TimeStamp", "%Y-%m-%d %H:%M:%S") % logging::trivial::severity % expr::message ); logging::core::get()->add_sink(sink); } { // for file logging::add_file_log( keywords::auto_flush = true, keywords::open_mode = std::ios::app, keywords::target = "log", keywords::file_name = "log/app_%Y%m%d.log", keywords::time_based_rotation = sinks::file::rotation_at_time_point(0, 0, 0), keywords::max_size = 10 * 1024 * 1024, keywords::format = expr::format("%1%\t%2%\t%3%") % expr::format_date_time<posix_time::ptime>( "TimeStamp", "%Y-%m-%d %H:%M:%S") % logging::trivial::severity % expr::message ); } logging::add_common_attributes(); #ifdef _DEBUG // do nothing #else logging::core::get()->set_filter( logging::trivial::severity >= logging::trivial::info ); #endif BOOST_LOG_TRIVIAL(trace) << "trace message"; BOOST_LOG_TRIVIAL(debug) << "debug"; BOOST_LOG_TRIVIAL(info) << "info message"; BOOST_LOG_TRIVIAL(warning) << "warning message"; BOOST_LOG_TRIVIAL(error) << "error message"; BOOST_LOG_TRIVIAL(fatal) << "fatal message"; logging::core::get()->remove_all_sinks(); }
Boost 1.54.0のLogをMac 10.7 (Xcode 4)でつかう。
10.8、10.9でもXcode 4系なら多分同じ。
注意点は2つ
- clangでビルドする
- dylibを使う
clangでビルド
環境は次の通り
GCC(引数指定無し)でビルドすると、途中で失敗して、例えばlibboost_log_setup.dylibなどはバイナリすらできません。また、プロジェクトで使用しようとした場合、Boost.Logのヘッダファイルがコンパイルできず、リンクにすら到達しませんので、とにもかくにもclangでビルドします。
clangはXcodeのPreferences -> DownloadからCommand Line Toolsをインストールして、最新にしておきます。
clangでのビルドは次のとおり。
$ ./bootstrap.sh --with-toolset=clang $ ./b2 toolset=clang cxxflags="-std=c++11 -stdlib=libc++" linkflags="-stdlib=libc++" threading=multi --with-log
dylibでリンク
clangでビルドすれば、Boost.Logを使用したソースがコンパイルできますが、今度はリンク時にあらかた関数類が見当たらなくエラーとなります。
それで、いろいろ調べていたのですが、Boost.Testの記事でにたような内容を見まして、もしや...と試したら通りました。
Boost.Logなので、#defineはBOOST_LOG_DYN_LINK。
#define BOOST_LOG_DYN_LINK #include <boost/log/core.hpp> #include <boost/log/expressions.hpp> #include <boost/log/sinks.hpp> #include <boost/log/sinks/debug_output_backend.hpp> #include <boost/log/support/date_time.hpp> #include <boost/log/trivial.hpp> #include <boost/log/utility/empty_deleter.hpp> #include <boost/log/utility/setup/common_attributes.hpp> #include <boost/log/utility/setup/file.hpp>
ただ、Macでdylibを使うと、install_nameが問題になってくるので、こっちの記事に繋がる訳です。
Macでまったく使えないのかと焦りましたが、これでなんとかなりそうです。
Boost.Logでファイルに出力する
Log出力するならやっぱりファイルに出力したいので、ファイル出力の設定をします。Log用のライブラリを使うなら、ログローテーションを活用しない手は無いですね。
出力用のbackendはtext_file_backendになるので、既定どおりbackend -> sink -> loggerに設定の流れで組んでもよいですが、TRIVIALで取り回す場合、次のメソッドを使うのが簡単です。
- boost::log::add_file_log
backendやsinkの準備の必要が無く、ファイル名や出力フォーマット、そしてログローテーションの設定を一気に行うことができます。
boost::log::add_file_log
サポート範囲内でなるたけ好みの出力が得られるように設定します。
- ファイル名は日付
- 日付が変わった段階で別ファイルに
- 一定の容量になったら、古いものから消していく
logging::add_file_log( keywords::auto_flush = true, keywords::open_mode = std::ios::app, keywords::target = "log", keywords::file_name = "log/app_%Y%m%d.log", keywords::time_based_rotation = sinks::file::rotation_at_time_point(0, 0, 0), keywords::max_size = 10 * 1024 * 1024, keywords::format = expr::format("%1%\t%2%\t%3%") % expr::format_date_time<posix_time::ptime>( "TimeStamp", "%Y-%m-%d %H:%M:%S") % logging::trivial::severity % expr::message ); ... logging::core::get()->remove_all_sinks();
ちっと、出力フォルダ、パス、サイズが適当ですが。
最後のremove_all_sinksは呼び出しておかないと、main関数を抜けた後にプログラムが落ちる場合があります。
backendの破棄時にローテーションが行われるのですが、logging::coreは(たぶん)グローバル空間に存在するため、main関数を抜けた後に処理されます。ところが、このタイミングだとローテーションに必要なインスタンスがすでに開放されているらしく、main関数を抜けて、ローテーションが実行されるタイミングでプログラムが不正終了する場合がありました。
そのため、明示的にbackendの破棄処理が実行されるように細工します。ただ、sinkやbackendをいちいち保持して...というのも手軽さが薄れるので、main関数を抜ける間際に、remove_all_sinksを呼び出し、ローテーションの実行と、sinkの登録解除を行います。
その他こまごまと
ローテーションの動作など、こまごま調べて気になったものがいくつかあるので、メモしておきます。
- ファイル数でのローテーションは無い
- file_nameで指定された場所に書き出され、ローテーションのタイミングでtargetに集められる
- %Nで、連番をつけた場合、起動毎にインクリメントされる
特に2つめの動きは同名ファイルへの追記処理や、ローテーションの動作などに影響するので、動作を把握しておいたほうがよいです。
これまでの処理と組み合わせる
これまで組んだ、コンソールへの出力、Visual Studioへの出力と組み合わせます。ここまでくると、とりあえず使うのに不便はないかなーといった感じです。
#include <boost/make_shared.hpp> #include <boost/shared_ptr.hpp> #include <boost/log/expressions.hpp> #include <boost/log/sinks.hpp> #include <boost/log/sinks/debug_output_backend.hpp> #include <boost/log/support/date_time.hpp> #include <boost/log/utility/empty_deleter.hpp> #include <boost/log/utility/setup/file.hpp> #include <boost/log/utility/setup/common_attributes.hpp> #include <boost/log/trivial.hpp> int main() { using namespace boost; namespace logging = boost::log; namespace expr = boost::log::expressions; namespace sinks = boost::log::sinks; namespace keywords = boost::log::keywords; { // for Visual Studio shared_ptr<sinks::debug_output_backend> backend = make_shared<sinks::debug_output_backend>(); typedef sinks::synchronous_sink<sinks::debug_output_backend> sink_t; shared_ptr<sink_t> sink(new sink_t(backend)); sink->set_formatter( expr::format("%1%\t%2%\t%3%\n") % expr::format_date_time<posix_time::ptime>( "TimeStamp", "%H:%M:%S") % logging::trivial::severity % expr::message ); logging::core::get()->add_sink(sink); } { // for std::clog shared_ptr<sinks::text_ostream_backend> backend = make_shared<sinks::text_ostream_backend>(); backend->add_stream(shared_ptr<std::ostream>( &std::clog, logging::empty_deleter())); backend->auto_flush(true); typedef sinks::synchronous_sink<sinks::text_ostream_backend> sink_t; shared_ptr<sink_t> sink(new sink_t(backend)); sink->set_formatter( expr::format("%1% [%2%] %3%") % expr::format_date_time<posix_time::ptime>( "TimeStamp", "%Y-%m-%d %H:%M:%S") % logging::trivial::severity % expr::message ); logging::core::get()->add_sink(sink); } { // for file logging::add_file_log( keywords::auto_flush = true, keywords::open_mode = std::ios::app, keywords::target = "log", keywords::file_name = "log/app_%Y%m%d.log", keywords::time_based_rotation = sinks::file::rotation_at_time_point(0, 0, 0), keywords::max_size = 10 * 1024 * 1024, keywords::format = expr::format("%1%\t%2%\t%3%") % expr::format_date_time<posix_time::ptime>( "TimeStamp", "%Y-%m-%d %H:%M:%S") % logging::trivial::severity % expr::message ); } logging::add_common_attributes(); #ifdef _DEBUG // do nothing #else logging::core::get()->set_filter( logging::trivial::severity >= logging::trivial::info ); #endif BOOST_LOG_TRIVIAL(trace) << "trace message"; BOOST_LOG_TRIVIAL(debug) << "debug"; BOOST_LOG_TRIVIAL(info) << "info message"; BOOST_LOG_TRIVIAL(warning) << "warning message"; BOOST_LOG_TRIVIAL(error) << "error message"; BOOST_LOG_TRIVIAL(fatal) << "fatal message"; logging::core::get()->remove_all_sinks(); }