2020年6月29日月曜日

Wandbox の CLI ツールを作ってみた


タイトルの通りコマンドラインツールを作ってみました。
リポジトリはこちら。https://github.com/srz-zumix/wandbox-api/

言語は Python で PyPI に Publish してますので、 pip install wandbox-api ですぐに使えます!
PyPI に初めて Publish しました〜
この辺は「Github ActionsでPyPIにパッケージを公開する」を参考に、GitHub Actions で tag をトリガーに Publish されるようにしました。
name: PyPI Publish
on:
  push:
    tags:
      - v*

jobs:
  pypi:
    runs-on: ubuntu-18.04
    steps:
    - uses: actions/checkout@master
    - name: Set up Python 3.7
      uses: actions/setup-python@v1
      with:
        python-version: 3.7
    - name: Init .pypirc
      env:
        PYPI_USERNAME: __token__
        PYPI_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
      run: |
        echo -e '[pypi]' >> ~/.pypirc
        echo -e "username = ${PYPI_USERNAME}" >> ~/.pypirc
        echo -e "password = ${PYPI_PASSWORD}" >> ~/.pypirc
    - name: Publish
      run: |
        pip install twine wheel
        python setup.py sdist bdist_wheel
        twine upload --repository pypi dist/*


現在の機能は、Wandbox が対応している言語・コンパイラー・オプションのリストアップとコンパイル&実行です。また、C++ 用に特化した wandbox-cxx コマンドもあります。
使用例
言語リストを取得
$ wandbox lang
Bash script
C
C#
C++
CMake
CPP
CoffeeScript
Crystal
D
Elixir
Erlang
F#
Go
Groovy
Haskell
Java
JavaScript
Lazy K
Lisp
Lua
Nim
OCaml
OpenSSL
PHP
Pascal
Perl
Pony
Python
R
Rill
Ruby
Rust
SQL
Scala
Swift
TypeScript
Vim script
コンパイラーのリストを取得
-l オプションで言語を選択してリストアップします。言語指定がない場合はすべての言語のコンパイラーをリストアップします。
$ wandbox -l C++ compiler
gcc-head
gcc-9.3.0
gcc-9.2.0
gcc-9.1.0
gcc-8.3.0
gcc-8.2.0
gcc-8.1.0
gcc-7.3.0
gcc-7.2.0
gcc-7.1.0
gcc-6.3.0
gcc-6.2.0
gcc-6.1.0
gcc-5.5.0
gcc-5.4.0
gcc-5.3.0
gcc-5.2.0
gcc-5.1.0
gcc-4.9.3
gcc-4.9.2
gcc-4.9.1
gcc-4.9.0
gcc-4.8.5
gcc-4.8.4
gcc-4.8.3
gcc-4.8.2
gcc-4.8.1
gcc-4.7.4
gcc-4.7.3
gcc-4.6.4
gcc-4.5.4
gcc-4.4.7
clang-head
clang-10.0.0
clang-9.0.0
clang-8.0.0
clang-7.0.0
clang-6.0.1
clang-6.0.0
clang-5.0.0
clang-4.0.1
clang-4.0.0
clang-3.9.1
clang-3.8.1
clang-3.7.1
clang-3.6.0
clang-3.5.0
clang-3.4
clang-3.3
clang-3.2
clang-3.1
zapcc-2017.08
zapcc-1.0.1
コンパイラーのオプションを取得
-c オプションでコンパイラーを選択すると、そのコンパイラーで使用できるオプションをリストアップします。指定がない場合はすべてのオプションをリストアップします。

$ wandbox -c clang-head option
C++: 
warning (default)
optimize
cpp-verbose
boost-1.73.0-clang-head (default)
  boost-nothing-clang-head
  boost-1.60.0-clang-head
  boost-1.61.0-clang-head
  boost-1.62.0-clang-head
  boost-1.63.0-clang-head
  boost-1.64.0-clang-head
  boost-1.65.0-clang-head
  boost-1.65.1-clang-head
  boost-1.66.0-clang-head
  boost-1.67.0-clang-head
  boost-1.68.0-clang-head
  boost-1.69.0-clang-head
  boost-1.70.0-clang-head
  boost-1.71.0-clang-head
  boost-1.72.0-clang-head
  boost-1.73.0-clang-head
sprout
msgpack
gnu++2a (default)
  std-c++-default
  c++98
  gnu++98
  c++11
  gnu++11
  c++14
  gnu++14
  c++17
  gnu++17
  c++2a
  gnu++2a
cpp-no-pedantic (default)
  cpp-no-pedantic
  cpp-pedantic
  cpp-pedantic-errors
C++ のコードをビルド・実行する
wandbox-api パッケージには C++ 用のコマンドとして、wandbox-cxx と wandbox-g++/wandbox-clang++ が含まれています。
cxx は wandbox コマンドに -l C++ を指定したのと同等です。wandbox-g++/wandbox-clang++ はさらに -c オプションでコンパイラーの指定をしたものになります
(バージョンはそれぞれ head)
また、これらは通常の wandbox コマンドのコマンドラインオプションに加えて、 C++ コンパイラー向けの Wandbox オプションがコマンドラインオプションで設定できるようになっています。
  --std VERSION         set --std options
  --boost VERSION       set boost options version X.XX or nothing
  --optimize            use optimization
  --cpp-verbose         use cpp-verbose
  --sprout              use sprout
  --msgpack             use msgpack
run に続く引数は、コンパイラーへの引数としてそのまま渡されます。
$ wandbox-clang++ run main.cpp test.cpp
program_message:
Hello, Wandbox!
test
$ wandbox-clang++ run main.cpp test.cpp -DTEST
program_message:
test
test
-s オプションを付けると permlink を発行できます。
wandbox-clang++ -s run main.cpp test.cpp -DTEST
program_message:
test
test


permlink: TuvMXWckFrkxGPAs
url: https://wandbox.org/permlink/TuvMXWckFrkxGPAs
[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ https://wandbox.org/permlink/6AGrKCojBOo1oCUv


使用したソースコードはこちらです。
今後
今後、ちまちまと他の言語のコマンドも追加していこうと思います。
もちろん PR 大歓迎です!
今回は以上です。では。



2020年6月23日火曜日

[AppVeyor] Python3 系を使うようにする

AppVeyor で現在提供されているエージェント(環境)は Python 2.7 がデフォルトになっているので、Python 3 系に変更しました。
AppVeyor の環境にプリインストールされている Python は下記から確認できます。

あとはお好きなバージョンのディレクトリを PATH に追加したら終わりです。

version: 1.0.{build}
branches:
  only:
  - master
image: Visual Studio 2019
init:
- cmd: SET PATH=C:\Python38;C:\Python38\Scripts;%PATH%
以上です。

2020年6月18日木曜日

[GIT] 「ファイルのタイムスタンプをコミット日時に合わせる」を爆速にした

svn には use-commit-times がある。
git だとないが git ls-files と git log の組み合わせで対応できる。
そしてそのスクリプトも git-set-file-times としてある。
ここまでは既出な情報である。


私も過去の記事を見て知った口だ。

さて、ここで今この記事を読んでいる読者は、私と同じように何らかの理由があってタイムスタンプを変更したい方が多いのではないか、と思います。
私がタイムスタンプを変更したい理由は、とあるビルドシステムがファイルのタイムスタンプを使って差分ビルドの「ビルドする・しないを決める」、かつ、同じワークスペースでブランチを行き来するため pull したときのタイムスタンプが、そのブランチにおけるファイルの変更と一致しないため、差分ビルドしたときに入ってほしいものが入ってない、入ってほしくなものが入ってしまった。という事象の解決のためでした。

なんか、パイプラインの問題でもある気がするけど、
git の変更履歴とファイルのタイムスタンプが時系列的にあえば、ビルドシステム的に解決。
しかもその方法はすでにあってコマンド1つで実現するのであれば、利用しちゃいますよね。
で、試したところ・・・

時間がかかりすぎてハンパない!!

個人開発のリポジトリで試したときは処理時間が気にはならなかったのですが、
実際それがほしいリポジトリは、超巨大・非テキスト・長履歴で、全部のファイルのタイムスタンプを設定するのに 15 分強 (1,000s くらい)かかってました。。

結論

紆余曲折があるのだが、先に結論だけ書く。
冒頭の Qiita で紹介されていた git-set-file-times に 1 つだけオプションを追加することで、10 倍速、約1分半(100s くらい)で処理できるようになりました!
修正した git-set-file-times はこちら。

追加したオプションは -c diff.renames=false です。
ファイルが更新されたことさえ知れればよく、Add なのか Move なのかは重要ではないので、 false にしても問題ありません。
git の diff は diff.renameLimit の一致度?(デフォルト 50%)で rename 判断をしてるみたいなので、おそらくファイルごとに一致度を調べてたから遅かったと思われます。

あと、↑の git-set-file-times では見つからなかったファイルに対して、git log の最後のコミットのタイムスタンプで更新するようも修正してます。

これは git-set-file-times --since 2020/06/17 のように log コマンドのオプションを設定できるためです。(この場合 2020/6/17 以降の変更はコミットのタイムスタンプ、2020/06/17 以降に変更がなかったファイルは全部同じタイムスタンプが設定されます。)

紆余曲折
コミットを辿るんじゃなくファイルリストごとに git log すればいいだけでは?
まずはじめに考えたのは、git ls-files で取得したファイルリストから、分散してファイル1つ1つ最終コミットを取得してタイムスタンプを更新するものでした。

クソ遅い!

git の仕組みを少し理解したので今ならわかりますが、この方法はクソ遅いです。
なんとなくファイル単位で git log したときのオーダーは O(1) なイメージで、すぐ取ってこれると思っていたのですが、そんなことはなく、実は commit を辿って辿って目当てのファイルでフィルタリングしてるだけみたいです。

なので、git-set-file-times のやり方で問題ないです。
(ファイルリストが空になるまで log を辿る。)

git コマンドじゃなくて git そのものを扱えばいいじゃない
git log からコミットのタイムスタンプとファイルリストを取得しているが、git そのものを扱えば速いんじゃない?と思い↓を作成しました。

libgit2/git2go を使って Go でコマンドを作成してます。

で、これも遅かった。
どれくらい遅かったかというと、git-set-file-times の 10倍遅かった

Go 書くの初めてだったので、お前のコードが悪い or libgit2/git2go の使い方が悪いのかもしれない。
ただ、Go 側で diff のファイルを foreach するよりも、ToBuf で文字列化したものを受け取って Split したほうが若干速かったので、C の呼び出しは減らしたほうがいいのかな?と思っている。

悔しいので Go で git log コマンドコールしてパースするバージョンも書いてみたけど、速度は対して変わらず。
まぁ git-set-file-times でいいわ、--since とかできるし。って結論になった。

また時間があれば C++ で再実装したり、別の言語のバインディングを使ってみたいと思う。

完璧ではないがリミットを設けたらどうだろうか?
first commit 以来、一切変更されてないファイルがあるとログを全部辿ることになるのでそりゃ時間かかるよな。そんな極端でなくてもあまり更新されないファイルって存在すると思うので、結構な数のログを辿らないと終わらないケースは多々ありそう。

と、思いながら git-set-file-times 眺めてたら、git log コマンドで引数を受け付けてたので、ログ数制限できるなーと思ったのでやってみた。

git-set-file-times --since 2020/06/01

って感じ。

これは、思ったとおりすぐ終わった!

じゃ、あとは残ったファイルには一律同じタイムスタンプつけとけば ok だなってことで、現在の git-set-file-times のようになりました。

まぁ完璧ではないけど、求められているものにはなったんじゃない??
いやーめでたしめでたし。

だったんですが、ふと気づいたのです。
なんか警告出てるな、と。

diff.renameLimit の警告が我を救った
warning: only found copies from modified paths due to too many files.
warning: you may want to set your diff.renamelimit variable to at least XXXX and retry the command
たまたまこんな警告が出てたんで、警告はでないようにしたいなと思ったのがきっかけ。
まず、diff.renameLimit=0 にしてみたんですが、おそろしく処理時間がかかるようになりました。 15分どころか、数時間待っても終わらず kill しました。。

ここでようやく気づきます。
Rename かどうかは重要ではない

ここからゴールはもうすぐです。
rename かどうかの追跡は git config の diff.renames で設定できることを調べて知ります。
config を一時的に変えてコマンド実行したいなと思い調べると、サブコマンドの前のオプションで -c 使えばいいことがわかります。

つまり git -c diff.rename=false log こうです。
そして、完成!!

試しに実行したときにすぐに完了したので感動しましたね。

最後に

git 奥深い。。。