トップ 最新 追記

2019-10-03 [長年日記]

_ Rubyスクリプトをcross-platformなstandaloneバイナリにしようとして諦めた

やりたいこと

  • gem は自由に使いたい
  • なんなら ffi も使いたい
  • Ruby で書いて macOS / Windows で動作させたい
  • Ruby そのもののインストールの手間を掛けさせたくない

きっかけは「GitHub Actions で Windows CI が使えるから、なんかいろいろゴニョゴニョすれば Windows で使う道具を作れるのでは?」

数年前に https://github.com/hone/mruby-cli も話題にあがったみたいなんだけど、確かに mruby 自体はプラットフォームを選ばないが gem 使いたいので除外した。

まとめ

Go をクロスプラットフォームで動く Ruby のように扱えないかなと思ったけど、やはりかなり厳しそうなので、2019年の今なら Ruby スクリプトをクロスプラットフォームで動かすなんてワケないのでは?と探したけど何度目かの挫折をして諦めた話。

  • やはり Windows が鬼門
  • 一時的に頑張る人が出てきても継続しない

もう Ruby のこの状況はずっと変わらないんだろうなぁ。個人的には型とか速度とかよりまず Windows で楽に動いてくれれば業務系の人ともっと仲良くしやすいんだけどなーと思ってるんだけど、もう諦めました。いい加減さすがに学べ、おれ。

※ Node.js でいってもいいんだけど、native extension とか ffi 周りは Ruby の方がだいぶマシだと思っている。

以下は戦いに破れた記録。

loureirorg/rb2exe: Ruby to EXE - Turn ruby scripts into portable executable apps

  • 名前に反してクロスコンパイルもできるっぽいんだけど、
  • Ruby が 2.2.2 固定のまま止まっている

これはさすがに放置だなぁということで捨て。

Enclose.IO: Compiling your application into a single executable

pmq20/ruby-packer: Packing your Ruby application into a single executable.

これは事前に実行環境込みの実行バイナリに固めてしまうので deploy や実行が速いといったメリットを掲げたプロジェクト。結構しっかりいろいろ作り込まれているが、配布自体は2年前に止まってしまっているように見える。

実際にやってみたが、

  • squashfs を使っていて実行時にファイルから require のようなことを行わないっぽい
  • コンパイルはすごく重い
  • クロスコンパイルはできない
  • 実行バイナリはそこそこの大きさで十分な速度で動く
  • Mac については普段から rbenv を使っていることもあり、開発環境も整っているので簡単だったが、Linux は bison がないとか CPPFLAGS="-P" が足りなくて ncurses でコケる現象に悩んだ。あとなんかコンパイルは終わるのに途中途中で error が出ていて怖い。
  • 予想通り Windows が厳しい
    • Visual Studio を入れても nmake がないとか怒られる
    • choco install で頑張る
    • なんか Build Tools みたいなの入れたけど zlib.lib がないと怒られる(コンパイルのログには出てくるんだけど)
    • これらを手作業じゃなくて「CI上で安定して動くようにする」のはだいぶつらそう

ということでこれで全部のプラットフォームを通すのは難しいと思って諦め。

mac で書いて Ruby 入っていない mac に持っていく分には時間さえ掛ければできそうなので、そういう用途に限ればよさそう。まぁいつまで使えるか分からないけど。

larsch/ocra: One-Click Ruby Application Builder

Windows は Windows 専用のやつの方がよいかもと思って試してみた。

  • rubyc と違ってコンパイルは概ねガッチャンコするだけでめちゃくちゃ速い
  • 逆に実行時には固めたものを展開して実行するようで、かなり遅い
  • 単純なスクリプトだと ocra の実行もできあがったスクリプトの実行も簡単
  • ただし実行時にそのまま require などが走るのでそこでファイルが見つからないと怒られる
    • いろいろオプションがあって、何も指定しないと標準ライブラリすらバンドルされない
  • 実行時エラーを潰して潰して頑張ったが openssl.so がないと怒られる
  • どうも .rb や .dll はバンドルするオプションがあるのに .so についてはオプションが存在しないっぽい

ということで手詰まり終了。

Tags: Ruby

2019-10-04 [長年日記]

_ pkgでNode.jsスクリプトを実行バイナリにする

Rubyスクリプトをcross-platformなstandaloneバイナリにしようとして諦めた - あーありがち(2019-10-03)

