Subscribed unsubscribe Subscribe Subscribe

Unyablog.

のにれんのブログ

vim-keymaps でキーマップを簡単に切り替える

vim

Vim のキーマップを簡単に切り替えられるプラグイン vim-keymaps を作りました。

github.com

下のように、複数のキーマップを設定することができ、簡単に切り替えることが出来ます。下では <C-k> でキーマップを切り替えています。

f:id:nonylene:20170322005931g:plain

今すぐダウンロード!

使い方

各キーマップを示すディクショナリを入れた配列を g:keymaps に設定し、後はキーマップを変更するためのキーを適当に割り当てる or コマンドを打つと使えます。詳しくは README.md で。

初めは接続されているキーボードでなんとか判別できないかなと考えましたが、 ssh 先でキーボードの配列を読むのは不可能なので手動で変更するようにしました。

経緯

私は普段、コーディングするアプリケーション上では数字と記号を入れ替えて使っています。例えば 1! になり、 !1 になります。

瞬時に括弧などが打てるので非常に便利に使っていたのですが、キーボードによって数字と記号の関係は異なるので、JIS 配列に合わせたマッピングを書いていると US キーボードでは崩壊してしまいます。記号だけではなく数字も入力できなくなるので非常に困る。

しかし、いろいろあって先月から US 配列の mac を使うことになりました。

mac のキーボードのみ使うなら .vimrc を書き直して US にマッピングし直せば良いのですが、普段接続して使っている HHKB はもちろん JIS 配列。本体キーボードで使う時だけ paste モードで使う運用をしていました。

しかし paste モードのままでは tab で(空白ではなく) tab が入力されたり、数字記号以外の各種マッピングが効かないなど困ります。

これはもうキーマップをキー配列ごとに変更するしかない。 せっかくの機会なのでプラグインを自分で作りました。

作成

vimプラグインを書くことは初めてでした。まともに vimscript も書いたこと無かったし。

書くときは下のサイトを参考にしたり、有名なプラグインのコードを見たりしながら書きました。

mattn.kaoriya.net

thinca.hatenablog.com

vim のドキュメントも結構読みました。読みやすい英語だったし、充実していて良かった。

vimscript, はじめは変数のスコープ定義が特殊だったりしてとっつきにくいですが、書けてくるとなかなか楽しいです*1

内部の話

設定ファイル

設定ファイルをどのように記述しようか迷って、結局 vimscript の配列を使うことにしました。ただ、 vimscript で配列や辞書型を複数行に記述しようとすると割りと面倒で、行の最初に \ を書くことで行の継続としなければなりません。あくまで行の継続なので、途中にコメントを書くことは出来ません…。( vim は一行ごとのコメントしかできない ) README を書いてる時にその事実に気づいて修正しました…。

マッピングの数は結構多いので、.vimrc に長大な配列を記述することになります。以下は自分の設定 を半分ぐらい省略したもの です。

let g:keymaps =  [
      \  {
      \    'name': 'JP',
      \    'keymap': {
      \      'noremap!': {
      \        '1': '!',
      \        '3': '#',
      \        '4': '$',
      \        '5': '%',
      \        '6': '&',
      \        '!': '1',
      \        '"': '2',
      \        '#': '3',
      \        '$': '4',
      \        '%': '5',
      \        '&': '6',
      \        "'": '7',
      \        '(': '8',
      \        ')': '9',
      \      },
      \      'imap': {
      \        '2': '<Plug>delimitMate"',
      \        '7': "<Plug>delimitMate'",
      \        '8': '<Plug>delimitMate(',
      \        '9': '<Plug>delimitMate)',
      \      },
      \      'cnoremap': {
      \        '2': '"',
      \        '7': "'",
      \        '8': '(',
      \        '9': ')',
      \      },
      \    },
      \  },
      \  {
      \    'name': 'PASTE',
      \    'paste': 1
      \  },
      \]

dein のように toml や json に分けれればなあと思ったのですが、 vimscript では標準ライブラリでシュッとパースすることはできません*2Python バインドでも使えば良いのですが、標準では入っていないことが多いので諦めました*3

