Rustコンパイラ組込みのlintについて調べる
はじめに
Rustコンパイラであるrustc
には組込みのlintがあります。
Rustのlintツールでは、clippyが有名ですが、rustc組込みの方もソースコードの改善に役立ちます。
7月の技術書同人誌博覧会のネタ探しの一環だったのですが、思ったより世の中の役に立ちそうだったので、ドラフトを公開します。 原稿落とさなければ、「組込み / ベアメタルRustテクニック集」的な薄い本を出す予定です。
ちなみにここに書いている内容は、全てThe rustc bookに書いてあります。
lint
lint
はソースコードをコンパイラより厳密なルールに則り、検査するためのツールです。
Rustコンパイラには、様々なlintルールが組み込まれています。
ソースコードをコンパイルする時、自動的にlintによる検査が行われます。
プロジェクトの運用ルールに合わせて、適切なlintルールを設定することで、ソースコードの品質をより向上できるでしょう。
lintレベル
rustcのlintレベルは、4つに分類されます。
- allow (許可)
- warn (警告)
- deny (拒絶)
- forbid (禁止)
各lintルールには、デフォルトのlintレベルがあり、コンパイルオプションかアトリビュートで上書きできるようになっています。 まず、lintレベルについて説明します。
allow (許可)
lintルールを適用しません。 例えば、次のコードをコンパイルしても、警告は発生しません。
pub fn foo() {}
$ rustc lib.rs --crate-type=lib
しかし、このコードはmissing_docs
ルールを違反しています。
lintレベルを上書きしてコンパイルすると、コンパイルエラーになったり、警告が出力されるようになります。
warn (警告)
lintルール違反があった場合、警告を表示します。
fn main() { let x = 5; }
このコードは次の警告が報告されます。
warning: unused variable: `x` --> src/main.rs:2:9 | 2 | let x = 5; | ^ help: consider prefixing with an underscore: `_x` | = note: #[warn(unused_variables)] on by default
deny (拒絶)
lintルール違反があった場合、コンパイルエラーになります。
fn main() { 100u8 << 10; }
このコードは、exceeding_bitshifts
ルールに違反しており、コンパイルエラーになります。
error: attempt to shift left with overflow --> src/main.rs:2:5 | 2 | 100u8 << 10; | ^^^^^^^^^^^ | = note: #[deny(exceeding_bitshifts)] on by default
forbid (禁止)
lintルール違反があった場合、コンパイルエラーになります。 forbidは、denyより強いレベルで、上書きができません。
下のコードは、アトリビュートでmissing_docs
ルールをallowに上書きしています。
#![allow(missing_docs)] pub fn foo() {}
missing_dogs
ルールを、denyレベルに設定してコンパイルすると、このコードはコンパイルできます。
$ rustc lib.rs --crate-type=lib -D missing-docs
一方、forbidレベルに設定してコンパイルすると、コンパイルエラーになります。
$ rustc lib.rs --crate-type=lib -F missing-docs error[E0453]: allow(missing_docs) overruled by outer forbid(missing_docs) --> lib.rs:1:10 | 1 | #![allow(missing_docs)] | ^^^^^^^^^^^^ overruled by previous forbid | = note: `forbid` lint level was set on command line
lintレベルの設定方法
コンパイラフラグで設定
コンパイルオプションで、-A
, -W
, -D
, -F
のいずれかを指定して、lintレベルを設定できます。
$ rustc lib.rs --crate-type=lib -W missing-docs
もちろん、複数のフラグを同時に設定することも可能です。
$ rustc lib.rs --crate-type=lib -D missing-docs -A unused-variables
Cargoの設定ファイル内で、lintレベルを設定することも可能です。
$ cat .cargo/config
[build] rustflags = ["-D", "unsafe-code"]
アトリビュートで設定
ソースコード内のアトリビュートで、allow
, warn
, deny
, forbid
のいずれかを指定して、lintレベルを設定できます。
$ cat lib.rs #![warn(missing_docs)] pub fn foo() {}
1つのアトリビュートに、複数のlintルールを指定できます。
#![warn(missing_docs, unused_variables)] fn main() { pub fn foo() {} }
複数のアトリビュートを組み合わせて使うこともできます。
#![warn(missing_docs)] #![deny(unused_variables)] pub fn foo() {}
lintルール
次のコマンドでlintルールと、デフォルトレベルの一覧が取得できます。
$ rustc -W help
デフォルトレベルごとに、サンプルコード付きでlintルールが説明されています。
知られざるlintルールたち
ちらっと目について、へー、と思ったルールを独断と偏見で紹介します。
ちょっとマニアック
- unsafe-code:
unsafe
ブロックがある。デフォルトallow。unsafe
ブロックを使わせたくないアイツの.cargo/config
にこっそりどうぞ。 - illegal-floating-point-literal-pattern: パターン内で浮動小数点が使われている。デフォルトwarn。
- no-mangle-generic-items: ジェネリック関数にno_mangleアトリビュートをつける。デフォルトwarn。
warn
なんだ?強制的にマングルされるってことかしら?
自分が知らんかった枠
- variant-size-differences: enumのヴァリアントのサイズが違いすぎる。デフォルトallow。
- improper-ctypes:
extern "C"
の中などFFIでRustの型を使う。デフォルトwarn。 - no-mangle-const-items: シンボルを持たない
const
変数にno_mangleアトリビュートをつける。デフォルトdeny。言われると納得。やったことないけど。
RustのRTFM (Real Time For the Masses)を試してみる⑤
はじめに
組込みRust界の伝説japaric
氏が実装しているReal Time For the Masses (RTFM) framework for ARM Cortex-M microcontrollersを試してみます。
サンプルコードをマクロ展開して心が折れそうです。 とりあえず、当面サンプルコードの表面をなぞります。
タスク
RTFMでは、割り込みと例外をハードウェアタスクとして扱います。 RTFMでは、ソフトウェアタスクも提供しています。
ソフトウェアタスクは、割り込みハンドラから、優先度を指定してディスパッチできます。
ソフトウェアタスクをディスパッチする割り込みハンドラは、extern
ブロックの中で宣言しておく必要があります。
ソフトウェアタスクになる関数は、task
アトリビュートをつけます。
ソフトウェアタスクをspawnする関数は、contextアトリビュートにspawn
引数をつけます。
#[app(device = lm3s6965)] const APP: () = { #[init(spawn = [foo])] fn init() { spawn.foo().unwrap(); } #[task(spawn = [bar, baz])] fn foo() { hprintln!("foo").unwrap(); // spawns `bar` onto the task scheduler // `foo` and `bar` have the same priority so `bar` will not run until // after `foo` terminates spawn.bar().unwrap(); // spawns `baz` onto the task scheduler // `baz` has higher priority than `foo` so it immediately preempts `foo` spawn.baz().unwrap(); } #[task] fn bar() { hprintln!("bar").unwrap(); debug::exit(debug::EXIT_SUCCESS); } #[task(priority = 2)] fn baz() { hprintln!("baz").unwrap(); } // Interrupt handlers used to dispatch software tasks extern "C" { fn UART0(); fn UART1(); } };
spawn
の結果は、Result
で返ってきます。
これは、タスクが起動できる数 (capacity) が指定できるようになっており、その数以上のタスクを起動できないからです。
capacityはデフォルトでは、1になっています。
#[interrupt(spawn = [foo, bar])] fn UART0() { spawn.foo(0).unwrap(); spawn.foo(1).unwrap(); spawn.foo(2).unwrap(); spawn.foo(3).unwrap(); spawn.bar().unwrap(); // runtime error spawn.bar().unwrap(); } #[task(capacity = 4)] fn foo(x: u32) { hprintln!("foo({})", x).unwrap(); } #[task] fn bar() { hprintln!("bar").unwrap(); debug::exit(debug::EXIT_SUCCESS); }
ソフトウェアタスクには、メッセージを渡すことができます。 メッセージは、タスクの引数として、シグネチャに含める必要があります。
#[app(device = lm3s6965)] const APP: () = { #[init(spawn = [foo])] fn init() { spawn.foo(/* no message */).unwrap(); } #[task(spawn = [bar])] fn foo() { static mut COUNT: u32 = 0; hprintln!("foo").unwrap(); spawn.bar(*COUNT).unwrap(); *COUNT += 1; } #[task(spawn = [baz])] fn bar(x: u32) { hprintln!("bar({})", x).unwrap(); spawn.baz(x + 1, x + 2).unwrap(); } #[task(spawn = [foo])] fn baz(x: u32, y: u32) { hprintln!("baz({}, {})", x, y).unwrap(); if x + y > 4 { debug::exit(debug::EXIT_SUCCESS); } spawn.foo().unwrap(); } }
foo
のコードで、static mut COUNT
を使用する際、デリファレンスしているのが気になりました。
fn foo(c: foo::Context) { static mut COUNT: u32 = 0; // ... c.spawn.bar(*COUNT).unwrap(); *COUNT += 1; }
どうも、static mut
を参照にマクロ展開しているようです。
次の手順でマクロ展開したコードを確認しました。
$ cargo rustc --example message -- -Z unstable-options --pretty=expanded > message_expanded.rs # フォーマットしないとツラいです $ rustfmt message_expanded.rs
foo
は次のように展開されます。
#[allow(non_snake_case)] fn foo(__locals: foo::Locals, c: foo::Context) { use rtfm::Mutex as _; let COUNT = __locals.COUNT; ::cortex_m_semihosting::export::hstdout_str("foo\n").unwrap(); c.spawn.bar(*COUNT).unwrap(); *COUNT += 1; }
foo::Locals
という型で引数を受け取っており、そこからCOUNTを取得しています。
foo::Locals
は、次の通りです。
#[allow(non_snake_case)] #[doc(hidden)] pub struct fooLocals<'a> { COUNT: &'a mut u32, __marker__: core::marker::PhantomData<&'a mut ()>, }
COUNT
が参照になっていますね。
試しに、COUNT
をデリファレンスせずに使うと、次のコンパイルエラーになります。
error[E0308]: mismatched types --> examples/message.rs:25:21 | 25 | c.spawn.bar(COUNT).unwrap(); | ^^^^^ | | | expected u32, found &mut u32 | help: consider dereferencing the borrow: `*COUNT` | = note: expected type `u32` found type `&mut u32`
いやー、知らないと意味がわからないですよ、これ。
RustのRTFM (Real Time For the Masses)を試してみる④
はじめに
組込みRust界の伝説japaric
氏が実装しているReal Time For the Masses (RTFM) framework for ARM Cortex-M microcontrollersを試してみます。
面白いので、引き続き触っていきます。
リソース管理続き
RTFMのstatic
変数は、通常のstatic
変数と異なり、実行時に初期化できます。
このように実行時に初期化するリソースをlate resourcesと呼んでいます。
補足すると、lazy_static
とは、また異なる仕組みのようです。Optionで包んで云々、ということはやっていません。
リソースの初期化は、init
関数で行う必要があり、init
関数は、初期化したリソースを戻り値にしなければなりません。
何を言っているのかわからないと思うので、サンプルコードを見てみましょう。
use heapless::{ consts::*, spsc::{Consumer, Producer, Queue}, }; #[app(device = lm3s6965)] const APP: () = { // Late resources `()`を初期値にします static mut P: Producer<'static, u32, U4> = (); static mut C: Consumer<'static, u32, U4> = (); #[init] fn init() -> init::LateResources { // NOTE: we use `Option` here to work around the lack of // a stable `const` constructor static mut Q: Option<Queue<u32, U4>> = None; *Q = Some(Queue::new()); let (p, c) = Q.as_mut().unwrap().split(); // late resourcesを初期化して、関数の戻り値とします init::LateResources { P: p, C: c } } #[idle(resources = [C])] fn idle() -> ! { loop { if let Some(byte) = resources.C.dequeue() { hprintln!("received message: {}", byte).unwrap(); debug::exit(debug::EXIT_SUCCESS); } else { rtfm::pend(Interrupt::UART0); } } } #[interrupt(resources = [P])] fn UART0() { resources.P.enqueue(42).unwrap(); } };
まず、()
で初期化されたlate resourcesを宣言します。
static mut P: Producer<'static, u32, U4> = (); static mut C: Consumer<'static, u32, U4> = ();
次に、init
関数のシグネチャが、init::LateResources
を返すものに変わります。
このinit::LateResources
は、late resourcesのP
とC
とのタプルです。
#[init] fn init() -> init::LateResources {
idle
はC、UART0
はPを、それぞれ専有して利用するため、ロックなしでリソースを利用することができます。
このサンプルでは、`heaphessクレートのqueueが使われています。
このheaplessクレートは、文字通り、ヒープがない環境でコレクションを使うためのクレートです。
queueは、single producer / single consumerのモデルになっています。
このqueueの初期化をinit
で行い、ProducerとConsumerのオブジェクトを共有リソースとして管理することで、うまいこと安全な機構になっているように見えます。
リソースがただのstatic
で、可変参照にならない場合も、ロックなしでリソースにアクセスできます。
これは、late resourcesにも適用されます。
#[app(device = lm3s6965)] const APP: () = { static KEY: u32 = (); #[init] fn init() -> init::LateResources { rtfm::pend(Interrupt::UART0); rtfm::pend(Interrupt::UART1); init::LateResources { KEY: 0xdeadbeef } } #[interrupt(resources = [KEY])] fn UART0() { hprintln!("UART0(KEY = {:#x})", resources.KEY).unwrap(); debug::exit(debug::EXIT_SUCCESS); } #[interrupt(priority = 2, resources = [KEY])] fn UART1() { hprintln!("UART1(KEY = {:#x})", resources.KEY).unwrap(); } };
RustのRTFM (Real Time For the Masses)を試してみる③
はじめに
組込みRust界の伝説japaric
氏が実装しているReal Time For the Masses (RTFM) framework for ARM Cortex-M microcontrollersを試してみます。
軽い気持ちで触り始めたのですが、読み進めるうちに、手続きマクロでゴリゴリ静的解析している変態実装であることがわかってきました。 少しずつフレームワークの実装にも突っ込んでいきたいですね。
リソース管理
cortex-m-rt
クレートを使用する場合、割り込みとメイン関数間でのデータ共有に制限があります。
常に、全ての割り込みを無効化するcortex_m::interrupt::Mutex
を使わなければなりません。
しかし、全ての割り込みを無効化することは、常に求められる条件ではありません。
例えば、2つの割り込みハンドラがデータを共有する場合、両者の優先度が同じで、プリエンプションが発生しないとすると、ロックは不要です。
RTFMでは、ソースコードを静的に解析することで、不要なロックをせずに、共有データにアクセスできるようになっています。
このような解析が可能な理由は、app
アトリビュート内にアプリケーションの実装を、全て書くためです。
また、RTFMでは、動的な割り込み優先度の変更をサポートしていません。 そのため、全ての割り込みハンドラ間の優先度は静的に決定します。 これがうまいこと生きており、ある共有データを使用する割り込みハンドラ同士で、最も優先度の高いハンドラはロックを取得しなくても共有データにアクセスできます。
そろそろサンプルコードを見てみましょう。
共有データは、static
変数として宣言します。
#[app(device = lm3s6965)] const APP: () = { // A resource static mut SHARED: u32 = 0; #[init] fn init() { rtfm::pend(Interrupt::UART0); rtfm::pend(Interrupt::UART1); } #[idle] fn idle() -> ! { debug::exit(debug::EXIT_SUCCESS); // error: `SHARED` can't be accessed from this context // SHARED += 1; loop {} } // `SHARED` can be access from this context #[interrupt(resources = [SHARED])] fn UART0() { *resources.SHARED += 1; hprintln!("UART0: SHARED = {}", resources.SHARED).unwrap(); } // `SHARED` can be access from this context #[interrupt(resources = [SHARED])] fn UART1() { *resources.SHARED += 1; hprintln!("UART1: SHARED = {}", resources.SHARED).unwrap(); } };
共有データにアクセスする関数は、アトリビュート内でresources = [<変数名>]
を引数にします。
ここで、2つの割り込みハンドラUART0
とUART1
は共に共有データSHARED
を使用します。
2つの割り込みは、割り込み優先度が同じで、プリエンプションも発生しません。
そのため、2つの割り込みハンドラでは、Mutexなしで共有データにアクセスできます。
割り込み優先度を設定する場合、同じ共有データを使用する割り込みハンドラのうち、優先度が最も高いハンドラだけはロックが不要です。
//! examples/lock.rs #![deny(unsafe_code)] #![deny(warnings)] #![no_main] #![no_std] extern crate panic_semihosting; use cortex_m_semihosting::{debug, hprintln}; use lm3s6965::Interrupt; #[rtfm::app(device = lm3s6965)] const APP: () = { static mut SHARED: u32 = 0; #[init] fn init(_: init::Context) { rtfm::pend(Interrupt::GPIOA); } // when omitted priority is assumed to be `1` #[interrupt(resources = [SHARED])] fn GPIOA(mut c: GPIOA::Context) { hprintln!("A").unwrap(); // the lower priority task requires a critical section to access the data c.resources.SHARED.lock(|shared| { // data can only be modified within this critical section (closure) *shared += 1; // GPIOB will *not* run right now due to the critical section rtfm::pend(Interrupt::GPIOB); hprintln!("B - SHARED = {}", *shared).unwrap(); // GPIOC does not contend for `SHARED` so it's allowed to run now rtfm::pend(Interrupt::GPIOC); }); // critical section is over: GPIOB can now start hprintln!("E").unwrap(); debug::exit(debug::EXIT_SUCCESS); } #[interrupt(priority = 2, resources = [SHARED])] fn GPIOB(mut c: GPIOB::Context) { // the higher priority task does *not* need a critical section *c.resources.SHARED += 1; hprintln!("D - SHARED = {}", *c.resources.SHARED).unwrap(); } #[interrupt(priority = 3)] fn GPIOC(_: GPIOC::Context) { hprintln!("C").unwrap(); } };
ドキュメントによると、このプログラムの実行結果は、次のようになるようです。
$ cargo run --example lock A B - SHARED = 1 C D - SHARED = 2 E
note: 手元の環境で実行すると、次の結果になります。要調査。レポジトリのCIは通っているようなので、手元環境の問題?
A D - SHARED = 2 B - SHARED = 2 C E
GPIOAの共有データアクセスは、クリティカルセクションのMutexを取得しており、割り込み禁止状態で行われます。
少し上記プログラムを改変して、どうなるか試してみます。
const APP: () = { static mut SHARED: u32 = 0; // ... #[interrupt(resources = [SHARED])] fn GPIOA() { hprintln!("A").unwrap(); // GPIOBと同じ方法でアクセスしてみます *c.resources.SHARED += 1; hprintln!("E").unwrap(); debug::exit(debug::EXIT_SUCCESS); } #[interrupt(priority = 2, resources = [SHARED])] fn GPIOB(mut c: GPIOB::Context) { *c.resources.SHARED += 1; hprintln!("D - SHARED = {}", *c.resources.SHARED).unwrap(); } // ... };
ビルドすると次のコンパイルエラーになります。
$ cargo run --example lock error[E0614]: type `resources::SHARED<'_>` cannot be dereferenced --> examples/lock.rs:41:9 | 41 | *c.resources.SHARED += 1; | ^^^^^^^^^^^^^^^^^ error: aborting due to previous error For more information about this error, try `rustc --explain E0614`. error: Could not compile `cortex-m-rtfm`. To learn more, run the command again with --verbose.
どうも無名の型になっているようで、詳細がつかみにくいですが、GPIOA()
とGPIOB
()とで、
resource::SHARED`の型が違う、ということがわかります。
RustのRTFM (Real Time For the Masses)を試してみる②
はじめに
組込みRust界の伝説japaric
氏が実装しているReal Time For the Masses (RTFM) framework for ARM Cortex-M microcontrollersを試してみます。
前回変なところで切ってしまったため、今回は軽めです。
idle
タスク
idle
タスクは、init
タスクの後に実行することができます。
init
タスクと異なり、割り込みが有効化された状態で実行します。
関数シグネチャは[unsafe] fn() - > !
であり、関数が戻ることは許されません。
下記のようにプログラムを書くことで、init
の後にidle
を実行します。
#[app(device = lm3s6965)] const APP: () = { #[init] fn init() { hprintln!("init").unwrap(); } #[idle] fn idle() -> ! { static mut X: u32 = 0; // Safe access to local `static mut` variable let _x: &'static mut u32 = X; hprintln!("idle").unwrap(); debug::exit(debug::EXIT_SUCCESS); loop {} } };
interrupt
/ exception
cortex-m-rt
クレートを利用する時と同じ方法で、interrupt
とexception
を使うことができます。
#[app(device = lm3s6965)] const APP: () = { #[init] fn init() { // Pends the UART0 interrupt but its handler won't run until *after* // `init` returns because interrupts are disabled rtfm::pend(Interrupt::UART0); hprintln!("init").unwrap(); } #[idle] fn idle() -> ! { // interrupts are enabled again; the `UART0` handler runs at this point hprintln!("idle").unwrap(); rtfm::pend(Interrupt::UART0); debug::exit(debug::EXIT_SUCCESS); loop {} } #[interrupt] fn UART0() { static mut TIMES: u32 = 0; // Safe access to local `static mut` variable *TIMES += 1; hprintln!( "UART0 called {} time{}", *TIMES, if *TIMES > 1 { "s" } else { "" } ) .unwrap(); } };
このプログラムを実行すると、2回割り込みをpend
しているので、UART0
が2回呼ばれます。
実行結果
$ cargo run --example interrupt init UART0 called 1 time idle UART0 called 2 times
RustのRTFM (Real Time For the Masses)を試してみる①
はじめに
組込みRust界の伝説japaric
氏が実装しているReal Time For the Masses (RTFM) framework for ARM Cortex-M microcontrollersを試してみます。
RTFM自体は、Real-time for the masses, step 1: Programming API and static priority SRP kernel primitives.という論文が大本のようです。
ドキュメントもしっかり整備されています。見習わないといけないですね。
RTFM brief
ドキュメントの機能一覧を抜粋します。
- タスク管理
- タスク間メッセージ
- タイマキュー
- プリエンプティブマルチタスキング
- 効率的でデータ競合のないメモリ共有
- デッドロックしない実行
- 最小限のスケジューリングオーバーヘッド
- 極めて効率的なメモリ使用
最小限のリアルタイムコアシステム、といった印象を受けます。
initサンプル
qemu-system-arm
があれば、動かせるサンプルが多く用意されています。
QEMUをターゲットに、少しサンプルを動かしてみましょう。
$ git clone https://github.com/japaric/cortex-m-rtfm.git $ cd cortex-m-rtfm $ cargo run --example init Finished dev [unoptimized + debuginfo] target(s) in 0.07s Running `qemu-system-arm -cpu cortex-m3 -machine lm3s6965evb -nographic -semihosting-config enable=on,target=native -kernel target/thumbv7m-none-eabi/debug/examples/init` init # サンプルで出力した文字列
単純に文字を出力するだけのサンプルのようです。 アプリケーションのソースコードを見てみましょう。
ソースコード
#![deny(unsafe_code)] #![deny(warnings)] #![no_main] #![no_std] extern crate panic_semihosting; use cortex_m_semihosting::{debug, hprintln}; use rtfm::app; #[app(device = lm3s6965)] const APP: () = { #[init] fn init() { static mut X: u32 = 0; // Cortex-M peripherals let _core: rtfm::Peripherals = core; // Device specific peripherals let _device: lm3s6965::Peripherals = device; // Safe access to local `static mut` variable let _x: &'static mut u32 = X; hprintln!("init").unwrap(); debug::exit(debug::EXIT_SUCCESS); } };
まず、良いなと思ったのは、最初に#![deny(unsafe_code)]
しています。
組込みRust普及活動をする上で、unsafe
だらけのコードになるのでは?という質問を良く受けますが、アプリケーションレベルでは、unsafeを許さないこのアトリビュートを設定できます。
後は、それだけこのRTFMフレームワークが上手にunsafeを閉じ込めた実装になっている、というのもこの1行からわかります。
見慣れないのは、#[app(device = lm3s6965)]
アトリビュートと、#[init]
アトリビュートです。
#[app(..)]
アトリビュートは、device
引数を使って、svd2rust
で生成されたperipheral access crate (PAC)を指すパスを指定します。
#[app(..)]
アトリビュートは、const itemに適用しなければなりません。これは、nightlyを使わないようにするための制限です。
app
アトリビュートは、init
アトリビュートでマーキングされた初期化関数があることを想定します。
init
アトリビュートは、[unsafe] fn()
をシグネチャに持つ関数につけることができます。
init
関数は、アプリケーションとして実行される最初の関数です。
この関数は、割り込み禁止状態で実行します。
core
とdevice
という変数があり、この変数を通して、Cortex-Mとペリフェラルにアクセスできます。
// Cortex-M peripherals let _core: rtfm::Peripherals = core; // Device specific peripherals let _device: lm3s6965::Peripherals = device;
app
かinit
アトリビュートあたりがマクロ展開されて定義されるのでしょう。
mdbook-epubお試し
はじめに
同人誌を書く環境を模索しています。 Rustが好きなので、Rust製のドキュメントビルダーmdbookも試してみます。
mdbook自体には、PDF出力機能がないため、3rd party pluginであるdbook-epubを試してみます。
インストール
Rust環境はあらかじめ構築しておいて下さい。
cargo install mdbook cargo install mdbook-epub
mdbookプロジェクトのbook.toml
に次の一行を追加します。
[output.epub]
通常通り、mdbookを呼び出します。
$ mdbook build 2019-05-02 20:51:53 [INFO] (mdbook::book): Book building has started 2019-05-02 20:51:53 [INFO] (mdbook::book): Running the epub backend 2019-05-02 20:51:53 [INFO] (mdbook::renderer): Invoking the "epub" renderer
$ ls book 'The Embedonomicon.epub'
htmlではなく、epub
が出力されています。
読んでみる
calibre
というepubリーダー / 編集ソフトを使って、少しCSSをいじりながら、調節してみます。
sudo apt install calibre
意外と悪くない…?