で Ruby を諦めたので、次は Node.js.

zeit/pkgですごく簡単にできる

zeit/pkg: Package your Node.js project into an executable

試したのは 4.4.0

基本的な使い方は

pkg [-t target] [-o output] <source>

で、必要な stub を自動でダウンロードして依存モジュールも含めて -o で指定した名前の実行バイナリを生成してくれる。

ネットワークが弱い場合はこのダウンロードでイライラするけど、そこが済めば*1実にあっけなく終わる。本当にあっという間。

ただし、通常ではできあがったバイナリはすごくでかい。どれだけ簡単なコードでも 30MB 以上になってしまう。もしかしたら V8 の option でサイズの削減はできるかもしれないけど、そこはまだ試していない。

でもcross-compileはできない

README には cross-compile できると書かれているんだけど、これは間違いだった。macOS 上で Windows ターゲットのコンパイルを試してみて、確かにコンパイル自体は動作はするんだけど、実際にできあがったバイナリを実行することはできなかった。Windows 上で Windows ターゲットのバイナリを作ったら普通に実行できたので、CI のプラットフォームを Windows にして pkg を実行する形でないとダメっぽい。

続きは GitHub Actionsでpkgを使ったNode.js実行バイナリをWindowsを含めてmatrixビルドする - あーありがち(2019-10-05)

localのファイルのrequireはそのままでは相対パスを解決できない

バイナリはできるけど実行すると以下のように怒られる。

Error: Cannot find module 'xxx
Require stack:
- /snapshot/src/index.js
1) If you want to compile the package/file into executable, please pay attention to compilation warnings and specify a literal in 'require' call. 2) If you don't want to compile the package/file into executable and want to 'require' it from filesystem (likely plugin), specify an absolute path in 'require' call using process.cwd() or process.execPath.
   at Function.Module._resolveFilename (internal/modules/cjs/loader.js:608:15)
   at Function.Module._resolveFilename (pkg/prelude/bootstrap.js:1287:46)
   at Function.Module._load (internal/modules/cjs/loader.js:524:27)
   at Module.require (internal/modules/cjs/loader.js:664:19)
   at Module.require (pkg/prelude/bootstrap.js:1166:31)
   at require (internal/modules/cjs/helpers.js:16:16)
   at Object.<anonymous> (/snapshot/src/index.js:0:0)
   at Module._compile (pkg/prelude/bootstrap.js:1261:22)
   at Object.Module._extensions..js (internal/modules/cjs/loader.js:768:10)
   at Module.load (internal/modules/cjs/loader.js:626:32)

解決策としては process.cwd() とか process.execPath ではなく、

https://github.com/zeit/pkg#config

を参考に、package.json に

"pkg": {
  "scripts": "src/**/*.js"
}

みたいなやつを足しておく必要あり。

Tags: Node

*1 環境変数を指定しておくとダウンロードした stub をキャッシュしてくれる


2019-10-05 [長年日記]

_ GitHub Actionsでpkgを使ったNode.js実行バイナリをWindowsを含めてmatrixビルドする

目的

pkgでNode.jsスクリプトを実行バイナリにする - あーありがち(2019-10-04) において、各プラットフォーム上で pkg を叩けば Node.js スクリプトを Windows, macOS, Linux それぞれで動く単体のバイナリにできることは分かった。

今日はこの pkg を使って各 platform 上で build する処理を GitHub Actions で自動化してみたときのメモ。これが完成すれば基本的には Windows 上のちょっとしたツールを、いつもの macOS で、いつもの JavaScript で書いて git push すればオッケーと言える。便利。

※ 現時点では公開できる完成リポジトリはないので、日本語だけ。

対象

CircleCI など YAML で CI の設定をした経験のある人向け。細かい説明は一切しません。

参考

GitHub ActionsでElectronアプリのクロスプラットフォームバイナリを配布する | Web Scratch

メモ

About GitHub Actions - GitHub Help

matrixビルド

複数の環境を指定していくつもビルドを実行することをmatrixビルドと呼ぶらしい。これは GitHub Actions 独特のものではない。

  • strategy で matrix を指定したうえで runs-on は ${{ matrix.os }} で
  • pkg の target にはこの matrix.os をそのまま使うことはできないので、「matrix.os を加工する action」を加える必要がある