map 方法

この g:keymap からマッピングするのは非常に素朴で、コマンド、左辺、右辺をスペースでつなげて放り込んでいます。なのでオプションを適当に入れても動くし、他のコマンドも打てちゃいます(キーマップ削除する時にエラー出ると思いますが)。

execute(l:cmd . ' ' . pair[0] . ' ' . pair[1])

特に立派な API は無かったのでこうしました。スペース等も自分で書くのと同じようにエスケープすれば動きます。

unmap 方法

デフォルト設定ではキーマップ変更時に以前のマッピングを削除しています。よって、マッピングと対になるアンマッピングコマンドを生成する必要があります。

map - Vim日本語ドキュメント

これも素朴に1文字目と2文字目をパースしています。

function! s:create_unmap(origin)
  let l:first = a:origin[0]
  if l:first is 'n'
    if a:origin[1] is 'o'
      " noremap
      let l:cmd = 'unmap'
    else
      let l:cmd = 'nunmap'
    endif
  elseif l:first is 'm'
    " unmap
    let l:cmd = 'unmap'
  else
    let l:cmd = l:first . 'unmap'
  endif

  if a:origin =~ '!'
    let l:cmd .= '!'
  endif

  return l:cmd
endfunction

paste モード関連

このプラグインは paste モードをサポートしているのですが、paste モードに入ってしまうとマッピングが全て外れるのでキーマップの切り替えができません。

set pastetoggle=<同じキー>

とすることで vim の機能によって paste モードを抜けることができるのですが、vim の機能で抜けているので次のキーマップには移動されず paste 用キーマップのままになってしまいます。

結局、autocmd を使うことによって paste モードの変更を検知して、フック上でキーマップを変更することにしました。

augroup keymaps
  autocmd!
  autocmd OptionSet paste call s:on_change_paste()
augroup END

