Rails の問題は Rails のベストプラクティスがフロントエンドのベストプラクティスの邪魔になるどころか全く逆方向で相反してる点です。DHHの思想がフロントエンドと根本的に逆行してる。そういう人が作るフレームワークなのでwebpackerの抽象化を根本的に間違ったりする。
— prev.js (@mizchi) December 1, 2020
昨日もリプライで少し書いたけど、DHH自体が直近のHeyの開発でも明確にJavaScriptというものを触れないようにすることを是としているような主張をしているので、DHH wayが色濃く反映される以上この状態はもう避けられない気がしている
— potato4d / Takuma HANATANI (@potato4d) December 1, 2020
Railsがフロントエンドの最先端をゆく人々1から良く思われないのは事実として。
View層においてJavaScriptのFrameworkもしくはコードの比重が増えていくとともに、バックエンドまで一気通貫で同じ言語を使用できればいいのにという思想からか、加えてRailsとモダンフロントエンドとの相性がよくないからか、Railsの出番は今後減っていくだろうという意見に対しての賛同が増えてきているのを感じている。
blitz-js prisma rails 倒し方 - zenn.dev/mizchi
実際のところ、HTMLがサーバーから返ってきて、ちょっとしたインタラクションがある、なんていうシンプルなWebアプリケーションの世界ならば、JavaScriptの存在は「塩」であれたかもしれない2。だが、例えば自分が関わったOOPartsだったり、Webブラウザ上でネイティブアプリケーションのようなリッチな体験を提供したいとなると、RailsはよくてJSONを返すAPI serverとしての立場でしか居られず、フロントでの資産をバックエンドでも使用したいという判断になった場合にはそもそもRubyの居場所が無くなってしまう。
そんなJavaScript優勢となりつつある昨今においてのRailsの優位点のひとつに、世界最強のORM3であるところのActiveRecordの存在が挙げられるが、それも前述の記事において触れられているPrismaの出来が良くなればひっくり返る。
RESTやActive Record patternとMVCの組み合わせ、CoCなどの先見性からRailsが優れていた時代はあるにせよ、それが永遠に続くとは限らない。仮にRailsが本当に使われなくなった時代が来たとして、別に自分はRailsと(言い方が悪いが)心中するつもりはないし、そんなつもりの人は世界に存在しないんじゃないか。そうならないように、みんなContributeしている。
では、Webの世界においてRailsが「レガシー」という世界になったとき、自分はどうすればいいだろうか。学生時代にRubyを触ったのが始まりとなって新卒でRailsを書き始め、そこからキャリアの大部分にRubyとRailsがしっかりと喰らい付いている自分は、Rubyに対してもRailsに対しても結構愛着がある。レガシーとなってほしくはない。採用され続ける選択肢であってほしい。
もちろん、Railsの仕事が消えることはない4。COBOLの仕事が消えないのと同様、既存のビジネスがRailsで回っているところは、余程のことがない限りはRailsを採用し続けるだろうし、他言語、他Frameworkに移行するとなってもRailsの経験は必要になる。何より、BasecampとHeyがそのビジネスを続けていく限りは、それに合わせてRails自体も進化し続けるだろうし、文字通りの「Railsの死」というものが来年のうちに来ることはないだろう5。しかし、では2年後は?3年後は?その先は?
Railsでは食べていけない、というのは何も仕事が無くなることのみを指しているのではなく、単価の面においても「上がらない」という状況も含む。昨今はプログラミングスクールがいくつも開設されている。その指導内容に玉石混淆はありつつも、良いスクールを卒業した方々はとても優秀で、自分の実力が追い越される日もそう遠くない。そうなったとき、市場原理からいっても、Railsが書ける、というスキルだけでは単価は伸び悩む。
自分の能力について、今後何について伸ばしていくべきなのかというのを考え直す必要がある6。今後需要が増えるスキルというのが何かを考えたとき、以下のものが挙げられるだろう。
ここから書くスキルについて、「そんなのもう主流な技術じゃないか、何周遅れの議論をしているんだお前は」という感想はぐうの音も出ないほど正しい。これは単純に、Railsでしか職を得てこなかった人間の切迫感を文章にしただけである。ここに挙げられている技術で手を動かすことができていなければ、今後は厳しいだろうという勝手な焦りであって、世の技術者全員がすべからくここに挙げた技術を使いこなせているべきだとは言っていない。
2018年の段階で私が知らないこと — Overreacted
まずはTypeScript。これは先程の文脈の通り。Webアプリケーションに関わる開発者なら、既に「書けないとまずい」という域になりつつあるだろう。では、具体的に「書けないとまずい」とはどのレベルを指して言うのだろう。自分は、既存のコードベースにおいて、軽いバグ修正や、Frameworkを使用したシンプルなWebサイト作成くらいはできる。しかし、それ以上のこと、例えばいくつかのライブラリを組み合せて0からWebアプリを構築するということが苦手だ、というかできない。TypeScriptという言語は特殊で、JavaScriptへと変換しなければ実行できないので、必然的にtscやそれら変換処理、bundleを担うwebpackなどのツール群、要はエコシステムへの深い理解も求められる。まあ、bundlingのことを考えるとTypeScriptがJavaScriptだろうが、あまり関係はない。エコシステムへの習熟だって、どんな言語においてもそうだろう。
次にGo。今やすっかり開発環境に欠かすことのできないDockerなどのコンテナ技術において、第一級市民言語であるところのGoは、実行ファイルが1バイナリに収まるdeployの手軽さ、M1 chipから始まるであろうArmプロセッサのシェア拡大という状況におけるクロスコンパイルの容易性、Rubyなどスクリプト言語と比較したときの実行速度からみても、API backendの実装言語としてさらにシェアが上昇していくことが予想できる。
自分にとってGoと似た立ち位置にRustが存在する。Rustに関しては、所有権などこれまでのプログラミングには出てこなかった概念が登場することから自分で勝手に壁をつくってしまっているところがあり、非常に良くない。
“Web” という領域で体験を追求していくなら、Service WorkerとWebAssemblyも見逃す訳にはいかない。WebAssemblyへコンパイルできる高級言語としても、Go/Rustはどちらかでも実戦で手を動かすことができるようになっていると助かるだろう。
言語に関係しないスキルとしては、設計がある。自分はClean Architectureを知らぬ。ActiveRecordに頼り、密結合したMVCばかり書いてきた。conventionに頼ってきたので、いざ自由に書くとなったときに指針が無く、途方に暮れることがままある。アプリケーション単体の設計についてもそうだし、大量のアクセス、大量のデータを捌くためのアーキテクチャというものについても知識が薄い。PofEAAを読み直す時期なのかもしれない。
そういう焦燥感があるので、PodcastのサイトをNext.jsで構築したりと、プライベートの時間はTypeScriptを書くことにしている。新しく触れる技術である程度の大きさのものを作ろうとすると、その技術について必要な知識のうち、自分に不足しているものがわかってくる。自分の場合、JavaScriptの場合はasync/awaitやPromiseが絡む、非同期処理が前提となった構造のコードが全然書けない。値を取得しようとしてはawaitを付け、しかしそこはasync functionではないというエラーが出る。こういうことを幾度となく繰り返している。Goの場合はgoroutineやchannel、contextについて全く無知である。存在は知っているが、どういうものか理解できていないし書けもしない。
フリーランスという立場上、技術についていけなくなり食い扶持を失うことを強く恐れている。別に社員時代にぬるま湯に漬かっていた訳ではなく、より一層その意識が強くなったというだけのこと。そんな中、自分が主戦場としている分野が、先駆者達からはいずれ避けるべきものになるという評価にあるということを知ると、反発したい気持ちになってしまうのは当然である。しかし、例えそれがポジショントークに過ぎず、新規事業においてはまだRailsが主選択肢たりえるとしても、一旦はその主張が正しいとしたときに自分はどういう方向に進むべきかというのを考えておきたい。これはその2020年末版。
エンジニアを仕事にしてるみんな、なにをモチベーションにして、なんのために仕事や勉強してるの?
— ダンボー田中📦 (@ktanaka117) December 9, 2020
書いている途中にこのツイートが目に入り、そういえばこの記事だと食べていけるかどうかしか気にしていなくて目的が不明確だなと思ったので自分のモチベーションについても少し書く。
自分のモチベーションは「自分が作りたいものを作れるようになりたい」で、そのために技術の幅を広げたり、仕事や勉強をしているつもりだ。ある「作りたいもの」が出てきたとき、それが実際に可能かどうかの判断、逆にある技術を知ったときに「こういう便利なものが作れるんじゃないか」という思考ができるようにしておきたい。そして実際に作れるようになりたい。
プライベートの時間に書いているというTypeScriptも、自分の日々の困り事を解決するためのアプリケーションを作成するのにJavaSctiptが適していて、ならばとTypeScriptで書いているという経緯がある。 フリーランスになってから若干、「仕事にできるかどうか」が判断基準に加わったかもしれないが、原動力は変わっていないと思っている。もちろんお金は欲しいが。
発言を引用した2人に共通する点として、「抱えているユーザー数が多い」というのがあることに書きながら気づいた。 ↩
RailsDM 2019における講演より。 https://togetter.com/li/1330578 JavaScriptは塩のようなものであるという意見。 ↩
誰かがそう言ってたような気がするんだけどソースが見つからなかった ↩
Railsしか書けないプログラマーのために社内にRailsプロジェクトが用意されているという「Railsが福利厚生」という状況になったら嫌だな、と思った。 ↩
とは言ったものの、DHHの考えがモダンフントエンドと乖離したままRailsが成長するのであれば、年々その開きは大きくなっていくだろうが。 ↩
いつだって考え直すべきだが? ↩
2020年のフロントエンドエンジニアの技術スタックの一例 | potato4d D(iary)
この記事と、TLで「これのバックエンド版が見たい」という発言に触発されたので書いてみます。口語体と文語体が入り乱れてるのは許してください。
冒頭のグラフはwakatimeで生成した今年1年間のプログラミング言語使用率です。2位はTypeScript、3位はTerraform、4位はYAMLでした。
フリーランスで、主にRailsやAWSを使用しているサービスの運用、開発に関わっています。いくつもの会社を見てきた訳ではなく、数社に深く関わっている1都合上、視野が狭いかもしれません。
公開している成果としては クラウドゲーミング最新開発事例 - #CEDEC2020 - Speaker Deck があります。
長年RubyとRailsを書いてきたので、技術スタックがそのあたりに偏っています。
太字は特記事項があるものです。また、挙げている技術は「ある程度触った」くらいで記載しており、全てに習熟している訳ではありません2。
各項目について、上のほうがよく使っているものという傾向がありますが、厳密ではないです。
主軸としてはRuby/Rails/AWSの主要コンポーネント というところでした。
今年も僕の主戦場はRubyでした。あと数年はRuby以外を軸に仕事をしていくことになるんじゃないかな……?ただ2020年らしいことは全くしていなくて、例えばRuby 2.7で導入された機能、Pattern matchingやnumberd parameterは使った記憶がないです。(Pattern matchingは使おうとしたが、愚直に書いたほうが短かくなったので断念)
今年はJavaScriptをBrowser console以外で書いた記憶がありません。
キャリアがRailsから始まっているので、思い入れがあります。また、新しい現場でもすぐにコードを書けるところは助かっています。とは言えコードが肥大化してくると何がどうやって動いているのか把握するのには時間がかかりますが、これはまあどんなものでもそうでしょう。 業界においてRailsが優位にあるとすれば、その実装速度の早さと、コードやデータ量、ビジネスロジックがある程度成長しても耐えられるRubyの表現力及びActiveRecordの底力があるように思います。あと数年は新規実装としてのRailsの立場は残っていると思います。それ以降はまた状況が変わっていると思うのでわかりません。
これを1つにまとめたのは、主にRails appに組み込まれたものを書いていたためです。TypeScriptもそうなのですが、JavaScriptに関しては既存のコードベースに手を入れるということはできても、設計を含めた0から書き始めるということができていない、できないのが自分の弱みだと感じているので、プライベートの時間ではキャッチアップをしています。
コンテナ技術はもう前提条件と言えるくらいには利用が広がっていますね。ECSやKubernetesをはじめとしたエコシステムの発展もあり、開発環境から本番環境までDockerで環境が統一されていることの便利さ、docker-compose一発で開発環境が準備できる手軽さは一度体験するともう戻れないと言っていいです。
Docker for Macが遅い遅いと言われ続けている昨今ですが、僕の環境においては許容範囲というか、あまりそこで困らなかった印象があります。大分改善されているのではないでしょうか。それとも僕が鈍いのか。
業務としては一切Kubernetesを使用しませんでした。関わっている範囲だとこの2つで十分要求を満たすことができていました。
これは、今年春から JetBrains All Products Pack を契約して使い始めました。これについては1本ブログ記事を書くつもりで下書きを温めていたのですが、この際ですからそれから引用する形にします。
Rubyを第一言語としていますが、PythonやGoなどで書かれたAPI serverの開発を素早く完了させる必然性が増えました。 これまでは腰を据えてプロダクトの全容をあらかた把握した上で開発を進めることが多かったのですが、とにかく素早く問題点を発見して修正する、それもあまり習熟していない言語で、という状況に置かれることが増えてきました。 それに、習熟しているつもりのRubyですら、今までにない大規模なアプリケーションの機能開発においては全容を把握しておくということが困難です。
また、DB clientとしてのDataGripが非常に便利なのは嬉しい誤算でした。
あまりRails界隈でこういう記事をみかけない気がするので、他の皆さんはどうなのか知りたいので書いてほしかったり……年の瀬に1年を振り返ってみるのは楽しかったです。
皆さん、Minecraftしてますか。サーバー、立ててますか。監視、してますか?
この記事では、2020年10月末に爆誕したMinecraftサーバー「ナ組サーバー」について、僕が勝手に監視している方法について現時点での構成をまとめておくものです。
なんか突然GCPを触りたくなったのでナ組マイクラ鯖を立てた
— 蜘蛛糸まな🕸️ / HolyGrail (@HolyGrail) October 30, 2020
「マイクラサーバー監視2020」と題していますが、僕はこれまでにMinecraftのサーバーを運用した経験はありません。何ならここで言及するサーバーについても、構築したのは蜘蛛糸まな氏です。 単純に、今Minecraftサーバーの監視をするならどうするか、ということについて述べています。過去のベストプラクティスは知りません。
ナナメさん (@7name_) とそのお友達が遊んでいるMinecraftサーバーです。構築は前述の通り、蜘蛛糸まな氏です。GCP上に構築されています。
この動画の冒頭でも成り立ちについては述べられています。
僕は、このサーバーへのアクセス権を頂いているので、そこから色々な作業を行い監視環境を構築してみました。以下は行なったことについてのまとめとなります。
まず、サーバーの監視を行うときに「何でモニタリングするか」というのが大きな部分を占めるでしょう。SaaSであればMackerelやDatadog、GCP上のサーバーであればCloud Monitoringを使っておくのもありでしょう。自分で構築するのであれば、PrometheusやZabbixやNagiosなどの選択肢もあります。
今回は、Grafanaとの連携で見た目が良いこと、新しめのものであることから、Prometheusで監視、Grafanaで可視化という構成を採用しています。
先ほどPrometheusを導入することに決めた、と書きましたが、そもそもMinecraftサーバーの状態をPrometheusから取得することができなければ無意味です。
Minecraftに導入できるPrometheus exporterはいつくか存在します。
star数、ドキュメントの量から、 sladkoff/minecraft-prometheus-exporter を導入することに決めました。このようなexporterが存在していたことも、Prometheus採用の一因です。
MinecraftサーバーへのPluginの導入については、各サーバーによってまちまちだと思うので詳しくは触れませんが、今回は特定のディレクトリ以下へplugin本体を展開しておくだけで済みました。
plugin自体の設定はYAMLで記述することができるので楽ですね。いい感じにやっておきましょう。
今回、MinecraftとPrometheus、及びGrafanaは同居させず、別のサーバーに分離することにしました。どちらもワンバイナリで動作するので、サーバーの適当なディレクトリに置いて起動させるだけで構築は大体完了です。追加でやることは、Prometheusのscraping_configs
にMinecraftサーバーを追加で指定することと、Grafanaのdata sourceにPrometheusを追加すること程度です。
Webサーバー運用についてのHTTPS化などのエトセトラはここで詳細に記述することはしません。1
ここまででGrafanaを使用してダッシュボードを作成することができるようになっていはずなので、あとはチマチマと必要そうなグラフを並べたダッシュボードを作成します。ここで作成したダッシュボードの変化を見ているときが一番楽しいですね。
このダッシュボードについてですが、MinecraftサーバーのIPアドレスがわかってしまうため、一般に公開はしていません。2
サーバーというものは、いつ止まってもおかしくありませんし、止まってしまっているときには何らかの方法で通知が欲しいものです。
Prometheusによるアラートといえば、Alertmanagerがあります。Alertmanagerは様々な手段でアラートを送信することができますが、一番手軽なものはWebhookによる通知なのではないでしょうか。そこで、Webhookによる通知をAlertmanagerから送信しようとしてみましたが、これがDiscordのWebhookには対応していませんでした。3
Alertmanager integration with Discord · Issue #1365 · prometheus/alertmanager
ナ組はコミュニケーションをDiscord上で行なっているので、そこに通知を集約できないのはつらいです。
しかし幸いなことに、Grafana自体のArert機能においては、DiscordのWebhookをサポートしていることが判明しました。
https://grafana.com/docs/grafana/latest/alerting/notifications/#list-of-supported-notifiers
そして、指定した閾値に達した場合に次のようにアラートをWebhook経由でDiscordに飛ばせることが確認できました。
ここでは、Minecraftが使用しているJVMのThread数が異常な値になった場合に通知をするという単純なルールになっています。
このようにしてナ組のMinecraftサーバーは監視されています。Minecraftは10年近く遊ばれているゲームであり、インターネット上の情報も豊富ではあるものの古くなっている記述が沢山あります。監視に関する情報も、Prometheusを使用しているような比較的新しいものは見当らなかったので、こうやって記事にしてみました。
また、この記事は whywaita Advent Calendar 2020 の12/2を担当する記事となります。whywaitaさんは現在プライベートクラウドに関する仕事に関わっていたり、過去にPrometheus clusterを破壊するなどの輝かしい功績のあることですから、今回僕の行なった作業についても、より良い方法を知っているものと思います。一度、対面で教えていただきたいものですね。
certbotを使用しています。それ以外の詳細については面倒になったので書きません。 ↩
わからないようにできないか?とがんばってみましたが僕には無理でした。 ↩
代替手段として https://github.com/benjojo/alertmanager-discord がありますが、コンポーネントを増やすことになるので導入はしませんでした。 ↩
2017年からS3 Static website hostingとCloudFrontの組み合わせで運用してきたこのブログを、AWS Amplifyによる運用へと変更しました。
記事、画像が増えてくると、S3 syncに時間がかかるようになってきます。大体10分前後かかるということで、ちょっとなんとかしたいなと思っていました。また、deployに使用している middleman-s3_sync gem の開発が停滞しているように見えるのも脱S3 syncの要因のひとつです。良く言えば安定している、と言えなくもないし、嫌ならS3 syncによるdeployをやめればいいという話ではあるかもしれませんが……
大した管理をしていたわけでもないですが、S3とCloudFrontの2つのサービスによる構成から、Amplifyだけを見れば良くなるので少し楽になります。
Amplifyも裏はCloudFront+S3っぽいし、何度か計測してると逆転することもあったので有意差はなさそう https://t.co/wa6hgje5PH
— うなすけ (@yu_suke1994) July 24, 2020
という感じで、Amplify consoleでもCloudFrontのdistrinution URLがCNAMEとして指定する値と出てくるように、CloudFrontがリクエストを受けるという構成は変化しないことがわかっていました。なので大した影響はないだろうと判断しました。
元々のCloudFrontで握っているTLS証明書との兼ね合いか、一度既存のCloudFrontのdistributionを削除(無効化ではだめだった)してからでないとAmplify console側で以前のものと同じドメインを設定させることができないようです。
そのためにダウンタイムは必ず発生してしまいます。
それがどのくらいかかるかについてですが、ダウンタイムは体感では5分もなかったように思います。
(特に予告なくシレッと移行作業を行いました)
Kaigi on Railsという初めてのイベントに参加していただいた皆様、素晴らしい発表をしてくださった発表者の皆様、惜しくも採択とはいかなかったものの、Proposalを提出してくださった皆様、協賛していただいたスポンサーの皆様、本当にありがとうございます。
Kaigi on RailsではProposal提出まわりと、yrindaさんと一緒に配信・動画まわりを担当していたうなすけです。
https://kaigionrails.org/team/
この記事では、完全オンライン開催にあたっての配信まわりについて、裏側がどうだったかについて触れるものです。リアル開催からオンライン開催に切り替えた苦労話なんかはチーフオーガナイザー氏が書いてくれたのでここでは触れません。
Kaigi on Railsのチーフオーガナイザーを務めました - okuramasafumiのブログ
全てをすっ飛ばして、本番の配信環境をまず紹介します。
本番配信はメインのyrinda houseとサブのunasuke houseの2系統を用意して冗長化していました。メインで何か問題が発生したらサブ側の配信URLを広報し、そちらを視聴してもらうイメージでした。そのために2人が同じ内容をメインとサブで配信する必要があり、シーンの切り替えタイミング等は当日全員が入っているDiscordでタイミングを合わせて行っていました。
ライブ発表の場合の構成は、アプリケーション側のZoomを発表者の映像用に、モニターを1枚占有した状態のブラウザからZoomに入り発表資料用に、そしてこれらをOBSに流しこみました。 その他に、それらの画面に重ねる発表情報の枠、Zoomからの音声出力キャプチャなどの設定をライブ発表用に用意しました。
動画発表の場合は、発表する方々から事前に提出いただいた動画を、同様の見た目になるよう発表用の枠をかぶせて書き出し、当日は再生するのみの運用になるようにしました。
当日のタイムテーブルの進行については、発表者、休憩それぞれでOBSのシーンを作成し、基本的にはそれを上から切り替えていくという省エネ運用になるように配信担当側で協力して作り込んでいきました。
まず当初、Kaigi on Railsはリアル会場で開催するつもりでした。ところが時世が変化し、どうしてもリアルで集まっての開催ができなくなり、他の多くのイベントと同様にインターネット上での配信による開催に切り替えることとなりました。
オンライン開催となったとき、ではどのような形式で開催するのかということが問題になってきます。配信プラットフォームはどれにするのか、どのように配信を行うのか、日付、時間帯、各トークの長さ、配信を委託するのか自分達でやるのか……
何度か話し合いを行った結果、運営チームにある程度の設備があることから、配信は自分達で行うことに、プラットフォームは強い決め手がある訳ではないですが、YouTube Liveを選択しました。議事録を振り返っていたのですが、全部録画もしくは配信ではなく、録画と配信を希望制にした理由は残っていませんでした。議論するまでもなく、そう決まったのだと思います。
オンラインイベントにすると決まった訳ですが、我々には知見が全くありませんでした。そこで、Kaigi on Railsそのものの宣伝も兼ね、「Kaigi on Rails new」というイベントを開催し、そこでオンラインイベントの “感じ” を掴むことにしました。
Kaigi on Rails new - Kaigi on Rails | Doorkeeper
結果様々なKPTを洗い出すことができ、本番までに何を準備すればよいか、どういう運用にすればいいのかが見えてきました。
その後は提出してもらった動画を編集し、書き出したものを共有したりで動画の準備は進んでいきます。機材がない方のために、事前に録画会をやろうという企画もありましたが、希望者少数のため実現しませんでした。
配信勢の皆さんに対しては、直前に配信テスト会を開催してZoomでの登壇の感じをつかんでもらう一方、配信スタッフ側でも実際にどういうオペレーションになるかの細部を詰めました。具体的には登壇者交代がどうなるか、などです。
そして、前述のような構成になりました。
事前に頂いていた動画の音量は正規化していたのですが、配信側の音量が想定以上に大きく、相対的に動画のほうが小さく聞こえてしまう問題がありました。アーカイブ公開版では再度正規化を行って、できる限りバラつきがないようにしてあります。
振り返ってみると、配信にあたって回線を冗長化するよりは、オペレーターを冗長構成にすべきだったかもしれません。要するに配信環境を1箇所に構築し、そこに配信担当者が集合するという体制です。今回は何事もなく終わりましたし、何事があってもイベントが進行するように準備をしましたが、次回どうするかはまた考える必要があります。何事もそうですが。
Kaigi on Railsは来年も開催することが決定となっていますが、「やる」以外は何も決まっていません(本当に、何も……)。配信環境についても、今回の反省を活かして何かしらの改善を行っていきたいですね。例えばクラウド上からの配信にしたり、配信用機材を導入したりといったことが考えられます。1年かけて、ゆっくり考えていきたいです。
この記事はHHKB Professional HYBRID Type-SとHelixPicoとChoco60で書かれました。
タイトルに書いてあるように、travisの org から com に移行した、という話なのですが、そこに至るまでに様々な苦労をしたのでお願いだから最後まで読んでねぎらってください。
ItamaeのCIには、travis-ci.org を使用していました。
CIというものは、たまに落ちることがあります。その原因は内部のコードが悪い場合や外部要因である場合などが挙げられます。ItamaeのCIも、例によってたまに落ちることがありました。
たまに落ちることはよくて、落ちたCIを再度実行して通れば良いのです。しかし問題は、「落ちたtestを再度実行することができない」 というところにありました。
いつからなのかは不明ですが、travis-ci.org では失敗したtestを再実行するUIが消えてしまっており、空commitを積むなどしなければ同じコードでのtestの再実行ができなくなっていました。
この “More Options” 内の “Requests” は一見再buildのリクエストのように見えますがそのような挙動はしません。
これは非常につらい。ので、CIをtravis-ci.orgではない別の何かに乗り換えることにしました。
CIについて話す前に、Itamaeのtestにおける対象の組み合わせについて触れておきます。CIでは以下のような組み合わせに対してテストを行っています。
unitなのかintegrationなのか、というのは、unitはItamae gemの実装に対してテストを行い、integrationは用意されているrecipeをDocker containerに適用して意図した状態にできているかを確認するテストとなっています。
(以前はDigitalOceanで起動したインスタンスに対してrecipeの適用を行っていました)
さて、OSSなら無料で使用できるCIサービスというものはいくつかありますが、代表的なものに以下の2つが上げられると思います。
新しもの好きということもあり、まずGitHub Actionsを試してみることにしました。
まずGitHub Actionsでのunit testは以下のようなYAMLで実行できます。
name: "unit test on ubuntu"
on:
push:
branches: "*"
jobs:
test:
strategy:
matrix:
os: [ubuntu-16.04, ubuntu-18.04]
ruby: [2.2, 2.3, 2.4, 2.5, 2.6, 2.7, head]
fail-fast: false
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
- run: bundle install
- run: bundle exec rake spec:unit
簡単ですね。これで問題なくunit testはpassするようになります。
しかし、GitHub Actionsでは integration testがどうしても成功しないという問題に直面してしまいました。
なぜ通らないのかの原因がわかればいいのですが、これが全くの不明でした。上記画像にあるように落ちているものもあれば通っているものもあり、全体的に不安定になっています。落ちている原因そのものはtmpディレクトリに書き込みができなくなって落ちているのですが、なぜそうなっているのかはわかりません。
友人に相談したところ、興味深い事実が明らかになりました。
「straceをonにするとpassする」 のです。
straceは何をするツールなのかと言うと、システムコールをトレースするツールです。それを有効にしただけで通るようになる、つまり実行に対してオーバーヘッドがかかるとtmpへの書き込みが成功してtestが成功する、という現象が発生しているようなのです。
なぜこのような状況になっているのかの調査はちょっとハードルが高すぎるので、GitHub Actionsの採用は見送ることにしました。
(self-hosted runnerを使用することでGitHub Actionsで使用されているインスタンスに特有の問題なのかを調査することもできますが、そこまでの元気は出ませんでした)
次に手を出したのはCircleCIです。CircleCIでは最近matrix jobを記述できるようになり、複数の条件を組み合わせたテストが書けるようになりました。
https://circleci.com/blog/circleci-matrix-jobs
とはいえ複雑なYAMLになってしまいました。
version: 2.1
orbs:
ruby: circleci/ruby@1.0.7
executors:
docker:
docker:
- image: cimg/base:stable
docker-1804:
docker:
- image: cimg/base:stable-18.04
machine:
machine:
image: circleci/classic:201808-01
jobs:
unit:
parameters:
ruby-version:
type: string
exec:
type: executor
default: ""
executor: << parameters.exec >>
steps:
- checkout
- ruby/install:
version: << parameters.ruby-version >>
- run: gem install bundler --version 1.17.3 --force
- run: bundle install -j4
- run: ruby -v
- run: bundle exec rake spec:unit
integration:
parameters:
ruby-version:
type: string
executor: machine
# executor: docker
steps:
- checkout
- setup_remote_docker:
version: 18.06.0-ce
- ruby/install:
version: << parameters.ruby-version >>
- run: gem install bundler --version 1.17.3 --force
- run: bundle install -j4
- run:
command: |
ruby -v
export PATH=$HOME/.rvm/bin:$PATH
ruby -v
- run: bundle exec rake spec:integration:all
unit-jit:
parameters:
ruby-version:
type: string
executor: docker
steps:
- checkout
- ruby/install:
version: << parameters.ruby-version >>
- run: gem install bundler --version 1.17.3 --force
- run: bundle install -j4
- run: ruby -v
- run: RUBYOPT=--jit bundle exec rake spec:unit
integration-jit:
parameters:
ruby-version:
type: string
# executor: machine
executor: docker
steps:
- checkout
- setup_remote_docker:
version: 18.06.0-ce
- ruby/install:
version: << parameters.ruby-version >>
- run: gem install bundler --version 1.17.3 --force
- run: bundle install -j4
- run: ruby -v
- run: RUBYOPT=--jit bundle exec rake spec:integration:all
workflows:
version: 2
all-test:
jobs:
- unit:
exec:
name: docker-1804
matrix:
parameters:
ruby-version: ["2.3"]
- unit:
exec:
name: docker
matrix:
parameters:
ruby-version: ["2.4", "2.5", "2.6", "2.7"]
- integration:
matrix:
parameters:
ruby-version: ["2.3", "2.4", "2.5", "2.6", "2.7"]
# all-test-with-jit:
# jobs:
- unit-jit:
matrix:
parameters:
ruby-version: ["2.6", "2.7"]
- integration-jit:
matrix:
parameters:
ruby-version: ["2.6", "2.7"]
CircleCIでItamaeのtestを実行するにあたり、いくつかの壁に突き当たったので紹介します。
CircleCIにはOrbという仕組みがあり、汎用的な手順ならYAMLに記述しなくてもOrbを導入することによって記述を省略できる仕組みがあります。
Rubyの実行環境を用意するOrbとして、CircleCIが公式で用意しているのが circleci/ruby
です。
このOrbですが、bundlerをインストールするのにGemfile.lockの存在をアテにしている部分があります。
Itamaeはgemであり、Gemfile.lockをリポジトリに含んではいません。ではこのOrbは使えないのかというとそうではなく、よく読むとわかるように bundler-version
を渡すとGemfile.lockがなくても指定したversionのbundlerをインストールできるように見えます。
しかし既存のbundlerを上書いてしまってよいかの確認ダイアログが出るため、どちらにしろ上手く動きません。
結局、bundlerのinstallは手で --force
を付与したコマンドを実行させることで回避しました。
unit testはこれでうまくいくようになりましたが、dockerコマンドを使用する都合上、machine executorで実行しているintegration testでエラーが出るようになりました。それもJITを有効にしている場合のみ失敗します。
エラーを見ると、--jit
というオプションが不正というものでした。しかし、このスクリーンショットで使用しているRubyは2.6であり、--jit
は有効なはずです。
不審に思い、Rubyのversionも出力するようにしたのが以下のスクリーンショットです。
なんと、Ruby 2.6 をインストールしたはずなのに、使用されているのはRuby 2.3になっています。これはRVMのPATHをこねくりまわしてもどうしても解決することができませんでした。どうしてこんなことになるのでしょう。
二進も三進もいかないので、remote dockerを使用してみることにしました。これは、unit testで使用しているDocker executorであればRubyのversionが正しく設定できているので、その環境においてdockerコマンドを使用できるようにするための仕組みです。
デプロイする Docker イメージを作成するには、セキュリティのために各ビルドに独立した環境を作成する特別な setup_remote_docker キーを使用する必要があります。 この環境はリモートで、完全に隔離され、Docker コマンドを実行するように構成されています。 ジョブで docker または docker-compose のコマンドが必要な場合は、.circleci/config.yml に setup_remote_docker ステップを追加します。
しかし、やはりエラーになってしまいます。これはテストの過程においてItamaeのコードをまるっとdocker container側にvolume mountしている部分があるのですが、remote dockerがvolume mountをサポートしていないためにエラーになります。
ジョブ空間からリモート Docker 内のコンテナにボリュームをマウントすること (およびその逆) はできません。 https://circleci.com/docs/ja/2.0/building-docker-images/#section=configuration
(docker cpはできますが)
という様々な躓きがあり、CircleCIを使うのはあきらめようと考えました。 (あきらめるまでに60回以上CIを回しています)
ではどこのCIを使おうか、となるのですが、そもそもtravis-ci.comへの統合が進められていることに気付きました。
2018年に、Travis CIはGitHub Appsとして導入できるようになり、OSSも travis-ci.com
でCIを実行できるようになっています。
2020年の今でも travis-ci.org
を使用しているので、試しに travis-ci.com
に移行してみることにしました。
この移行はとても簡単で、 travis-ci.com
にGitHub accountでログインしてMigrateボタンをクリックするだけです。
結果ですが、このように “Restart build” ボタンが出現しており、テストの再実行ができるようになりました!
という諸々があり、ItamaeのCIは travis-ci.com
に移行することになります。
これについて、第53回情報科学若手の会のLT枠で発表した資料を貼っておきます。
先日、きりきりやままさんがこのような記事を公開していました
UTF-8 の文字列をできる限り Shift_JIS に変換したい - きりきりやま
それでは実際にそのような文字列変換を行うにはどうすればよいのか、またコメントでiconvについて触れられていたので、この記事ではUnicodeにおけるNFKC正規化をどうやって行うのか試してみることにしました。
僕にとって文字列処理といえばRubyなので、まずは以下のようなscriptを書いてみました。
puts "\u304c"
puts "String#encode('Shift_JIS') => #{"\u304c".encode('Shift_JIS').inspect}"
puts "codepoints => #{"\u304c".codepoints}"
puts "NFKC normalized codepoints => #{"\u304c".unicode_normalize(:nfkc).codepoints}"
puts "Encode to SJIS with NFKC => #{"\u304c".unicode_normalize(:nfkc).encode('Shift_JIS').inspect}"
puts "=" * 20
puts "\u304b\u3099"
puts "String#encode('Shift_JIS', undef: :replace) => #{"\u304b\u3099".encode('Shift_JIS', undef: :replace).inspect}"
puts "codepoints => #{"\u304b\u3099".codepoints.inspect}"
puts "NFKC normalized codepoints => #{"\u304b\u3099".unicode_normalize(:nfkc).codepoints}"
puts "Encode to SJIS with NFKC => #{"\u304b\u3099".unicode_normalize(:nfkc).encode('Shift_JIS').inspect}"
puts "=" * 20
puts "\u0063\u006d"
puts "String#encode('Shift_JIS') => #{"\u0063\u006d".encode('Shift_JIS').inspect}"
puts "codepoints => #{"\u0063\u006d".codepoints}"
puts "NFKC normalized codepoints => #{"\u0063\u006d".unicode_normalize(:nfkc).codepoints}"
puts "=" * 20
puts "\u339d"
puts "String#encode('Shift_JIS', undef: :replace) => #{"\u339d".encode('Shift_JIS', undef: :replace).inspect}"
puts "codepoints => #{"\u339d".codepoints}"
puts "NFKC normalized codepoints => #{"\u339d".unicode_normalize(:nfkc).codepoints}"
$ ruby script.rb
が
String#encode('Shift_JIS') => "\x{82AA}"
codepoints => [12364]
NFKC normalized codepoints => [12364]
Encode to SJIS with NFKC => "\x{82AA}"
====================
が
String#encode('Shift_JIS', undef: :replace) => "\x{82A9}?"
codepoints => [12363, 12441]
NFKC normalized codepoints => [12364]
Encode to SJIS with NFKC => "\x{82AA}"
====================
cm
String#encode('Shift_JIS') => "cm"
codepoints => [99, 109]
NFKC normalized codepoints => [99, 109]
====================
㎝
String#encode('Shift_JIS', undef: :replace) => "?"
codepoints => [13213]
NFKC normalized codepoints => [99, 109]
https://wandbox.org/permlink/CQaSM6ffOHc0zLu6
Rubyにおいては、Unicode正規化を行うには String#unicode_normalize
によって行うことができます。その際にoptionとして正規化の形式を指定することができます。とても簡単ですね。
https://docs.ruby-lang.org/ja/latest/class/String.html#I_UNICODE_NORMALIZE
import unicodedata
print('\u304c (U+304c)')
print('codepoints => ', end='')
for char in '\u304c'.strip():
print(hex(ord(char)) + ' ' , end='')
print()
print('NFKC normalized codepoints => ', end='')
for char in unicodedata.normalize('NFKC', '\u304c').strip():
print(hex(ord(char)) + ' ' , end='')
print()
print('=' * 20)
print('\u304b\u3099 (U+304b U+3099)')
print('codepoints => ', end='')
for char in '\u304b\u3099'.strip():
print(hex(ord(char)) + ' ' , end='')
print()
print('NFKC normalized codepoints => ', end='')
for char in unicodedata.normalize('NFKC', '\u304b\u3099').strip():
print(hex(ord(char)), end='')
print()
print('=' * 20)
print('\u0063\u006d (U+0063 U+006d)')
print('codepoints => ', end='')
for char in '\u0063\u006d'.strip():
print(hex(ord(char)) + ' ' , end='')
print()
print('NFKC normalized codepoints => ', end='')
for char in unicodedata.normalize('NFKC', '\u0063\u006d').strip():
print(hex(ord(char)) + ' ' , end='')
print()
print('=' * 20)
print('\u339d (U+339d)')
print('codepoints => ', end='')
for char in '\u339d'.strip():
print(hex(ord(char)) + ' ' , end='')
print()
print('NFKC normalized codepoints => ', end='')
for char in unicodedata.normalize('NFKC', '\u339d').strip():
print(hex(ord(char)) + ' ' , end='')
print()
https://wandbox.org/permlink/cMc7S5blWLZLLObD
Pythonにおいては、unicodedata
モジュールをインポートすることによって使用できる unicodedata.normalize
により、形式を指定して正規化を行うことができます。
unicodedata — Unicode データベース — Python 3.8.5 ドキュメント
package main
import (
"fmt"
"strings"
"unicode/utf8"
"golang.org/x/text/unicode/norm"
)
func printCodepoints(str string) {
fmt.Print("codepoints => ")
for i, w := 0, 0; i < len(str); i += w {
runeValue, width := utf8.DecodeRuneInString(str[i:])
fmt.Printf("%U ", runeValue)
w = width
}
fmt.Print("\n")
}
func main() {
fmt.Println("\u304c (U+304c)")
printCodepoints("\u304c")
fmt.Print("NFKC normalized ")
printCodepoints(norm.NFKC.String("\u304c"))
fmt.Println(strings.Repeat("=", 20))
fmt.Println("\u304b\u3099 (U+204c u+3099)")
printCodepoints("\u304b\u3099")
fmt.Print("NFKC normalized ")
printCodepoints(norm.NFKC.String("\u304b\u3099"))
fmt.Println(strings.Repeat("=", 20))
fmt.Println("\u0063\u006d (U+0063 U+006d)")
printCodepoints("\u0063\u006d")
fmt.Print("NFKC normalized ")
printCodepoints(norm.NFKC.String("\u0063\u006d"))
fmt.Println(strings.Repeat("=", 20))
fmt.Println("\u339d (U+339d)")
printCodepoints("\u339d")
fmt.Print("NFKC normalized ")
printCodepoints(norm.NFKC.String("\u339d"))
}
https://play.golang.org/p/xG255G32mlJ
Goでは、norm
packageを使用することで正規化を行うことができます。
// function from https://jsprimer.net/basic/string-unicode/#code-point-is-not-code-unit
function convertCodeUnits(str) {
const codeUnits = [];
for (let i = 0; i < str.length; i++) {
codeUnits.push(str.charCodeAt(i).toString(16));
}
return codeUnits;
}
console.log('\u304c (U+304c)')
console.log('codepoints => ' + convertCodeUnits('\u304c'))
console.log('NFKC normalized codepoints => ' + convertCodeUnits('\u304c'.normalize('NFKC')))
console.log('=' .repeat(20))
console.log('\u304b\u3099 (U+304b U+3099)')
console.log('codepoints => ' + convertCodeUnits('\u304b\u3099'))
console.log('NFKC normalized codepoints => ' + convertCodeUnits('\u304b\u3099'.normalize('NFKC')))
console.log('='.repeat(20))
console.log('\u0063\u006d (U+0063 U+006d)')
console.log('codepoints => ' + convertCodeUnits('\u0063\u006d'))
console.log('NFKC normalized codepoints => ' + convertCodeUnits('\u0063\u006d'.normalize('NFKC')))
console.log('='.repeat(20))
console.log('\u339d (U+339d)')
console.log('codepoints => ' + convertCodeUnits('\u339d'))
console.log('NFKC normalized codepoints => ' + convertCodeUnits('\u339d'.normalize('NFKC')))
console.log('='.repeat(20))
https://wandbox.org/permlink/JLQH8LasdQo9ewgS
JavaScriptでは、 String.prototype.normalize()
によって正規化を行うことができます。
それでは他のツールはどうでしょうか。
nkfはNetwork Kanji Filterの略で、古くからある文字コード変換ツールです。
https://ja.osdn.net/projects/nkf/
Rubyはnkfを同梱しているので、手軽に試すことができます。今回は一度Shift_JISに変換してからUTF-8に戻すことで、正しく変換できているかを確認してみます。
require 'kconv' # kconvはnkfのラッパーです
puts "\u304c (U+304c)"
puts "Endoce to SJIS by nkf => #{"\u304c".tosjis.inspect}"
puts "=" * 20
puts "\u304b\u3099 (U+304b U+3099)"
puts "Endoce to SJIS to UTF-8 by nkf => #{"\u304b\u3099".tosjis.toutf8}"
puts "=" * 20
puts "\u0063\u006d (U+0063 U+006d)"
puts "Endoce to SJIS by nkf => #{"\u0063\u006d".tosjis.inspect}"
puts "=" * 20
puts "\u339d (U+339d)"
puts "Endoce to SJIS to UTF-8 by nkf => #{"\u339d".tosjis.toutf8}"
$ ruby script.rb
が (U+304c)
Endoce to SJIS by nkf => "\x{82AA}"
====================
が (U+304b U+3099)
Endoce to SJIS to UTF-8 by nkf => 縺九y
====================
cm (U+0063 U+006d)
Endoce to SJIS by nkf => "cm"
====================
㎝ (U+339d)
Endoce to SJIS to UTF-8 by nkf => ㎝
このように、「が」(U+304B U+3099
) の変換に失敗していることがわかります。そもそもnkfはUnicodeにおける正規化形式を指定できるのでしょうか。
nkfは2006-03-27にリリースされた 2.0.6 以降 (正確には2.0.6-beta2以降) においてUnicodeの正規化に対応するようになりましたが、「UTF8-MACの範囲のみ」と明言されています。
ここでの UTF-8-MAC
は、macOSがAPFS以前1に採用していた HFS+ というファイルシステムにおいて使用されている正規化形式の通称2で、一見NFD形式のようで互換性のない正規化3を行っています。
nkfは入力においてのみUTF-8-MACを受け付けるようになっているようで、他の正規化形式に対応していません。
nkfにオプションから文字コードを指定した変換をして確かめてみましょう。
# nkf.rbとして保存
require 'nkf'
puts "\u304b\u3099 : U+304b U+3099"
puts "nkf --ic=UTF-8 --oc=Shift_JIS"
ga_to_sjis_from_utf8 = NKF.nkf('--ic=UTF-8 --oc=Shift_JIS', "\u304b\u3099")
puts ga_to_sjis_from_utf8.inspect
puts ga_to_sjis_from_utf8.encode('UTF-8')
puts "=" * 20
puts "nkf --ic=UTF-8-MAC --oc=Shift_JIS"
ga_to_sjis_from_utf8mac = NKF.nkf('--ic=UTF-8-MAC --oc=Shift_JIS', "\u304b\u3099")
puts ga_to_sjis_from_utf8mac.inspect
puts ga_to_sjis_from_utf8mac.encode('UTF-8')
puts "=" * 20
puts "\ufa19 (U+fa19)"
puts "NFD normalized => #{"\ufa19".unicode_normalize(:nfd).inspect}"
puts "NFKC normalized => #{"\ufa19".unicode_normalize(:nfkc).inspect}"
puts "nkf convert => #{NKF.nkf('--ic=UTF-8-MAC --oc=UTF-8', "\ufa19").inspect}"
$ ruby nkf.rb
が : U+304b U+3099
nkf --ic=UTF-8 --oc=Shift_JIS
"\x{82A9}"
か
====================
nkf --ic=UTF-8-MAC --oc=Shift_JIS
"\x{82AA}"
が
====================
神 (U+fa19)
NFD normalized => "\u795E"
NFKC normalized => "\u795E"
nkf convert => "\uFA19"
ところで、㎝ (U+339D
)の変換にも失敗しそうな気がしますが、成功しています。これはどういうことなのでしょうか。
Shift_JISに含まれる文字列の集合はJIS X 0201とJIS X 0208です。このどちらにも1文字で"cm"となる字体は定義されていません。4ではこの「㎝」はどこからやってきたのでしょうか。
「㎝」はNEC特殊文字に含まれており、NECやIBMによるShift_JIS拡張が統合された文字コードであるWindows-31Jに含まれています。これをCP932と呼ぶこともあり5、CP932からUnicodeへの文字変換表には CP932における 0x8770
をUnicodeでの 0x339D
に変換すると定義されています。
https://www.unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP932.TXT
Shift_JISの規定において、0x8770
は「保留域」となっています。6 このことからも、JISに規定されているShift_JISにはなく、それの拡張であるWindows-31Jに含まれている文字であることがわかります。7
また余談として、JIS X 0213にて規定されたShift_JISX0213における 0x8770
に「㎝」の字形が含まれています。8
これもnkfで確認することができ、CP932において拡張された文字を扱わないオプション --no-cp932ext
を指定することで文字が消えていることが確かめられます。
require 'nkf'
puts "\u339d : U+339d"
cm_to_sjis_from_utf8 = NKF.nkf('--ic=UTF-8 --oc=Shift_JIS --no-cp932ext', "\u339d")
puts cm_to_sjis_from_utf8.inspect
puts cm_to_sjis_from_utf8.encode('UTF-8').inspect
puts "=" * 20
$ ruby nkf.rb
㎝ : U+339d
""
""
====================
nkfでは、事前にNFKC正規化を行ってからでないと正しくShift_JISに変換できないことがわかりました。
iconvは、以前はRubyの標準添付ライブラリでしたが、2.0で削除されました。
https://www.ruby-lang.org/ja/news/2013/02/24/ruby-2-0-0-p0-is-released/
現在でもgemとしてインストールできるようにはなっていますが、String#encode
を使用することが推奨されているので、今回はコマンドラインの結果をみることにします。
# iconv.rbとして保存
require 'open3'
puts "\u304c (U+304c)"
puts "String#encode('Shift_JIS') => #{"\u304c".encode('Shift_JIS').inspect}"
Open3.popen2e('iconv --from-code=UTF-8 --to-code=SHIFT-JIS') do |stdin, stdout_e, _|
stdin.print "\u304c"
stdin.close
result = stdout_e.read
puts "Convert to Shift_JIS by iconv => #{result.inspect}"
puts "Re-convert to UTF-8 => #{result.force_encoding('Shift_JIS').encode('UTF-8')}"
end
puts "=" * 20
puts "\u304b\u3099 (U+304b U+3099)"
puts "String#encode('Shift_JIS', undef: :replace) => #{"\u304b\u3099".encode('Shift_JIS', undef: :replace).inspect}"
Open3.popen2e('iconv --from-code=UTF-8 --to-code=SHIFT-JIS') do |stdin, stdout_e, _|
stdin.print "\u304b\u3099"
stdin.close
result = stdout_e.read
puts "Convert to Shift_JIS by iconv => #{result.inspect}"
puts "Re-convert to UTF-8 => #{result.force_encoding('Shift_JIS').encode('UTF-8')}"
end
puts "=" * 20
puts "\u0063\u006d (U+0063 U+006d)"
puts "String#encode('Shift_JIS') => #{"\u0063\u006d".encode('Shift_JIS').inspect}"
Open3.popen2e('iconv --from-code=UTF-8 --to-code=SHIFT-JIS') do |stdin, stdout_e, _|
stdin.print "\u0063\u006d"
stdin.close
result = stdout_e.read
puts "Convert to Shift_JIS by iconv => #{result.inspect}"
puts "Re-convert to UTF-8 => #{result.force_encoding('Shift_JIS').encode('UTF-8')}"
end
puts "=" * 20
puts "\u339d (U+339d)"
puts "String#encode('Shift_JIS', undef: :replace) => #{"\u339d".encode('Shift_JIS', undef: :replace).inspect}"
Open3.popen2e('iconv --from-code=UTF-8 --to-code=SHIFT-JIS') do |stdin, stdout_e, _|
stdin.print "\u339d"
stdin.close
result = stdout_e.read
puts "Convert to Shift_JIS by iconv => #{result.inspect}"
puts "Re-convert to UTF-8 => #{result.force_encoding('Shift_JIS').encode('UTF-8')}"
end
Open3.popen2e('iconv --from-code=UTF-8 --to-code=SHIFTJISX0213') do |stdin, stdout_e, _|
stdin.print "\u339d"
stdin.close
result = stdout_e.read
puts "Convert to ShiftJISX0213 by iconv => #{result.inspect}"
puts "Re-convert to UTF-8 => #{result.force_encoding('CP932').encode('UTF-8')}"
end
$ bundle exec ruby iconv.rb
が (U+304c)
String#encode('Shift_JIS') => "\x{82AA}"
Convert to Shift_JIS by iconv => "\x82\xAA"
Re-convert to UTF-8 => が
====================
が (U+304b U+3099)
String#encode('Shift_JIS', undef: :replace) => "\x{82A9}?"
Convert to Shift_JIS by iconv => "\x82\xA9iconv: illegal input sequence at position 3\n"
Re-convert to UTF-8 => かiconv: illegal input sequence at position 3
====================
cm (U+0063 U+006d)
String#encode('Shift_JIS') => "cm"
Convert to Shift_JIS by iconv => "cm"
Re-convert to UTF-8 => cm
====================
㎝ (U+339d)
String#encode('Shift_JIS', undef: :replace) => "?"
Convert to Shift_JIS by iconv => "iconv: illegal input sequence at position 0\n"
Re-convert to UTF-8 => iconv: illegal input sequence at position 0
Convert to ShiftJISX0213 by iconv => "\x87p"
Re-convert to UTF-8 => ㎝
nkfと同様に「が」(U+304B U+3099
) の変換に失敗している様子がわかります。「か」までの出力には成功していることから、濁点 U+3099
の変換に失敗していそうですね。
またnkfについての説明で触れた「㎝」 (U+339D
) については、Shift_JISへの変換は失敗していますが、Shift_JISX0213への変換は成功していますね。
他に指定できそうなoptionもないので、iconvでも事前にNFKC正規化しておく必要がありそうです。
それでは、Rubyを使用せずコマンドラインから使用できる、Unicodeの正規化形式も扱うことのできるツールはないのでしょうか?
これを行うことのできる uconv というものがあります。これはUnicode Consortiumが保守しているInternational Components for Unicodeというコンポーネント(?)に含まれており、Debianにおいては icu-devtools
というパッケージ名で入手できます。
https://packages.debian.org/buster/icu-devtools
uconvに対して -x nfkc
というふうに正規化形式を指定する(正確には、適用したいTransliterationを指定する)ことによって、NFKC正規化がされた上で文字コードの変換ができます。
require 'open3'
puts "\u304c (U+304c)"
puts "String#encode('Shift_JIS') => #{"\u304c".encode('Shift_JIS').inspect}"
Open3.popen2e('uconv --from-code UTF-8 --to-code Shift_JIS -x nfkc') do |stdin, stdout_e, _|
stdin.print "\u304c"
stdin.close
result = stdout_e.read
puts "Convert to Shift_JIS by uconv => #{result.inspect}"
puts "Re-convert to UTF-8 => #{result.force_encoding('Shift_JIS').encode('UTF-8')}"
end
puts "=" * 20
puts "\u304b\u3099 (U+304b U+3099)"
puts "String#encode('Shift_JIS', undef: :replace) => #{"\u304b\u3099".encode('Shift_JIS', undef: :replace).inspect}"
Open3.popen2e('uconv --from-code UTF-8 --to-code Shift_JIS -x nfkc') do |stdin, stdout_e, _|
stdin.print "\u304b\u3099"
stdin.close
result = stdout_e.read
puts "Convert to Shift_JIS by uconv => #{result.inspect}"
puts "Re-convert to UTF-8 => #{result.force_encoding('Shift_JIS').encode('UTF-8')}"
end
puts "=" * 20
puts "\u0063\u006d (U+0063 U+006d)"
puts "String#encode('Shift_JIS') => #{"\u0063\u006d".encode('Shift_JIS').inspect}"
Open3.popen2e('uconv --from-code UTF-8 --to-code Shift_JIS -x nfkc') do |stdin, stdout_e, _|
stdin.print "\u0063\u006d"
stdin.close
result = stdout_e.read
puts "Convert to Shift_JIS by uconv => #{result.inspect}"
puts "Re-convert to UTF-8 => #{result.force_encoding('Shift_JIS').encode('UTF-8')}"
end
puts "=" * 20
puts "\u339d (U+339d)"
puts "String#encode('Shift_JIS', undef: :replace) => #{"\u339d".encode('Shift_JIS', undef: :replace).inspect}"
Open3.popen2e('uconv --from-code UTF-8 --to-code Shift_JIS -x nfkc') do |stdin, stdout_e, _|
stdin.print "\u339d"
stdin.close
result = stdout_e.read
puts "Convert to Shift_JIS by uconv => #{result.inspect}"
puts "Re-convert to UTF-8 => #{result.force_encoding('Shift_JIS').encode('UTF-8').inspect}"
puts "codepoints => #{result.codepoints}"
end
Open3.popen2e('uconv --from-code UTF-8 --to-code cp932') do |stdin, stdout_e, _|
stdin.print "\u339d"
stdin.close
result = stdout_e.read
puts "Convert to CP932 by uconv => #{result.inspect}"
puts "Re-convert to UTF-8 => #{result.force_encoding('CP932').encode('UTF-8')}"
puts "codepoints => #{result.codepoints}"
end
$ bundle exec ruby uconv.rb
が (U+304c)
String#encode('Shift_JIS') => "\x{82AA}"
Convert to Shift_JIS by uconv => "\x82\xAA"
Re-convert to UTF-8 => が
====================
が (U+304b U+3099)
String#encode('Shift_JIS', undef: :replace) => "\x{82A9}?"
Convert to Shift_JIS by uconv => "\x82\xAA"
Re-convert to UTF-8 => が
====================
cm (U+0063 U+006d)
String#encode('Shift_JIS') => "cm"
Convert to Shift_JIS by uconv => "cm"
Re-convert to UTF-8 => cm
====================
㎝ (U+339d)
String#encode('Shift_JIS', undef: :replace) => "?"
Convert to Shift_JIS by uconv => "cm"
Re-convert to UTF-8 => "cm"
codepoints => [99, 109]
Convert to CP932 by uconv => "\x87p"
Re-convert to UTF-8 => ㎝
codepoints => [34672]
このように、NFKC正規化を行ったうえで、正しくShift_JISに変換できていることが、またCP932を指定したときには正規化を行わなくても「㎝」 (U+339D
)を1文字のまま相互に変換できていることがわかります。
ちなみにTransliterationを活用するとこのようにひらがなをローマ字に変換するという面白いこともできます。
$ echo おはようございます | uconv -x '::hiragana-latin;'
ohayougozaimasu
元記事や現在において指定できる正規化形式、NFC、NFD、NFKC、NFKDの4つは、それぞれの名前が規格に登場するのはUnicode 3.0.1からであり、そのリリースは 2000-08-31 です。
https://web.archive.org/web/20050211134342/http://www.unicode.org/unicode/reports/tr15/tr15-19.html
それ以前のリリースにおいては、正規化形式について触れられている記述がなく、前述のUnicode Standard Annex #15から参照できる"Previous Version" においても、 “It is a stable document and may be used as reference material” などの記述が存在しないことから、正規化形式というものが存在するのはUnicode 3.0.1 以降ということになります。
https://web.archive.org/web/20050207015030/http://www.unicode.org/unicode/reports/tr15/tr15-18.html
このあたりの話は、技術評論社から出版されている[改訂新版]プログラマのための文字コード技術入門 の Appendix 4「Unicodeの諸問題」にて詳細に説明されています。この本はとても面白いのでぜひ読んでみてください。
あるUnicode文字列に対してNFKCなどの正規化を適用したい場合、Rubyでは String#unicode_normalize
にオプションとして、コマンドラインでは uconv を使用することで目的を達成できます。文字コード変換で良く知られるnkfやiconvでは適切に正規化が行われていない文字列を変換することができません。
ではAPFSではどうなのかというと、規格書には j_drec_hashed_jey_t
構造体に格納される name_len_and_hash
を計算するときにNFD正規化を行うとこが記載されていますが、ファイル名そのものの正規化についての記述は見付かりませんでした。 https://developer.apple.com/support/downloads/Apple-File-System-Reference.pdf 正規化が行われず、与えられたバイト列をそのまま保持するようになっているのか、同じ名前に見えるファイルを複数作成することができるという記事もあります。 https://eclecticlight.co/2017/04/06/apfs-is-currently-unusable-with-most-non-english-languages/ ↩
軽く目を通しましたが、Apple側でこの正規化形式に名前をつけたりはしていないようです。 https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPFileSystem/BPFileSystem.html#//apple_ref/doc/uid/10000185-SW1 ただし、Appleの配布しているiconvのencodingには UTF-8-MAC
を指定できるので、ほぼ公式だとしていいでしょう。 ↩
https://developer.apple.com/library/archive/qa/qa1173/_index.html より ↩
JIS X 0208-1997 附属書3 表1より https://www.jisc.go.jp/pdfb6/PDFView/ShowPDF/5gMAAIAz9AGm_33fhhRp ↩
「呼ぶこともあり」というのは、当初定められたCP932をいくつかのベンダが独自拡張したあと、それをMicrosoftが統合したWindows-31JのこともCP932と呼ぶからです。現代においてはCP932 = Windows31J としていいと思いますが。 ↩
JIS X 0208-1997 附属書1より https://www.jisc.go.jp/pdfa8/PDFView/ShowPDF/7gIAAKR6fwk8ZKtaZttC ↩
MSDNに記載されていたCP932の文字一覧より https://web.archive.org/web/20180405180457/https://msdn.microsoft.com/en-us/library/cc194892.aspx ↩
JIS X 0213-2000 附属書4 表23より https://www.jisc.go.jp/pdfa5/PDFView/ShowPDF/5AIAAHLePslwm6mc6z4g ↩
https://unasuke.fm を公開しました。ロゴは衰咲ふち(@otoroesaki)さんに作成していただきました。この場を借りてお礼申しあげます。ありがとうございます。
一旦は今までのエピソードを聞けるようにしただけになります。今後、エピソードごとのサマリやshownoteの記載、RSS feedの公開などの改善を進めていきます。
加えて、次の収録についても日程含め検討中になります。
Podcastは、なぜだかまたブームが来ているようで、AnchorやTransistor、stand.fmなどのプラットフォームが登場しています。 プラットフォームに依存することで、労力の削減や便利な機能を使うことができますが、自分はいちから構築することにしました。
自分が発信する場を他人に委ねる怖さ、というものも多分どこかにあって、一度Twitterのアカウントが凍結されたときにはそれはもう気が気ではありませんでした。ブログをMiddlemanでbuildしてS3にホストしているのも、大して有効活用できていないMastodonを維持し続けているのも、そのためかもしれません。
今回は今までの自分の選択であるMiddlemanによる構築ではなく、Next.jsによる構築を選択しました。
理由は、Webサイトをつくるのであれば、Webの一級市民言語であるJavaScriptでつくるほうがいいのではないかという判断、自分の技術の幅を広げたくて、まだ仕事でも触ったことのないReactに慣れておきたかったという学習目的の2つが主です。また、Next.js を選択するのであれば、素JavaScriptよりは、TypeScriptで書くほうがいいと思い、そのようにしました。
そうなると、TypeScriptとReactとNext.jsの3つを初めて触ることになるので、2つのことを同時に学ばない (by ところてん)ようにするどころか3つのことを同時に学ぼうとしてしまっています。 実際、書いては思うように動かず、何度も友人に助けを求めたり、ひたすらWeb上の記事を読んだりして、それでも全然わからず、多忙を言い訳にしてしばらく放置してしまったりしました。
半分エタりかけていましたが、公式ドキュメントを何度も何度も読んでいくうちになんだかある程度「わかる」ようになりました。結局自分は、悩んで泥臭く試行錯誤を繰り返して手を動かさないと技術を身に付けることはできないんじゃないかと思います。
正直まだまだ機能面や見た目の面でも足りていない部分が多く構築中ですが、オリジナルのファイルがSoundCloud上にもう存在しないので、とりあえずWeb上に参照可能な形で復活させることを優先しました。RSSもないのでPodcastとしては片手落ちですが、"Done is better than perfect" と言いますし、まずは公開することにしました。
まず、Next.jsを静的サイトジェネレーターとして採用しました。そうなると必然、Viewを記述するためにReactを採用することになります。また前述のように、主にTypeScriptで記述しています。ESLintとPrettierを用いてコードフォーマットをしています。
音声ファイルのホスティングについては、転送量による課金のないWasabiを、WebサイトそのもののホスティングについてはFirebase Hostingを選択し、DeployはGitHub Actions経由で行うようにしました。確認用にmaster branchをNetlifyにてHostingしています。
また Content Security Policy を有効化にし、Report Onlyの状態にして report-uri.io に収集させるようにしました。
余談ですが、開発はほぼ全てをWindows 10(not WSL)上で行いました。開発環境において特にハマることはありませんでしたが、ESLintのruleに改行コードをUnix styleかWindows Styleかに設定するものがあり、これの扱いが悩ましいところでした。
— うなすけ (@yu_suke1994) June 28, 2020
まずはこの動画の状況になるまでの出来事を書いていきます。
一度セーフモードに入ると抜けられなくなるというのは結構な絶望感がありました。
という流れだったのでした。イチから環境構築をやりなおしになってしまいましたが、ひとまず直ってよかったです。
※ この記事は復旧させたWindowsのWSL上で書かれました。
久々に自分がReleaseしたので、なにが入ったのかを軽くまとめます。
debug logにどのactionによる実行なのかを表示するものです。descriptionに貼ってあるlogを見るとわかりやすいですね。
check_package_is_installed
calling when package version is not specified by pocke · #314package
resourceにおいて、そのresourceのversionが指定されていない場合には不要となる処理を実行せずfast returnすることによって処理の高速化を図るものです。具体的には。installするactionにおいては、versionが指定されていない場合にはどのversionが存在するかの確認はしなくていいですし、removeするactionにおいてはinstallされているかどうかのみ確認すればよいだけです。
patch authorによるbenchmarkでは1.3倍高速になったとか。
git resourceにおいて、git rev-list
の最初行を取得するのではなく、git rev-parse
の結果を使用するように変更するものです。無駄な出力をさせなくてよくなり、実行速度も少し向上するようです。
SSH時の設定について、 StrictHostKeyChecking
が no
に設定されている場合にはその設定値を削除し、代わりにverify_host_key
を :never
に設定するものです。
net-ssh gemの v6.0.2 において ArgumentError: invalid option(s): strict_host_key_checking
が出てしまったことからいただいたpull requestではあるのですが、itamaeが依存しているSpecinfra側で対応がされたため、こちらでは特に対処しないこととしました。
基本的に僕のレスが遅れがちでちょっと申し訳なさがあります。そして6月もリリースする予定です。