環境の課題
  • step の run に複数行与える書き方は Windows 標準の shell の cmd では正しく動かない*1
  • run の中の文字列は shell の世界。bash だったり cmd だったりするので env の展開方法はそれぞれ異なる
  • うっかり yarn <command> と書くと Windows では動かない。しっかり yarn run <command> と書く。
YAMLの中の条件判別などのSyntax大事

Contexts and expression syntax for GitHub Actions - GitHub Help

何が大事って、この部分は shell でも JavaScript でもない独自の文法の世界ってこと。リファレンス見ないと分からないし、オフラインの linter などもまず存在しない世界。

Actionの書き方
  • uses で action を利用する際の書き方は二種類。GitHub上のactionとそのバージョンの指定か、同一repos内のディレクトリのいずれか。外部のリポジトリを指す場合はバージョンを明記しろと言われるが、ローカルのファイルの場合はむしろ書いたらダメ。
    • <owner/repos@version>
    • ./.github/actions/<name>
  • uses で action を利用する場合、inputs / outputs ともに決められた方法でやりとりする
  • action を JavaScript で書く場合、npm install は実行されないので node_modules も含める。Linux / macOS ではなんとなく動く場合もあるけど、Windows だとちゃんとエラーになる。

.github/ ディレクトリの中に直接 action を置いてしまってもよいが、uses の書き方がやや異なるので注意が必要。

共通で使いそうなものは独立させて Creating a JavaScript action - GitHub Help に書いてある通り zeit/ncc を使って node_modules 込みでパッケージにしてしまった方がテストもしやすい(入出力は環境変数っぽい)のでよさそう。

※ 急に zeit さんと親しくなってしまった。

役に立つAction

release にできあがった file を upload する方法に go binary を使っている記事も見かけたけど、action があるのでそっちを使う方が手軽でよさげ。

matrix buildしてるYAMLの例の一部

outputs の読み取り方が結構独特。

run-on: ${{ matrix.os }}

strategy:
  matrix:
    os: [macos-latest, windows-latest, ubuntu-latest]

steps:
  - uses: <owner/repos@version>
    id: <action>
    with:
      os: ${{ matrix.os }}
  - name: package executable
    env:
      <NAME>: ${{ steps.<action>.outputs.<name> }}
    run: command $<NAME>
    if: matrix.os == 'windows-latest'
  - name: package executable
    env:
      <NAME>: ${{ steps.<action>.outputs.<name> }}
    run: command %<NAME>%
    if: matrix.os != 'windows-latest'

ここで env, if の部分は完全に GitHub Actions 独自の Syntax で、if の部分は $ で囲まなくてもよい。ただし、参照できるオブジェクトは決まっている。

run の中身は shell で実行されるので、shell に応じて環境変数の展開方法などを変える必要がある。

*1 少なくとも 2019-10-05 時点で


2019-10-09 [長年日記]

_ JavaScript Standardだけだと足りなかったのであれこれ

対象バージョン

このエントリは以下のバージョンを基準にしています。未来において違うことを言っている可能性はおおいにあります。

  • Node.js 12.10.0
  • ESLint 6.5.1
  • Standard 14.3.1
  • Jest 24.9.0
  • Power-Assert 1.6.1
  • eslint-config-standard 14.1.0
  • eslint-plugin-jsdoc 15.10.0

これまで

先月に Flycheck を入れてみて すこぶる快適なのだけど、やはり JavaScript についてはいろいろ不満は残っていて、

  • describe, it が undef で怒られる
  • そのくせ console は ok で browser 向けだとまずそう
  • 現実と戦う際に env: { jquery: true } とか欲しかったり
  • 結局 .eslintrc.js が欲しくなる

ということが分かってきた。多くのエディタでリアルタイムに近いレベルでチェックを走らせるとか、ほんとの基本となる基準としてはよいけど、十分かと言われるとそうでもない。

結局ESLintの設定を見直した

ということで ESLint の設定でしばらく悩んでいたんだけど、こんな感じになった。

module.exports = {
  extends: [
    'standard',
    'plugin:jsdoc/recommended'
  ],
  env: {
    browser: true,
    node: true
  },
  rules: {
    'no-console': process.env.NODE_ENV === 'development' ? 'off' : 'error',
    'no-debugger': process.env.NODE_ENV === 'development' ? 'off' : 'error',
    'jsdoc/require-param-description': 'off',
    'jsdoc/require-returns-description': 'off'
  }
}