function! s:on_change_paste()
  " sometimes v:option_new not working
  if g:keymaps_paste_auto_rotate && !&paste
        \ && get(keymaps#get_current_keymap(), 'paste', 0)
    " exit paste mode -> rotate
    call keymaps#rotate_keymap()
  endif
endfunction

lightline との連携

keymaps#get_current_keymap_name で現在のキーマップ名を取れるようにしたので、これを lightline に表示すれば現在のキーマップを簡単に見れます。最初に示した gif もそうしています。

paste モード変更時にもうまくするには lightline も paste を検知できるように autocmd を新しく設定する必要があります。

augroup local
  autocmd!
  autocmd OptionSet paste call lightline#update()
augroup END

その他

  • <SID> をキーマップに使っているとその <SID> は実行時のスクリプトに依存するのでプラグイン用のスコープになってしまう。
    • 同じような理由から、 <expr> をキーマップに使って関数を実行したい場合、呼び出す関数はグローバルでなければならなかったりする。
  • autoload/ に多くを分割したのだけど、最初のキーマップを設定するために起動時に読み込まれるので逆効果になっているかもしれない。
    • まあ重いスクリプトではないので大丈夫だと思うけど。
  • vimscript 、 endforend にできるが endfunctionend に省略できなかったりして面白い。
    • 結局全部フルで書いた。
  • l: をちゃんと書いてたけど実は暗黙的に設定されるっぽい。
  • vimscript の文法、だいぶ知れたのでこれからは .vimrc サクサク書けそう。
  • US キーボード、' が一瞬で打てるのが良いですね。でも数字と記号を入れ替えていると 0 に記号が割り当てられてしまって不便だったりする。

まとめ

これで今まで通り記号が簡単に入力できるようになりました。 「paste モードではないが記号のマップは削除したキーマップ」なども設定できて快適です。

Vim 最高!みんなも今すぐプラグイン書こう!!

*1:perl っぽいですね(?)

*2:雑に eval すると大体動くとは思う

*3:実は go とか使うとよかったりするのかな

zsh の色付き vcs_info を一行上の右端に出す

Shell Zsh

書いておかないとあとで分からなくなるのでメモ。

zsh 便利ですよね。vcs_info も便利ですよね。

vcs_info は名前の通り VCS の情報を出してくれる君です。

zshのターミナルにリポジトリの情報を表示してみる · けんごのお屋敷

本題

最近 zsh のプロンプトを以下のように変更しました。

f:id:nonylene:20170308024523p:plain

f:id:nonylene:20170308034716p:plain

ポイントは左にあった vcs_info が右に移動していることです。

RPROMPT 問題

vcs_info を右に移動するには vcs_info を単純に RPROMPT に移動させれば良いのでは?と思いますが、 RPROMPT はコマンド入力と同じ場所に表示されるため、パスやコマンドが長いと消えてしまいます。

f:id:nonylene:20170308025835p:plain

なら上に表示すれば良いのではと思って PROMPTRPOMPT を二行にしてみると、今度は表示されなくなってしまいます。

RPOMPTPROMPT が複数行に渡っても最終行と同じ行に表示され、改行ができません。

f:id:nonylene:20170308025857p:plain

一行上の右端に表示するには、 precmd 内でプロンプト復帰前に printf コマンドを用いて表示する必要がありました。

printf 問題

色がない場合、 printf で右端に表示するには以下のように行なえば良いです。

# プロンプト復帰前に呼ばれる
precmd() {
  # $prompt_right -> 右端に表示する文字列
  # $prompt_left -> 左端に表示する文字列
  printf "%s%*s" "$prompt_left" "$((${COLUMNS} - ${#prompt_left}" "$prompt_right"
}

%*s ( * で幅を指定する) で長さに "$((${COLUMNS}-${#prompt_left}" (全体の横の長さから $prompt_left の長さを引いたもの) を指定することによって、右寄せを実現しています。

しかし、これを色付きで行うと崩れてしまいます。

f:id:nonylene:20170308031636p:plain

なぜなら、ANSI エスケープシーケンスを含めた時、 $prompt_right 等にエスケープシーケンスが入って文字の長さが長くなり、 printf の幅空けや ${#prompt_left} による文字長の取得が狂うためです…!!!!😇

この時、ちゃんとシーケンスの長さを考慮してあげるとうまく表示できるようになります。

# 長さは適当に 10 にしている
printf "%s%*s" "$prompt_left" "$((${COLUMNS} - ${#prompt_left} + 10" "$prompt_right"

vcs_info の action 問題

vcs_info には actionformats という機能があり、コンフリクト解決やリベース時などにその action を表示することが出来ます。

自分は、acrion の中身は目立つように赤色にしています。なので action があると、シーケンスの文字数が増えて再び描画が崩れてしまいます。

f:id:nonylene:20170308032729p:plain

action があるかどうかは precmd 内部では分かりません。どうしようかなと調べていると、 vcs_info にはフック関数を設定できることが分かりました。

zsh.sourceforge.net

これを使って、フック関数の中で action があるかどうかを見れば、あとは変数にセットすることで precmd 内でも分かります。

最終的に以下の様になりました。

# tput でエスケープシーケンスを出力する
tput_normal=$(tput sgr0)
tput_yellow=$(tput setaf 3)
tput_red=$(tput setaf 1)
tput_cyan=$(tput setaf 6)

# vcs_info の設定
zstyle ':vcs_info:*' enable git hg
zstyle ':vcs_info:*:*' formats "[${tput_yellow}%r: %b${tput_normal}]"
zstyle ':vcs_info:*:*' actionformats "[${tput_yellow}%r: %b${tput_normal} (${tput_red}%a${tput_normal})]"

# hook 関数の設定
# メッセージが設定される直前
zstyle ':vcs_info:*+set-message:*' hooks vcs_info_hook
# VCS が存在しないとき
zstyle ':vcs_info:*+no-vcs:*' hooks no_vcs_hook

# VCS がないときのエスケープシーケンス長
novcs_color_len=$((${#tput_cyan} + ${#tput_normal}))
# VCS があるが action がないとき
noaction_color_len=$((${novcs_color_len} + ${#tput_yellow} + ${#tput_normal}))
# VCS があり、action もあるとき
action_color_len=$((${noaction_color_len} + ${#tput_red} + ${#tput_normal}))

# $prompt_color_len にシーケンス長を設定する
# VCS があるときの hook
function +vi-vcs_info_hook() {
  # action があるかどうかは hook_com[action] を見れば良い
  if [ -z ${hook_com[action]} ]; then
    prompt_color_len=$noaction_color_len
  else
    prompt_color_len=$action_color_len
  fi
}

# VCS がないときの hook
function +vi-no_vcs_hook() {
  prompt_color_len=$novcs_color_len
}

precmd() {
  vcs_info
  # 左寄せする文字列
  prompt_left="${tput_cyan}${USER}@${HOST}${tput_normal}"

  # 各情報を表示
  printf "\n%s%*s\n" "$prompt_left" \
    "$((${COLUMNS}-${#prompt_left}+${prompt_color_len}-1))" \
    "${vcs_info_msg_0_}"
}

# このプロンプトは printf 後に表示される (パスの部分)
PROMPT='%~ %(!. !root! #.>) '

途中で -1 しているのは、右寄せの最後に一文字空けるためです (wrap っぽく判別されるのを防ぐ)。

これで大体いい感じになりました。

f:id:nonylene:20170308034641p:plain

リサイズ問題

ウィンドウのサイズを変更した時、 RPROMPT では良い感じに右寄せ文字が移動してくれるのですが precmdprintf した文字列は再描画されないので残念ながらこのままではズレてしまいます。

f:id:nonylene:20170308135044p:plain

ここで、 TRAPWINCH を用います。

TRAPWINCH はサイズが変更された時に呼ばれる関数です。しかし、単純にここで printf すると、プロンプトの下に追加されてしまうので思った通りになりません。

f:id:nonylene:20170308135638p:plain

そこで、 printf していたのを PROMPT に変数として組み込み、TRAPWINCH 内で zle reset-prompt することによって PROMPT ごと再描画するようにします。

# $prompt_header を生成する
function generate_promopt_header() {
  prompt_header=$(printf "\n%s%*s" "$prompt_left" \
    "$((${COLUMNS}-${#prompt_left}+${prompt_color_len}-1))" \
    "${vcs_info_msg_0_}")
}

precmd() {
  vcs_info
  prompt_left="${tput_cyan}${USER}@${HOST}${tput_normal}"
  # printf をやめて $prompt_header を生成するのみとする
  generate_promopt_header
}

TRAPWINCH() {
  # 長さが変わったので $prompt_header を再生成   
  generate_promopt_header
  # プロンプト再描画
  zle reset-prompt
}

# $prompt_header を改行前に含める
PROMPT='${prompt_header}
%~ %(!. !root! #.>) '

こうして、幅が変わっても無事に再描画されるようになりました。

f:id:nonylene:20170308140603g:plain

残る問題

実はまだ問題が残っていて、ブランチ名等が日本語等のときにカラム幅が合わなくなり、はみ出してしまいます。

f:id:nonylene:20170308140749p:plain

wc -c 等を使ってもバイト数だから上手くいかないし、どうしたものか…。

まあ日本語で git ブランチや git 用のディレクトリ作ることはそうそう無さそうなので今回は放置。

全体的な zshrc はこちらです。(大したことやってない)

dotfiles/.zshrc at master · nonylene/dotfiles · GitHub

Shell の pipe と redirection を実装する

Shell Linux

新年なので Shell の pipe と redirection を実装した。

前回の記事はこちら

nonylene.hatenablog.jp

前回実装してみて、さすがに pipe と redirection ぐらいは実装しておいてもいいのでは?と思って実装してみた。

pipe

github.com

パイプについての説明は

パイプでつなぐ - PukiWiki

が分かりやすくて、ここを見ると大体いい感じに実装できた。

一つ困ったのは、試しに

$ ls | ls

とした時に

ls: write error: Broken pipe

が確率的に発生していた。試しに C で同じような実装をすると特に発生せず、速度の問題かと思って C で sleep しても再現しなくて困っていた(まあこんなコマンド打つことないけど…)。

結局原因は Python がデフォルトでは SIGPIPE を無視することで、これによって後段の ls が終了しても前段が正しく終了せずに ls が実行されてしまっていた。確率的に起きていたのは SIGPIPEexecvp が実行される前か後かの違いによるものだと思う。

PythonSIGPIPE を無視しないようにすると直ったので良かった。

signal.signal(signal.SIGPIPE,signal.SIG_DFL)

redirection

リダイレクションも pipe と原理は同じで、ファイルディスクリプタdup するだけでシュッと実装できた。

github.com

段々気合でパースしている部分が大変になってきている…。

面白かったのは

$ ls > a | cat

とした時に zsh では出力がなされたけど bash / dash では出力されなかった。

これは、 zshmultios という機能によって zsh が redirection と pipe どちらにも出力しているからだった。

zsh.sourceforge.net

unsetopt multios すると、bash / dash と同じ挙動になった。なるほど〜

Mackerel Agent for Android

Android Linux

この記事は Mackerel アドベントカレンダー 21 日目の記事です。

qiita.com

Mackerel について

Mackerel とはサーバーを監視するサービスで、Slack などとの連携が便利だったりグラフが美しかったりと良いサービスです。

Mackerel にデータを送るためのサーバー監視ソフトウェアとしては mackerel-agent というものがあり、これをインストールするとサーバー上でメトリックを定期的に取得し、送信してくれます。

github.com

本題

今回はその Android 版を作ってみました。

Mackerel Agent for Android です。

f:id:nonylene:20161221015614p:plain

昨日から PlayStore に公開しています!

f:id:nonylene:20161222000441p:plain:w150

play.google.com

GitHub はこちらです。

github.com

対応バージョンは Android 4.4 以上です。

機能

1分毎にメトリックを集め、5分毎にサーバーに送信します。Mackrel には 1 ホストとして登録するようにしています。

残念ながら mackerel-plugin は使えませんが、公式の mackerel-agent が現在送信しているメトリックと spec は全てカバーしています。

動機

実装すると proc が学べるし良いかなと思ったからです。あとバッテリー切れそうな時に通知してくれると便利かもと思いました(がまだ Android 関係のカスタムメトリックは実装していない…)。

実装等

AndroidLinux なので概ね mackrel-agent の Linux 版と同じ処理でメトリックを取得できます。

実装は Kotlin で行いました。アプリの特性上 proc ファイルをたくさん読むのですが、Kotlin だとファイル関係がかなり楽なので良かったです。

// Interface の Spec を読む例
File("/proc/net/dev").readLines()
        .map { it.split(":").map(String::trim) }
        .filter { it.size == 2 }
        // remove loopback
        .filter { it[0] != "lo" }
        .mapNotNull {
            val values = it[1].split(Regex("\\s+")).map(String::toDouble)
                // all zero -> remove data
            if (values.all { it == 0.0 }) return@mapNotNull null
            InterfaceStat(it[0].trim(),
                    values[0], values[1], values[2], values[3], values[4], values[5], values[6],
                    values[7], values[8], values[9], values[10], values[11], values[12], values[13],
                    values[14], values[15], time
            )
        }

ログや取得した値のキャッシュは Realm を使っています。proc を用いて差分を出すには数秒間待つなどの必要がありますが、そのあたりは RxJava を用いていい感じにしました。*1

Android の辛いところ

コマンド

mackerel-agent ではいくつかコマンドを打って結果をパースしている物があります。

dfuname がそれに当たるのですが、これを Android で打とうとするとバージョン問題に直面します。

Android 5.0 未満では df は一切オプション受け付けない上に独自の形式で出力され、uname においては存在すらいしていません。

結局 df では以下のようにバージョンで分けるようにして、uname は proc から取得できる値を取得することにしました。*2

MackerelAgentAndroid/FileSystem.kt at 48a67bdd9387ec519800612e847ea28bc4bacf84 · nonylene/MackerelAgentAndroid · GitHub

Android 6.0 以降では toybox のお陰でコマンド環境は向上し、大抵の必要なコマンドは使えるようになりました。

詳しくは

nonylene.hatenablog.jp

に書いています。

定期実行

Android で定期的にタスクを実行するにはいくつかの方法があります。このアプリはフォアグラウンドサービスとして常駐するようにしていますが、代表的なものは AlarmManager でしょう。

しかし、今回は Forground Service を使いました。なので通知バーに常駐しています。

これは Android 6.0 からのバッテリー節約のための新機能である Doze / AppStandby によるのものです。

developer.android.com

doze で1分間毎に実行するのが崩れる*3のは構わないのですが、問題は App Standby です。

AppStandby は、充電していない状態で一定の条件とネットワークの使用が 一日に一回 に制限されるというものです。

この "一定の条件" とは

  • The user explicitly launches the app.

  • The app has a process currently in the foreground (either as an activity or foreground service, or in use by another activity or foreground service).

  • The app generates a notification that users see on the lock screen or in the notification tray.

Optimizing for Doze and App Standby | Android Developers

となっています。

裏で毎分取得して API に送信するようなアプリはわざわざ立ち上げることも無いでしょうし、通知も出すことも無いので、バックグラウンドで走らせているとこの条件に当てはまるのは必須です。せめて端末が点いているときは送信して欲しいので、あまり推奨はされていませんが Forground Service を用いています。

Forground Service を使うと電池持ちが悪くなるように思えますが、 Forground Service で定期実行してもスリープ中は動作は遅れているので影響はほとんどありませんでした。

また、稀に CPU の使用率が -100 % になったりするみたいです。ちゃんと見ていませんが、これは CPU 使用量などの値が定期的にリセットされることによるものだと思います。結局マイナスの場合は例外を投げることにしました。

まとめ

proc を読んで Kotlin で mackerel-agent を実装していくのは楽しかったです。送信したデータを見るのも、アプリを使っている時間にメモリや CPU が使われているのが分かったりして面白いです。ぜひ入れてみてください。PR などもご気軽にどうぞ!

*1:ちなみに RxJava を Kotlin から使うと時々 SAM が辛くなったりする… https://youtrack.jetbrains.com/issue/KT-14754

*2:NDK を入れて C から System Call を呼べば取得できる気がしますが、今回は行っていません。

*3:システムがしばらく未使用状態となり Doze に入ると AlarmManager の定期実行が遅延して、許可された時間のみに行われるようになる

軽い Shell を作った

Linux Shell

↓の記事を読んで自分でもサクッと作れそうなので作ってみた。

brennan.io

名前は nsh (nonylene shell の略)。記事を参考にコードを書いていたら大体同じようなものになってしまった。

github.com

記事では C で書いているけど、nsh では Python3 で書いてみた *1。結局パースで6割ぐらい占めていて、コマンドを実行するのはこの部分だけ。

def execute_line(args):
    pid = os.fork()
    if pid == 0:
        os.execvp(args[0], args)
    elif pid < 0:
        print("error with fork!")
    else:
        while True:
            wpid, status = os.waitpid(pid, os.WUNTRACED)
            if os.WIFEXITED(status) or os.WIFSIGNALED(status): break

それなりに使えるようになったので満足。

f:id:nonylene:20161209003000p:plain

fork して execvp している以外は通常の Python の処理なので、特段困ったこともなく一時間ぐらいで作れたので良かった(小並感)。

*1:今考えたらシステムコール打ちたい時に困りそうだけど、軽く作って見る分には os.fork や os.execvp を使う程度だったので大丈夫だった。

サーバーを ConoHa に移行した

Server Rasberry pi ConoHa

サーバーを家の Raspberry Pi から ConoHa に移行しました。

理由は単純に Raspberry Pi ではいろいろするのに辛くなってきたのと、最近謎のエラー*1により SEGV することが多くなって悲しくなってきたためです。カーネルの Early Memtest 使って軽くメモリチェックしてみたり microSD の r / w チェックしたりしてみたけど特に異常も無くてよく分からなかった。

ConoHa にした理由は特に無くて、強いて言えばスペックが後から変更できることですが、だからと言って変更することはなさそう。

VPS の作成は速いし、コンソールもシンプルで使いやすいので良いと思います。

管理

ついでに Ansible で管理していたのを itamae に置き換えました。Ansible は Python のバージョンが面倒だったのですが itamae はそういった事もないので良いですね。

ConoHa は IPv6 アドレスを 17 個もらえるのですが、まだメインの 1 つしか使っていないのでどうしようかなと考えているところ…。

https://nonylene.net

ついでに HTTPS 化しました。Let's encrypt でシュッと作成して設定しただけです。/blog/ とか Mixed Contents 入りまくってますがもう使ってないので放置してます...(テンプレート直して jekyll 回すだけ)。

更新もletsencrypt renew を cron で回せばいいので楽ですね。

退役した Raspberry Pi は家の温度取得したりエアコン操作したり目覚ましにしたりできればいいなあと思っています。目覚ましは omxplayer 使ってスピーカー鳴らせばすぐできそうな気がする。

*1:relocate で死ぬ

Android Studio をビルドする

Android Studio Android

この記事は KMC アドベントカレンダー 4日目の記事です。自分は5日目だと思っていてのんびりしてました… :bow: :bow:

www.adventar.org

昨日は id:nojima718 さんの

nojima.hatenablog.com

でした!

本題

今回は Android Studio をビルドしてみます。

大体ここに従ってます。

tools.android.com

何故

Android Studio 2.2 になってから mac で外部ディスプレイに繋げたときに色が全体的に色が濃くなりました。僕の使ってる Molokai テーマには致命的で、元からコントラストが高いのにさらに濃くなると目が痛くて仕方ありません。

だからといってテーマ変えても濃くてつらく、最近は外部ディスプレイに繋がずに使っていました。

ここで Android Studio の更新ログを見ると

Improved color management on OS X

とのこと。

もしこれのせいなら、 該当する Commit を Revert して自分でビルドすればいいのでは? と思い立ち、とりあえず自分でビルドしてみることにしました。

ちなみに原因はそれではありませんでした(後述)。

ビルド

ソースコード

repo

Android では複数の git repository を一度に扱う repo というコマンドで管理することを推奨しています。*1

Repo command reference | Android Open Source Project

上に従って repo (実体は Python スクリプト) をダウンロードして実行できるようにします。

repo sync

次は適当な場所にソースコードをダウンロードします。とりあえずビルドしてみるだけなので --depth=1 としてファイル容量を抑えています。*2

$ mkdir studio
$ cd studio
$ repo init -u https://android.googlesource.com/platform/manifest -b studio-2.2.2 --depth=1
$ repo sync

大量のレポジトリが 3GB ほど降ってきます。後のビルドでも 5GB 程度は容量食うのでそれなりに確保しておきましょう。

ビルド

とりあえずビルドしてみます。ドキュメントにある通り、 ant をインストールした後

$ cd tools/idea
$ ant

とすれば後はビルド終了を待つのみ。

自分の Macbook では 20 分ほどかかりました。ポケモンピクロスでもして待っていましょう。

成果物

tools/idea/out/artifacts 以下にビルドした AndroidStudio 達が存在しています。Mac でビルドしましたが Windows / X Window 用も作られていました。

$ ls out/artifacts
android-studio-SNAPSHOT-no-jdk.mac.zip
android-studio-SNAPSHOT-no-jdk.tar.gz
android-studio-SNAPSHOT-no-jdk.win.zip
core
intellij-core-AI-145.SNAPSHOT.zip
jps
sources.zip

ここから android-studio-SNAPSHOT-no-jdk.mac.zip を解凍すれば Android Studio.app が生成されます。こうして自分専用の Android Studio が使えるようになりました。

JDK

ところでファイル名を見ると no-jdk となっており、Idea JDK が含まれていないようです。なくても困ることはないですが、推奨されているのだから使いたい。

どうやら prebuilts/studio/jdk に然るべき JDK を置けば一緒に含めてくれるみたいです。

ということで、repo のレポジトリ管理ファイルである manifest.xml に新しいレポジトリを追加します。

ローカルの manifest.repo/local_manifests に置けば良いので、.repo/local_manifests/manifest.xml

<?xml version="1.0" encoding="UTF-8"?>
<manifest>

  <remote  name="aosp"
           fetch=".."
           review="https://android-review.googlesource.com/" />

  <project path="prebuilts/studio/jdk" name="platform/prebuilts/studio/jdk" clone-depth="1" revision="studio-master-dev"/>

</manifest>

と記述して repo sync します。

すると prebuilts/studio/jdkplatform/prebuilts/studio/jdk (https://android.googlesource.com/platform/prebuilts/studio/jdk/) の studio-master-dev ブランチが clone されるので、再ビルドすると

$ ls out/artifacts
android-studio-SNAPSHOT-no-jdk.mac.zip
android-studio-SNAPSHOT-no-jdk.tar.gz
android-studio-SNAPSHOT-no-jdk.win.zip
android-studio-SNAPSHOT.mac.zip
android-studio-SNAPSHOT.tar.gz
android-studio-SNAPSHOT.win.zip
android-studio-SNAPSHOT.win32.zip
core
intellij-core-AI-145.SNAPSHOT.zip
jps
sources.zip

が生成されました。

しかし、実行してみると起動してくれない…。ブランチが違ったりしてるし諦めて JDK 無しでやりました。

その後

実行できたので早速 git のログを漁り初めたのですが、それっぽいログがない。実際にソースを見てみてもそれっぽい部分がないので困っていると、ちょうどそれらしきプラグインを発見。*3

github.com

どうやら問題は IntelliJJDK を Idea JDK (8) に切り替えたことが原因のようです。

https://youtrack.jetbrains.com/issue/IDEA-149601

...ということでプラグインを入れることで問題が解決してしまった🙏。*4

ビルドが終わった頃には studio ディレクトリは 12GB も食っていました。残り容量はなんと 500MB になり、警告も出ています。

よって、速やかに studio を削除してこの話は終わりとします。ありがとうございました。

$ df -h 
Filesystem      Size  Used Avail Use% Mounted on
/dev/disk1      186G  185G  529M 100% /
/dev/disk0s4     47G   27G   21G  57% /Volumes/BOOTCAMP
$ rm -rf studio
rm -rf studio  0.81s user 20.83s system 60% cpu 35.700 total
$  df -h
Filesystem      Size  Used Avail Use% Mounted on
/dev/disk1      186G  173G   13G  94% /
/dev/disk0s4     47G   27G   21G  57% /Volumes/BOOTCAMP

余談

  • Cross Compile...?

Windows 用はどうやって作っているのだろうと見たところ、ある程度の exe は初めから置いておいて、最後は気合の実装で作るという感じでした。すごい。他のプラットフォームも割りとそういう感じ。

intellij-community/tools/launcher-generator/src/com/pme/exe at master · JetBrains/intellij-community · GitHub

ということは何か将来個人的な変更を加えたい場合は、 CI に投げて自動的に MacWindows 用の AndroidStudio を作るといったことができそう。

  • 容量問題

ソースコードを見たいだけなら https://android.googlesource.com/platform/ から見たいものを clone するだけで良いです。ビルドにはいろいろ必要ですが、見るべきなのは一つのリポジトリ程度なので。

また、intellij 本家を GitHub で見るのもオススメです。

github.com

明日

明日(今日)は id:tyage さんです!!!楽しみですね!!!

*1:使わなければ一つ一つレポジトリを clone していくことになりめんどくさい

*2:depth を付けないと 8GB ほど埋まるので注意

*3:偶然にもこの方の IntelliJ 用 Molokai テーマを弄ったものを使っていた

*4:まだ試してませんが