端折っている部分もあるし、すべてにおいてこれと同じではないけど、だいぶ減らせたのはありがたい。

基本的な考え方

  • エディタ編集時のチェックで怒られなさすぎるのは良くない
  • でもじゃあエディタ編集時のチェックを全部「ESLint + .eslintrc + エディタの設定」に寄せるのはコストと合わない
    • describe, it についてはファイルローカルの /* global describe, it */ で回避
  • プロジェクトローカルではこれまで通り ESLint を入れて(ブラウザ向けかどうかなど考慮しつつ)設定を追加

JSDoc 周りは ESLint 4 以下の時代の方が自分の意図にはしっくりきてたので、今後の eslint-plugin-jsdoc の進化に期待。2019-10 現在まさに breaking changes をモリモリ入れながら開発が進んでいる感じ。*1

特に痛いのは require-jsdoc してるのに function definition に対しては lint が有効だけど method definition に対しては効かない点。これはだいぶ痛い。

describe, it については ESLint を使うなら globals の設定で ok だし、standard でもコマンドラインオプションを渡せばよいのも分かっているんだけど、上に書いた通り、まず(現在メンバーの使っている)

すべてのエディタでカジュアルに動かせること

を優先した結果、

/* global describe, it */

あたりが落としどころかなと考えている。

// eslint-disable-line

と同じような考え方だと思ってもらえればよいです。

*1 semver のおかげでバージョンがすごいことになってる


2019-10-16 [長年日記]

_ JavaScriptのcallbackをstub/mock化できるようにしたいのでbind(this)する

まとめ

JavaScript の callback メソッドを testable にしたい。

  • testable にするには独立して呼べることが大事。つまり独立メソッドにし、名前をつけることが必要。
  • 独立メソッドを callback として渡すにはリファレンスにする
  • リファレンスとして渡されたものは実行時に this が決定するのでそのまま実行すると同じ class 内でも this を見失う
  • リファレンスを bind(this) しておこう

this.method(val, function(err, res) {
  ..
})

みたいなコードはクロージャの中だけをテストすることができないからやめたい。じゃあこれを独立した callback メソッドにするとどうなるか。

this.method(val, this.callback)

これは callback メソッドのリファレンスを渡している形。これで必要な引数は自動的に渡るようになる。

このために method の中身は以下のようになっている。

method (val, cb) {
  ..
  cb(err, res)
  ..
}

これをそのまま実行すると callback は this を見失う。this は lexical scope ではなく dynamic に実行時に bind される。this を共有するには this を与える必要がある。

つまり正解はこう。

this.method(val, this.callback.bind(this))

arrow function を挟んで以下のようにすることもできるが、冗長だし、間違えそうなので避けたい。引数の数が少なく、名前も短いうちは読みやすいとは思うけど。

this.method(val, (err, res) => {
  this.callback(err, res)
})
Tags: JavaScript

2019-10-18 [長年日記]

_ node --inspect-brk && jest -iでデバッグ

  • Node.js には inspect というサブコマンドのようなもので debugger を有効にするモードがある。これは Node.js と CLI だけで完結する
  • 同じく Node.js には --inspect というオプションがある。これは V8 inspector integration で remote debug を有効にする
    • 実際には --inspect-brk にしておかないと何も待たずに動作を完了してしまう。daemon process 以外は --inspect-brk の方がよい。
    • Chrome には chrome://inspect で remote debug する機能がある。上記の node --inspect はこれに対応している。
    • Debugging Node.js with Chrome DevTools - Paul Irish - Medium
  • Jest はそのままではこの node --inspect には対応してなくて Jest 自身に -i ( runInBand ) オプションが必要

忘れちゃうんだよね。


2019-10-20 [長年日記]

_ kyのAPI違ってた

以下のバージョンで確認。

  • ky 0.15.0
  • ky-universal 0.3.0

sindresorhus/ky: &#127795; Tiny & elegant HTTP client based on window.fetch

ドキュメントでは以下のように書けることになっているんだけど、

await ky(uri).json()

これはダメで、

const res = await ky(uri)
const doc = await res.json()

のように分けないと動かなかった。

こんな超基本的な使い方の部分で間違わないでほしい…。pull-req 出そうにも範囲が大きいから絶対 reject でしょ、こんなの

Tags: JavaScript

2019-10-21 [長年日記]

_ thenから始めないといけないキモいオブジェクトで、stub/mockとか頑張る

Swagger Clientの設計がキモい

swagger-client 3.9.5 で確認。

swagger-api/swagger-js: Javascript library to connect to swagger-enabled APIs via browser or nodejs

いろいろあるけどいちばんキモいのはコレ。

const Swagger = require('swagger-client')

const s = new Swagger()
s
.then(client => {
  ..
})
.catch(e => {
  ..
})

これ、インスタンスが Promise そのものなので、まず then を書かないと実際の Swagger Client を取得できずに何も実行することができない。何らかのメソッドが Promise を返すのは分かるけどインスタンスそのものって、さすがにどうなの。

これって単に「自由な callback メソッドだと引数の順番のルールが統一されていないので、とりあえず Promise の仕様に合わせたよ」以上の意味がないように見えるんだけど、どうなんだろうなぁ。

PromiseになってしまったオブジェクトのメソッドをwrapするPromiseを作ってawaitできるように

上のような Promise は何も動作し始めていないので async/await できない。無限に wait して Timeout する。これは困る。then / catch の世界は結局 callback であり*1、scope が切れて扱いにくい。

そこで何らかのメソッドを呼ぶための wrapper を書いて、そいつが Promise を返すようにすると async/await で書けるようになる。上の例で言うと以下のような感じ。

async foo (retry) {
  return new Promise((resolve, reject) => {
    s
    .then(async (client) => {
      const r = await client.execute()
      resolve(r)
    })
    .catch(async (err) => {
      if (retry > 0) {
        return await foo(--retry).then(resolve).catch(reject)
      } else {
        return reject(err)
      }
    })
  })
}

ここではあえて古式ゆかしい Promise の書き方をしている。.then().catch() で数珠つなぎにしてる場合はそのまま値を return することはできないので async/await のように楽することができない。しっかり return new Promise() で resolve, reject してあげよう。こうしておかないと var を使って破壊的に値を代入したうえでその値をチェックして return するか throw するか、みたいな余計なロジックが必要になってしまう。

上のような Promise を作ると async/await の書き方とマッチする。

プロパティに収まった何もしないPromiseに対してstub, spyを作る

上の書き方で扱いにくい Promise を async/await で書けるようになったわけだが、まだ問題がある。テストコードである。

例えば c というオブジェクトの中に s という property で Swagger インスタンスを持っているとする。

class C {
  constructor () {
    this.s = new Swagger()
  }
}
const c = new C()

sinon で stub out する場合、then の方はこんな感じ。

sinon.stub(c, 's').get(() => {
  return Promise.resolve({
    async execute () {
      return {
        ..
      }
    }
  })
})

Promise.resolve() で client に相当するオブジェクトを作って返してやる。例えば client.execute() の実行が必要ならそれに対して適当な値が返ってくるように仕立ててやるとその周辺のコードのテストができる。async/await 時代はこういうコードがだいぶ書きやすくなってて助かる。

catch の方はこんな感じになる。

sinon.stub(c, 's').get(() => {
  return Promise.reject(..)
})

考え方は同様。Promise.reject() で Error オブジェクトを作って返してやる。

ポイントは sinon.stub().get() で、これは getter メソッドの stub out に使える記法。

少しまとめると、

  • そもそも stub out はメソッドに対して行うものだが、どうしても property に対して行いたいということであれば、property を getter メソッドと看做して stub().get() を使うことで対応できる
  • then() と catch() それぞれのテストは stub().get() で Promise.resolve() か Promise.reject() を return してあげることで kick できる

sinon の部分の続きは Sinon.JSにもう少し詳しくなったのでちょっとまとめ - あーありがち(2019-10-22) へ。

他の test double も同様の考え方を適用できるんだと思う。試してない。悪しからず。

execute を実環境に対して実行するテストはいろいろ重いので、平時は execute() に対して何らかの固定値を返すものを用意しておいて、その後の動作をテストする形になると思う。

*1 深くなりにくいだけ


2019-10-22 [長年日記]

_ Sinon.JSにもう少し詳しくなったのでちょっとまとめ

Sinon.JS - Standalone test fakes, spies, stubs and mocks for JavaScript. Works with any unit testing framework.

対象バージョン

Sinon 7.5.0

背景

  • これを書いている人はテスティングフレームワーク組み込みの double より独立しているものの方が好み
    • その方が寿命が長い傾向にあるため
  • これを書いている人はもともと Ruby で RR をよく使っている
  • Ruby の場合は property はそもそも private
  • public なのはすべて method
  • ただし Ruby の method は定義時も呼び出し時も () を付ける必要がなく、引数がない場合は property アクセスと同じように見える

Sinonの基本

  • Sinon の double はもともと存在している property に対してしか書けない
    • TypeError: Cannot stub non-existent own property <name> エラー
    • RR では存在していようがいまいが関係ない
  • Sinon の double は一回一回ちゃんと restore しないといけない
    • 同名のdouble に対して double をもう一度定義できない
    • TypeError: Attempted to wrap <name> which is already wrapped エラー
  • Sinon のドキュメントでは生成した double の object をレシーバにして restore しているが、sinon.restore() でもよい

Sinonのdoubleはmethodに対して機能する

Ruby と RR の組み合わせに慣れているとちょっと「あれ?」と思ってしまうが、以下の書き方を見ると納得。ここでは stub を例に挙げる。

sinon.stub(obj, <method>).returns(<val>)
sinon.stub(obj, <method>).callsFake(<func>)

ここでうっかり property を対象に stub を書いてしまっても定義はできるが property アクセスした際の値には影響しないし、method として call しようとすると not a function で TypeError になる。

では property を諦めて全部 method にしないといけないかというと、そうではなくて、これは getter method なのだと見做すとよい。正解はこう。

sinon.stub(obj, <method>).get(() => <val>)

何らかの値を return する function を get に与えてやればよい。

上に書いたように Ruby は property のように見えるものも method だし、method は式なので、RR の場合は return するか call するかのような区別は必要ない。これらの Ruby の API が DSL を作る際に非常に有利に働いていることがよく分かる。

stub().get()はなぜか多重定義できる

上に書いた原則に反するし、これを知っていても別に得しないが、

sinon.stub(obj, <method>).get(() => <val>)

に関しては restore なしに複数回実行できる。

callback, Promise地獄のJSでは純粋な関数とSpy重要

Promise は書いた順番通りに実行されないし、debugger でも完全に自由に break できるわけでもない。そこで

spyObj = sinon.spy(obj, <method>)
..
assert(spyObj.called)

のようにしておくと、どのメソッドが呼ばれたか ≒ どのルートを通っているかなどの確認ができるし、これが可能なように細かく純粋な関数を増やしておくと非常にテストしやすくなるのでオススメ。*1

*1 純粋な関数がよいのは sinon とか double とかに限らない普遍的なテクニック


2019-10-24 [長年日記]

_ PlantUML始めました

最近いろいろ新しいものを試せているのは Emacs の環境が新しくなってパッケージ管理と設定に悩まなくなったからだなぁ。というわけで前々から VS Code では書けたけど Emacs で書けるようになったのでおさらい。

基本

シンプルなテキストファイルで UML が書ける、オープンソースのツール

PlantUML は UML をテキストで書くための文法であり処理系の名前でもある。UML については割愛。

ねらい

  • 画像エディタと画像ファイルと共同管理の組み合わせで発生する種々様々な課題を避けたい

必要なもの

PlantUML の処理系は以下に依存している

Java の話はまた後日。

VS Codeで

以下の extension を入れる。

PlantUML - Visual Studio Marketplace

動作確認しているのは 2.12.2

以前は plantuml.com のサーバにデータを投げる仕様だったのかな? 最新版では plantuml.server の設定はデフォルトではなくなったよ、と書かれている。

  • 文法のパーサを持っている
  • plantuml.jar が同梱されている

ので、あとは Java と Graphviz だけ。PATH の通っている場所に java or jar コマンドと dot コマンドがあればたぶん何も設定しなくても動く。

Preview ペインを持っていて、一度これを開いておくとファイルの保存を待たずに適当なタイミングで rerender が走るのでカジュアルに始めるのにオススメ。ただ、どうも内部で一回一回コマンドを叩いてゼロから JVM が起動しているようで、preview と言う割にそこそこ重くて逆に気になるかもしれない。

Emacsで

plantuml-mode - MELPA

これも構成としては VS Code の場合と一緒。ただし、plantuml-mode は plantuml.jar を同梱していないのでその設定から行う必要がある。

plantuml-mode は 20191019.1309 の時点でデフォルトで plantuml.com のサーバに繋ぎにいくので注意が必要。

また、一度その動作をしないと M-x customize で設定項目が出てこない。この現象は以下のバージョンで確認済み。

  • Emacs 26.2
  • 20191019.1309

一度、何も内容のない UML で preview を叩いてみると plantuml.com に繋ぎに行くので、そこでキャンセルして

M-x customize

と叩くと設定できるようになっている。ここで

Plantuml Default Exec Mode

jar

にしておくとサーバに繋ぎに行かなくなる。あとは Java の設定やら Jar の設定やら必要かもしれないので頑張る。

plantuml を brew などなんらかのパッケージ管理ツールで入れている場合はその辺の設定の参考になると思う。

コマンドで

Homebrew を利用している場合は brew install plantuml すると plantuml コマンドというか sh script ができるのでこれを利用する。

この中身を見ると

  • 同じく brew で入れた dot
  • brew で入れた plantuml.jar

のパスが直書きされているのが分かる。

Emacs の plantuml-mode の設定の際にこれが必要になるかも。逆に plantuml-mode の設定を見ていると plantuml -help では分からない設定があって、それが

-headless

オプション。これがないと一瞬 Dock に Duke が現れて今使いたいアプリからフォーカスを奪って逃げていく。すごく腹がたつので絶対に設定しておきたい。

出力形式とビュワー

  • デフォルトは PNG を生成するので適当な画像ビュワーで開ける
  • 書き込みが細かくなってくるとベクター画像が欲しくなってくるが、そうなるといろいろ挙動があやしい。とりあえずでやってみたら PDF は生成できなかった。EPS と SVG はイケた。
    • ベクター画像が欲しければ SVG を生成してブラウザで開く、くらいがお手軽かも

本当は dot ファイルそのものが出力できるといいんじゃないかと思ったんだけど、どうやら PlantUML にそういう出力オプションを作るのは難しいらしい。(request はちょくちょく上がっている模様。)

SVG はブラウザが対応してるので各種 Web サービスに上がっていてもそのまま表示できるわけで、まぁまぁよさげ。ちょっと確認したところ OmniGraffle も draw.io も対応してるし、なんかいろいろ大丈夫じゃないかな。

Tags: UML

2019-10-25 [長年日記]

_ chokidar-cliとパイプを使って本当に変更のあったファイルだけ何かしたい

kimmobrunfeldt/chokidar-cli: Fast cross-platform cli utility to watch file system changes

chokidar <files> -c '<command> <files>' 方式は無駄が多いかも

chokidar を使った処理でよく見るのは以下のような方法だと思う。

chokidar '**/*.js' -c '<command> **/*.js'

この方法だと監視対象のいずれかのファイルに何らかのイベントが発生した場合に、対象ファイル群すべてに対してコマンドを実行してしまう。すべてを対象にトランスパイルして bundle ファイルを生成するような処理なら仕方ないが、一つ一つの処理が独立していてお互いに関係せず、かつそこそこ負荷が高い場合には別々に実行したい。

chokidarはコマンド実行だけはなくイベントのstream outputもできる

chokidar は対象ファイル群を watch し始めたら、検知したイベントとファイル名を

イベント:ファイル名

の形で stdout に出力する機能を持っている。

この出力を受け取って何らかの処理をするコマンドを作れば、上のような無駄を防げる。

ただし、注意が必要

愚直にパイプで繋いで

chokidar <files> | <command>

ってやってしまうと、以下のような無駄な出力も受け取ってしまう

  1. yarn run コマンドの出力 ( yarn run <version> )
  2. chokidar コマンドを起動した文字列そのまま
  3. unlink したファイル

1 と 2 は単純に chokidar の出力が始まる前のものなので、そもそも必要な情報が入っていない。unlink イベントは chokidar の反応なのだが、unlink されているファイルに対して何らかの処理をするコマンドを叩いてもエラーにしかならないので、これも除外する必要がある。

本当に欲しかったもの

総合すると必要なものは

chokidar <files> | <filter> | <command>

のような形。

実装例

実際やったのは PlantUML の render で、以下のような感じ。

chokidar "**/*.plantuml" | awk -F : "
  !/unlink|run|\*/ {
    print \$0
    system(\"plantuml -headless -tsvg \" \$2)
  }
"

これで処理対象の PlantUML のファイルが増えても最小のコストで re-render できる。

chokidar なんか名前が好きじゃなくて敬遠してたんだけど、この機能はよい。とてもよいぞ。好きになった(簡単)。