ブログをNext.jsからwasm(Rust)に移行してみた

定期的にブログの構成や中身を変えているのですが、今回は

  • Next.js + WordPress + Vercel

から

  • Yew(Rust) + WordPress + Cloudflare pages / workers

に移行しました

インフラもごっそり入れ替えることは結構リスキーですが、

  • 個人レベルのプロジェクトであること
  • Rustという選択肢が取られ始めていること
  • Edge computingにチャレンジしてみたい

という理由があり、少しだけ攻めてみました

利用しているversionについて

Rust edition: 2021
Yew: 0.19.3
Worker: 0.0.15

Rustについて

一般的にRustは難しい言語と考えられていますが、個人的にはGoとそこまで変わらないのでは?と思いました🤔

ただし、所有権と非同期処理についてはそこそこハマったので、こちらの2点は注意が必要かと思います

既に詳細に記述されている記事がたくさんあるので、参考にしたURLを記載しています

所有権のムーブについて

所有権の移動(ムーブ)とは

所有権のライフタイムについて

RustのLifetimeってなんなん

ブログ移行後の構成図

ブログ移行後は下記の構成へと変更しました

元々記事はWordPressに記載しているので、WordPressの記事をBFF部分のworkerで整形したりしています

実装初期はCloudflare pagesに全てまとめる予定でしたが、pagesは表示するだけでコンテンツの整形など全てserverでまとめた方が後々楽そうだと思い分けています

そしてpagesはYewというReact likeなフレームワークを利用しています

実装で大変だったところ

この記事のメイン部分です
Rustでwasmを生成してCloudflare pagesでEdge computingを実現している内容はあまり見受けられなかったので、実装する上で辛かった点、悩んだ実装内容についてまとめました🙏

SSR

SSRは一応お試し的な状態ではあるのですが、wasm環境のみで実装するのはかなり難しそうなので、現状「諦めている」という状態です

Server-side Rendering

しかし、クローラが確認してくれることや、sitemap等の対策を行うことである程度はGoogleがよしなにやってくれている、とのことだったので、通常のSPAでチャレンジするという方針にしました

wasm製アプリケーションもGoogleボットはきちんとインデックスしてくれる

環境変数

wasm環境下でもdotenvぐらいいけるでしょ!と考えていたのですが、buildタイミング、実行タイミングを考慮したり、そもそも情報が足りていないという状態で、環境変数を扱うことはできていません、、、、

今回は個人ブログですし、github actionsで利用するCloudflareのAPIキー以外は、WordPressのリンクだけなので、特に問題視はしていません

pagesで/functionsを利用

pagesでは/functions ディレクトリでworkerを稼働させることができます

そしてwasm + 呼び出すjsファイルだけでも良いかも?と考えていたのですが、決まったフォーマットがあるようです

当初はRustでworkers向けのプロジェクトをwasm生成すればそのまま実行できる、と考えていましたがリリースなどを確認すると、jsからwasmを呼び出す、ということでしか利用できなさそうです

Use the language of your choice with Pages Functions via WebAssembly

// functions/api/distance-between.js

import wasmModule from "../../pkg/distance.wasm";

export async function onRequest({ request }) {
  const moduleInstance = await WebAssembly.instantiate(wasmModule);
  const distance = await moduleInstance.exports.distance_between();

  return new Response(distance);
}

.tomlファイルの環境変数対応

workersではデプロイや実行するタイミングでwrangler.tomlというファイルが必要です

可能であれば環境変数を使いたかったのですが、そもそもtomlファイル自体が対応していないとのことだったので、CIでシェルで生成するという方法を取りました

// wrangler_template.sh
#!/bin/bash

export $(cat .env)
envsubst '$ACCOUNT_ID $KV_BINDING $KV_ID $KV_PREVIEW_ID' < wrangler.toml.template > wrangler.toml
----

// wrangler.toml.template
name = "blog-content-worker"
workers_dev = true
compatibility_date = "2022-02-24"

main = "build/worker/shim.mjs"
account_id="$ACCOUNT_ID"

kv_namespaces = [
  { binding = "$KV_BINDING", id = "$KV_ID", preview_id = "$KV_PREVIEW_ID"}
]

[env.prod]
WORKERS_RS_VERSION = "0.0.9"

[build]
command = "cargo install -q worker-build && worker-build --release"

[[rules]]
globs = ["**/*.wasm"]
type = "CompiledWasm"

外部のSitemapにリダイレクトさせる方法

pagesには _redirectsというファイルをrootに配置して下記のような内容を記載することでリダイレクトを実現できる仕組みがあります

下記の実装だと、 / にアクセスすると /pages/1 にリダイレクトする、という記述です

/ /pages/1 301

この実装で、workersに作成した別のdomainにリダイレクトさせて、結果的にsitemapの内容を表示できると考えていたのですが、設定したドメイン(pagesプロジェクト内部)レベルのリダイレクトらしく、機動的に外部にアクセスを転送することはできない模様

なのでfunctionsでこちらの一般的なリバースプロキシを追加して対応しました

const handleReverseProxy: PagesFunction = async (context) => {
  const originalUrl = context.request.url;
  const url = new URL(originalUrl);
  if (url.pathname.indexOf("/sitemap") !== 0) {
    return await context.next();
  }
  const newUrl = new URL("https://api.masahiro.me/sitemap");
  const response = await fetch(new Request(newUrl));
  return new Response(response.body, {
    status: response.status,
    statusText: response.statusText,
    headers: new Headers(response.headers),
  });
};

Twitter cardに値を表示する方法

今回はSPAで生成しているので、metatagに関してはページを表示した後に動的に変更しています

なのでURLをtwitterやslack等のプラットフォームで掲載しても、初期値が表示されるだけで、クリック率や流入にも影響を与えてしまいそうです

  • SSR
  • SSG
  • workersで生HTMLを生成する古典的なSSR

などの可能性を考えたのですが、そもそもuse agentで判別できるのでは?と考えこちらを実装しました

まずはfunctionsのコード

const handleBot: PagesFunction = async (context) => {
  const originalUrl = context.request.url;
  if (!originalUrl.includes("/posts/")) {
    return await context.next();
  }
  const isSocialMediaBot = (userAgent: string): boolean => {
    return ["Twitterbot", "Slackbot"].some((botUserAgent) =>
      userAgent.includes(botUserAgent)
    );
  };

  const userAgent = context.request.headers.get("user-agent") ?? "";
  if (!isSocialMediaBot(userAgent)) {
    return await context.next();
  }
  const url = new URL(context.request.url);
  const splitedUrl = url.pathname.split("/");
  const slug = splitedUrl[splitedUrl.length - 1];
  const newUrl = new URL(`https://api.masahiro.me/meta?slug=${slug}`);
  const resp = await fetch(new Request(newUrl));
  return new Response(resp.body, {
    headers: { "Content-Type": "text/html" },
  });
};

そしてこちらがHTMLを生成するworkersのコード
該当のURLにslugを投げるとheadに適切なmetatagをセットしてHTMLを生成してpagesにResponseを返す -> pagesのfunctionsレベルでresponseを生成するのでSPAにアクセスすることなく、use agentで判断ができています

pub async fn run(req: Request, env: Env) -> Result<Response> {
    let router = Router::new();
    router
        .get_async("/meta", handle_get_meta_request)
        .run(req, env)
        .await
}

pub async fn handle_get_meta_request(req: Request, ctx: RouteContext<()>) -> Result<Response> {
    // 省略
    let post = match result {
        Some(post) => post,
        None => return Response::error("Not found", 404),
    };
    let meta = render_meta(&post);
    let mut resp = Response::ok(meta).unwrap().with_cors(&cors).unwrap();
    resp.headers_mut().set("content-type", "text/html").unwrap();
    Ok(resp)
}

Twitterで表示すると意図したUIが表示されています

Sitemapを作成する方法

事前にファイルを作成するか都度生成するかで悩んだのですが、workersでKVを利用してキャッシュを生成していたので、そのデータを利用してXML形式のレスポンスを返す、という方針で実装しました

そこそこ悩んだのですが、これが一番楽な実装かと思います

pub async fn handle_get_sitemap_request(req: Request, ctx: RouteContext<()>) -> Result<Response> {
    log_request(&req);
    let slugs = fetch_slugs_usecase(&ctx.env).await.unwrap();
    let mut sitemap = String::new();
    sitemap.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
    sitemap.push_str("<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">");
    sitemap.push_str(
        "<url>
            <loc>https://masahiro.me/pages/1</loc>
            <priority>1.0</priority>
        </url>",
    );
    sitemap.push_str(
        "<url>
            <loc>https://masahiro.me/projects</loc>
            <priority>0.8</priority>
        </url>",
    );
    sitemap.push_str(
        "<url>
            <loc>https://masahiro.me/about</loc>
            <priority>0.8</priority>
        </url>",
    );
    for slug in slugs {
        sitemap.push_str(&format!(
            "<url>
                <loc>https://masahiro.me/posts/{}</loc>
                <priority>0.8</priority>
            </url>",
            slug
        ));
    }
    sitemap.push_str("</urlset>");
    let cors = Cors::new()
        .with_origins(vec!["*".to_string()].iter())
        .with_max_age(86400)
        .with_allowed_headers(vec!["*".to_string()])
        .with_methods(vec![Method::Get, Method::Options, Method::Head]);
    let mut res = Response::ok(sitemap).unwrap().with_cors(&cors).unwrap();
    res.headers_mut()
        .set("content-type", "application/xml")
        .unwrap();
    Ok(res)
}

wasmサイズの最適化

適切な設定を加えないとbuildサイズはそこそこ大きいです

Yew側のサイズは何もしていない状態で1.5MGで、個人的にはかなり大きい印象

次はLTO(Link Time Optimization)の設定を追加

# Cargo.toml

[profile.release]
lto = true

サイズは0.2MB小さくなりました

最後はコードサイズ最適化の設定を追加

# Cargo.toml

[profile.release]
opt-level = z

最終的には949KBまで削減することができました

ちなみにですが、ここからさらに圧縮するためにwasmの中を確認してみたのですが、50%ほどはもしかしたら削減できそうな気も?という内容でした

cargo install twiggy
twiggy top -n 10 dist/masahiro-89414b795cb07fda_bg.wasm
 Shallow Bytes │ Shallow % │ Item
───────────────┼───────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────
        256994 ┊    26.14% ┊ data[0]
        238288 ┊    24.24% ┊ "function names" subsection
         15614 ┊     1.59% ┊ http::header::name::parse_hdr::hc3914b1af2df09b6
         12390 ┊     1.26% ┊ <yew::html::component::lifecycle::RenderRunner<COMP> as yew::scheduler::Runnable>::run::h0f98a0286f25da9c
         11863 ┊     1.21% ┊ url::host::Host::parse::hd93938c25f0b0e37
          9828 ┊     1.00% ┊ reqwest::wasm::request::RequestBuilder::send::{{closure}}::hf898947247698921
          8493 ┊     0.86% ┊ core::fmt::float::float_to_decimal_common_shortest::h104ccad32a6d10e2
          8139 ┊     0.83% ┊ <yew::html::component::lifecycle::RenderRunner<COMP> as yew::scheduler::Runnable>::run::h84df7697082f83b4
          6979 ┊     0.71% ┊ core::fmt::float::float_to_decimal_common_exact::h37c3cd7e1a60c62a
          6976 ┊     0.71% ┊ <yew::virtual_dom::vnode::VNode as yew::virtual_dom::VDiff>::apply::he2bab8f4b493960d
        407650 ┊    41.46% ┊ ... and 2259 more.
        983214 ┊   100.00% ┊ Σ [2269 Total Rows]

Rustのみで実装することの難しさ

当初はマークアップ以外はRustだけで実装する予定でした

しかし/functionsディレクトリを利用しようとすると、どうしてもTypeScriptを記述する必要があり、一部TypeScriptを利用してしまっています。。。

作業を終えて考えること

Rustを開発で使うべき?

正直使っても良いと考えてます
コンパイラが非常に強力なので物凄く安全に開発することができるので、フロントでもバックエンドでも問題ないかと思います

ただし、認証系のcrateは希薄なので自前で実装する必要がほとんどかと
その場合認証系のリスクを背負うことになるので、そこだけが心配です

ランタイムエラーは起こせる

Rustのメリットとしてランタイムエラー排除、があるかと思います
正直どの言語でも同じことだと思いますが、エンジニアが横着すると普通にエラーを起こせてしまうので丁寧な実装が必要です

// 場合によってはエラー起きる
let kv_data = kv_client.get(&cache_key).await.unwrap();

// エラーの場合もハンドリングしてる
let kv_data = kv_client.get(&cache_key).await;
let data = match kv_data {
  Ok(data) => data,
  Err(e) => { // do something }
};

NimやGoと比べると

所有権があるおかげで非常に使いやすく、個人的には3つのうちでダントツでRustが使いやすいと思います

Nimを時々触っている + 流行ってほしいという気持ちがあるのでNimをおすすめしたいところではありますが、エコシステムや言語のコンパイラの強力さを考えると、ビジネス価値的にRustはやはり良い選択肢だと考えています

TODO(残り作業)

  • wasmで環境変数を参照
  • pagesの/functions ディレクトリでwasmを直接実行(継続的に方法を模索したい)
  • DDDのコードとして完成させたい
  • テスト
  • SSR or SSGの可能性の模索
  • 必要なcrateを自作 or OSSにコミット
  • wasmサイズの圧縮

Rustブログのリポジトリ

https://github.com/masahiro04/masahiro-me

2022の振り返りと2023の抱負

みなさま新年明けましておめでとうございます🎍

昨年は短いような長いような1年でしたが、進みたい方向へ順調に歩みを進めることができ、非常に実りある時間を過ごせました

お客様、参加しているチームの皆様、大変お世話になりました🙇‍♀️

昨年の振り返り

シードからシリーズBへ

長らくシード期のプロダクト開発を行なっていましたが、やはり専門性を身につけたいと考え、シリーズB以降の企業とお付き合いをさせていただく、という決断をしました

開発スタイルの大幅な変更

今までは

  • 最速の開発
  • リリース
  • 仮説検証

ということを行なっていたので、早く作れること、が自分の一番の武器となっておりました

しかしシステムが成長する過程で早く開発して効果検証するよりも、

  • 技術負債を減らす
  • 継続的な開発ができること
  • 個人ではなくチーム戦
  • 命名やアークテクチャの重要性

などが頻繁に議論され、自分が意識していた早く作れることは既に価値がかなり薄れていること、という実感がありました

なので、今まで慣れ親しんだ開発スタイルの一切を捨てて、大規模プロダクト開発を行うためのスタイルへと大きく変更しました

※以前のようなスピードがなく驚いている方もいらっしゃるかもしれませんが、明確な方針があってのことです🙏

本年の抱負

今年は下記4点を重点的に業務でも個人でも積極的に学び、来年以降に活かしていきたいと考えています

  • Edge computing
  • Infrastructure
  • Firebase( NoSQL )
  • 高稼動性担保

現時点では全て中途半端なので、「フロント×インフラ」と行ったら大久保、と名前を呼んでいただけるようになりたいです

経験も能力も足りていない要素が多いですが、昨年に続き3年間は勉強期間と捉えておりますので、温かい目で見守っていただけますと幸いです

本年はお客様、チームの皆様と大きく成長することができる1年を実現できるように精一杯努力しますので、本年もどうぞよろしくお願い申し上げます🎍🎍🎍🎍🎍

Vimプラグイン自作の方法

基本的にはGithubを漁って自分に合ったプラグインを探して利用する、という方針だったのですが、どうしても実現できない機能がいくつかあり、新しいチャレンジを行うという試みでプラグインの自作に踏み出してみました

難しいことはやらずに簡易的な機能を提供するだけのプラグインをとりあえず作ってみます🦀

事前準備

現在NeoVimとそれなりに多いプラグインで構成されているので、最小構成のVim環境を新しいブランチを作成しましょう

特にやる必要はないかもですが、今後作成していく自作プラグインは一応Githubで公開予定なので可能な限り依存を少なくする、という意味合いで準備します

※deinやmotion移動等だけ残してそれ以外は消しました

超最小構成*.vimの実行

最小構成のvimに変更したら、任意のdirに sample.vim というファイルを作成します

[masahirookubo@MacBook-Pro-2 (arm64):~/dotfiles][feature/plugins]
% touch sample.vim

そしてprint文を記載して実行

echo "hogehgoe!!!"

:source sample.vim

動きました!!

最小構成ではこちらでいけますので、一旦これで完了とします

超最小構成のvimプラグインの実装

最小構成のプログラムが稼働することを検証できたので、次はもう少しプログラムを追加してそれをremoteで公開してみます

まずはpluginを作っていくためのdirなどの準備コマンドの実行

# 任意のdirへ移動してから実行
mkdir sample_plugin
mkdir plugin
touch plugin/sample_plugin.vim
vi plugin/sample_plugin.vim

作成したら、下記のプログラムを追加

function! sample_plugin#SamplePlugin()
  echo 'It works!'
endfunction

command! SamplePlugin call sample_plugin#SamplePlugin()

プログラムの追加が完了したら、ファイルを読み込ませて実行してみます

:source sample_plugin.vim
:SamplePlugin

問題なく読み込めています👍

GIthubから利用してみる

localでの稼働も検証できたので、Githubに上げてdeinで追加して検証を行います

※パッケージマネージャはお使いのものを利用してください

[[plugins]]
repo = 'masahiro04/sample_plugin'

追加した後はvimを起動して下記を実行・検証してみます

「sam」と打ち込むと候補が出力され

エンターを押すと、実装したプラグインが表示されました!

もうちょっと難しいと思ってたんですけど、思いのほか簡単に実装できたので、自分が作りたいプラグインをこれから作ってみようと思います!!!

参考記事

はじめてのvimscriptとvimプラグインの作成

簡単なVimプラグインを作って基礎を学ぶ

[dein] Vim(lua):E5107: Error loading lua [string “:lua”]:3: ‘=’ expected near ‘nnoremap’

NeoVimでLuaを導入したところタイトルのエラーが発生しました

どうやらこのエラーは、luaの前方にスペースやタブが入っていてもエラーが出るようなので、

[[plugins]]
repo = 'nvim-telescope/telescope.nvim'
depends = ['plenary.nvim']
hook_add = '''
  lua << EOF
    require('telescope').setup()
  EOF
  nnoremap <leader>ff <cmd>lua require('telescope.builtin').find_files()<cr>
'''

こちらに修正したところ解決できました!

[[plugins]]
repo = 'nvim-telescope/telescope.nvim'
depends = ['plenary.nvim']
hook_add = '''
lua << EOF
  require('telescope').setup()
EOF
  nnoremap <leader>ff <cmd>lua require('telescope.builtin').find_files()<cr>
'''

参考記事

Unable to use lua inside a `.vim` file

Flutterにcloud_firestore導入したらAMSupportURLConnectionDelegate is implemented in bothが出た

FlutterにFirebase追加したら下記のエラーが発生しました

※一部だけ掲載予定でしたが、思いのほかハマったので後続の方のために全て記載しています

バージョン情報

flutter: 3.0.5
Dart: 2.18 ?ぐらい、ここあんま関係ない
Mac: M1 MAX
Changing current working directory to: /Users/masahirookubo/me/flutter/firestore_test
Launching lib/main.dart on iPad Pro (12.9-inch) (5th generation) in debug mode...
Running pod install...                                             16.4s
Running Xcode build...
Xcode build done.                                           54.7s
Failed to build iOS app
Error output from Xcode build:
↳
    objc[54809]: Class AMSupportURLConnectionDelegate is implemented in both /usr/lib/libauthinstall.dylib (0x1ddd5eb90) and /Library/Apple/System/Library/PrivateFrameworks/MobileDevice.framework/Versions/A/MobileDevice (0x103b382c8). One of the two will be used. Which one is undefined.
    objc[54809]: Class AMSupportURLSession is implemented in both /usr/lib/libauthinstall.dylib (0x1ddd5ebe0) and /Library/Apple/System/Library/PrivateFrameworks/MobileDevice.framework/Versions/A/MobileDevice (0x103b38318). One of the two will be used. Which one is undefined.
    ** BUILD FAILED **


Xcode's output:
↳
    Writing result bundle at path:
        /var/folders/bb/n3dq0s2963v_4c8dw8ygjlgm0000gn/T/flutter_tools.IPaDUI/flutter_ios_build_temp_dirGu70oL/temporary_xcresult_bundle

    While building module 'absl' imported from /Users/masahirookubo/me/flutter/firestore_test/ios/Pods/gRPC-Core/src/core/ext/xds/xds_server_config_fetcher.cc:21:
    In file included from <module-includes>:1:
    In file included from /Users/masahirookubo/me/flutter/firestore_test/build/ios/Debug-iphonesimulator/abseil/absl.framework/Headers/abseil-umbrella.h:13:
    In file included from /Users/masahirookubo/me/flutter/firestore_test/build/ios/Debug-iphonesimulator/abseil/absl.framework/Headers/algorithm/algorithm.h:29:
    /Users/masahirookubo/me/flutter/firestore_test/build/ios/Debug-iphonesimulator/abseil/absl.framework/Headers/base/config.h:52:10: error: include of non-modular header inside framework module 'absl.base.config': '/Library/Developer/Toolchains/swift-5.6.2-RELEASE.xctoolchain/usr/bin/../include/c++/v1/limits.h' [-Werror,-Wnon-modular-include-in-framework-module]
    #include <limits.h>
             ^
    While building module 'absl' imported from /Users/masahirookubo/me/flutter/firestore_test/ios/Pods/gRPC-Core/src/core/ext/xds/xds_server_config_fetcher.cc:21:
    In file included from <module-includes>:1:
    In file included from /Users/masahirookubo/me/flutter/firestore_test/build/ios/Debug-iphonesimulator/abseil/absl.framework/Headers/abseil-umbrella.h:13:
    In file included from /Users/masahirookubo/me/flutter/firestore_test/build/ios/Debug-iphonesimulator/abseil/absl.framework/Headers/algorithm/algorithm.h:29:
    In file included from /Users/masahirookubo/me/flutter/firestore_test/build/ios/Debug-iphonesimulator/abseil/absl.framework/Headers/base/config.h:67:
    /Users/masahirookubo/me/flutter/firestore_test/build/ios/Debug-iphonesimulator/abseil/absl.framework/Headers/base/policy_checks.h:28:10: error: include of non-modular header inside framework module 'absl.base.policy_checks': '/Library/Developer/Toolchains/swift-5.6.2-RELEASE.xctoolchain/usr/bin/../include/c++/v1/limits.h' [-Werror,-Wnon-modular-include-in-framework-module]
    #include <limits.h>
             ^
    While building module 'absl' imported from /Users/masahirookubo/me/flutter/firestore_test/ios/Pods/gRPC-Core/src/core/ext/xds/xds_server_config_fetcher.cc:21:
    In file included from <module-includes>:1:
    In file included from /Users/masahirookubo/me/flutter/firestore_test/build/ios/Debug-iphonesimulator/abseil/absl.framework/Headers/abseil-umbrella.h:53:
    /Users/masahirookubo/me/flutter/firestore_test/build/ios/Debug-iphonesimulator/abseil/absl.framework/Headers/base/internal/spinlock_linux.inc:17:10: fatal error: 'linux/futex.h' file not found
    #include <linux/futex.h>
             ^~~~~~~~~~~~~~~
    3 errors generated.
    /Users/masahirookubo/me/flutter/firestore_test/ios/Pods/gRPC-Core/src/core/ext/xds/xds_server_config_fetcher.cc:21:10: fatal error: could not build module 'absl'
    #include "absl/strings/str_join.h"
     ~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~
    While building module 'openssl' imported from /Users/masahirookubo/me/flutter/firestore_test/ios/Pods/gRPC-Core/src/core/tsi/ssl_transport_security.h:25:
    In file included from <module-includes>:1:
    /Users/masahirookubo/me/flutter/firestore_test/build/ios/Debug-iphonesimulator/BoringSSL-GRPC/openssl_grpc.framework/Headers/umbrella.h:39:1: warning: umbrella header for module 'openssl' does not include header 'siphash.h' [-Wincomplete-umbrella]
    /Users/masahirookubo/me/flutter/firestore_test/build/ios/Debug-iphonesimulator/BoringSSL-GRPC/openssl_grpc.framework/Headers/umbrella.h:39:1: warning: umbrella header for module 'openssl' does not include header 'x509_vfy.h' [-Wincomplete-umbrella]
    /Users/masahirookubo/me/flutter/firestore_test/build/ios/Debug-iphonesimulator/BoringSSL-GRPC/openssl_grpc.framework/Headers/umbrella.h:39:1: warning: umbrella header for module 'openssl' does not include header 'hpke.h' [-Wincomplete-umbrella]
    /Users/masahirookubo/me/flutter/firestore_test/build/ios/Debug-iphonesimulator/BoringSSL-GRPC/openssl_grpc.framework/Headers/umbrella.h:39:1: warning: umbrella header for module 'openssl' does not include header 'e_os2.h' [-Wincomplete-umbrella]
    /Users/masahirookubo/me/flutter/firestore_test/build/ios/Debug-iphonesimulator/BoringSSL-GRPC/openssl_grpc.framework/Headers/umbrella.h:39:1: warning: umbrella header for module 'openssl' does not include header 'blake2.h' [-Wincomplete-umbrella]
    /Users/masahirookubo/me/flutter/firestore_test/build/ios/Debug-iphonesimulator/BoringSSL-GRPC/openssl_grpc.framework/Headers/umbrella.h:39:1: warning: umbrella header for module 'openssl' does not include header 'hrss.h' [-Wincomplete-umbrella]
    /Users/masahirookubo/me/flutter/firestore_test/build/ios/Debug-iphonesimulator/BoringSSL-GRPC/openssl_grpc.framework/Headers/umbrella.h:39:1: warning: umbrella header for module 'openssl' does not include header 'trust_token.h' [-Wincomplete-umbrella]
    7 warnings generated.
    7 warnings and 4 errors generated.
    note: Using new build system
    note: Planning
    note: Build preparation complete
    note: Building targets in dependency order
    /Users/masahirookubo/me/flutter/firestore_test/ios/Pods/Pods.xcodeproj: warning: The iOS Simulator deployment target 'IPHONEOS_DEPLOYMENT_TARGET' is set to 8.0, but the range of supported deployment target versions is 9.0 to 15.2.99. (in target 'leveldb-library' from project 'Pods')

    Result bundle written to path:
        /var/folders/bb/n3dq0s2963v_4c8dw8ygjlgm0000gn/T/flutter_tools.IPaDUI/flutter_ios_build_temp_dirGu70oL/temporary_xcresult_bundle


Lexical or Preprocessor Issue (Xcode): Include of non-modular header inside framework module 'absl.base.config': '/Library/Developer/Toolchains/swift-5.6.2-RELEASE.xctoolchain/usr/bin/../include/c++/v1/limits.h'
/Users/masahirookubo/me/flutter/firestore_test/build/ios/Debug-iphonesimulator/abseil/absl.framework/Headers/base/config.h:51:9

Lexical or Preprocessor Issue (Xcode): Include of non-modular header inside framework module 'absl.base.policy_checks': '/Library/Developer/Toolchains/swift-5.6.2-RELEASE.xctoolchain/usr/bin/../include/c++/v1/limits.h'
/Users/masahirookubo/me/flutter/firestore_test/build/ios/Debug-iphonesimulator/abseil/absl.framework/Headers/base/policy_checks.h:27:9

Lexical or Preprocessor Issue (Xcode): 'linux/futex.h' file not found
/Users/masahirookubo/me/flutter/firestore_test/build/ios/Debug-iphonesimulator/abseil/absl.framework/Headers/base/internal/spinlock_linux.inc:16:9

Parse Issue (Xcode): Could not build module 'absl'
/Users/masahirookubo/me/flutter/firestore_test/ios/Pods/gRPC-Core/src/core/ext/xds/xds_server_config_fetcher.cc:20:9

Could not build the application for the simulator.
Error launching application on iPad Pro (12.9-inch) (5th generation).

[masahirookubo@MacBook-Pro-2 (arm64):~/me/flutter/firestore_test/ios]!+[master]
%

結論

XCodeを再インストールしました

下記のようなコマンドも実行しましたが特に変わらず

cd ios
rm -rf ~/Library/Caches/CocoaPods
rm -rf Podfile.lock
rm -rf Pods
rm -rf ~/Library/Developer/Xcode/DerivedData/*

下記の手順でXCodeを削除、そしてinstallで解決できました

sudo rm -rf /Applications/Xcode.app
sudo rm -rf /Library/Preferences/com.apple.dt.Xcode.plist
sudo rm -rf ~/Library/Preferences/com.apple.dt.Xcode.plist
sudo rm -rf ~/Library/Caches/com.apple.dt.Xcode
sudo rm -rf ~/Library/Application Support/Xcode
sudo rm -rf ~/Library/Developer/Xcode
sudo rm -rf ~/Library/Developer/CoreSimulator

// こちらは念の為実行、理由は後述
sudo rm -rf /Library/Developer/CommandLineTools

問題の原因

再インストール直前で、2点が目につきました

 One of the two will be used. Which one is undefined

Lexical or Preprocessor Issue (Xcode): Include of non-modular header inside framework module 'absl.base.config': '/Library/Developer/Toolchains/swift-5.6.2-RELEASE.xctoolchain/usr/bin/../include/c++/v1/limits.h'
/Users/masahirookubo/me/flutter/firestore_test/build/ios/Debug-iphonesimulator/abseil/absl.framework/Headers/base/config.h:51:9

1行目のエラー情報は割とあるあるだと思うのですが、下の方の情報にはswift-5.6.2がToolchainsから参照されています

ここは思いっきり心当たりがあって、1ヶ月ほど前にvimでswift開発をするためにToolchainsやswiftバージョンをを変えるために、必要なコードをダウンロードしてきたものです

途中までは流石に問題を引き起こすことはないだろう、ぐらいに考えていたのですが、

  • Flutterアプリをcreate
  • flirebaseを導入
  • cloudstore導入で急にエラー

はあまりにもおかし過ぎるし、もしこれが普遍的な問題であれば社会的な問題にもつながると思います

※大きなインフラなので冗談抜きで本当に問題として取り上げられ、twitterなどですぐに情報が回るはず

なので一度削除する、という選択に踏み切りました

注意点

再インストール自体は簡単な選択ですが、

  • 時間かかる(2, 3時間ぐらいかかった。通信状況などにも大きく依存するかと)
  • アカウント登録情報も削除される(Apple IDでログインすれば良いだけだけど、手元に情報を用意しておく必要あり)
  • もしかしたら、重要なファイルや情報なども削除されるかも
  • Toolchainsを削除するとgitなど関連するものを全て削除されるので、削除前によく検討すること

が必要です

自分の場合は基本的にDockerで環境を整えている、かつSwift、Dartでの開発しか行っていないのですぐに踏み切れましたが、複数の案件に関わっている場合や、時間がない場合は特に注意が必要です

まとめ

おそらく上記の順番で実行すれば、cloudfireだけでなくほとんどの問題解決の最終手段になり得ると思います

調べてもダメな場合は、寝る前に再インストールして朝実行ということも可能かと思うので、困っている方は最後の手段としておすすめです🙏

参考記事

Xcodeの再インストール

Supabase dbでカスタムなqueryを発行する方法

SQL Editorでカスタムなqueryの登録試みていたのですが、エラーが発生し結構詰まってしまったので記事にしてみました

結論

create or replaceで登録ではなく、posgreのviewを利用することで対応することで解決しました

こちらがそれぞれのコード


-- 動かない
create or replace function get_answers() 
returns table (
  id int,
  body text,
  topic_id int,
  reply_to int,
  created_at timestamp,
  count int
)
language sql
as $$
  select *, (select count(*) from answers a2 where a1.id = a2.reply_to) from answers a1;
$$;

-- 動く!
CREATE VIEW public.hogehuga AS
    select *, (select count(*) from answers a2 where a1.id = a2.reply_to) from answers a1;

試したこと

まずは動かないコードですが、公式docや技術ブログ系では機能するといった情報がありましたが、私が実行したところ、id単体(別カラムでもOK)では問題なく機能するのですが、

※動くコード


create or replace function get_answers() 
returns table (
  id int
)
language sql
as $$
  select id from answers;
$$;

2つ以上のカラムをreturnしようとしたら下記のエラーが発生しました
※最初に提示したコードを実行

Failed to run sql query: cannot change return type of existing function

エラーを見る限り、既存のfunctionのreturn値を変更できない、とのことですが、そもそもidやtopic_idなど単体では実行が可能だったので、複数選択することで問題が発生するものと思われます

※データ型が正確ではない、という可能性もあります→つまり上記の私のコードが間違い

PostgreSQLのView機能について

数時間粘っても問題解決できなかったので、下記の参考記事で紹介されている、posgreのviewという機能を利用することにしました

view is a convenient shortcut to a query. Creating a view does not involve new tables or data. When run, an underlying query is executed, returning its results to the user.

https://supabase.com/blog/2020/11/18/postgresql-views

簡単に説明すると、特定のqueryを作成して、それに命名した上で実行できるショートカットを作成する、ということです

なので、上記の実行したいコマンドを下記のように設定して実行することで、求める結果を獲得することが可能となります

CREATE VIEW public.hogehuga AS
    select *, (select count(*) from answers a2 where a1.id = a2.reply_to) from answers a1;

こちらが呼び出し元のコード

final answersRequest = client.from('hogehuga').select('*').execute();

取得できました!

参考記事

Supabaseのデータベースを使うときに役立つ情報

Postgres Views

NeoVim + COCのhover UIが崩れる問題の解消

iOSのビルドやUI以外は原則vimで開発を行なっているのですが、COCのホバーで情報を確認する際にUIが崩れていて長い間じわじわと精神的苦痛が伴っていたのですが解決できたので情報を共有します

結論下記をコメントアウトしました

" NG
set ambiwidth=double

" OK
" set ambiwidth=double

before

after, 綺麗!!!感激です😭😭

今までは定義元へジャンプしたりして対処していたのですが、本来は上記画像のように丁寧な見出し分的な情報があるみたいです😳

そもそも何故ambiwidthをセットしていたかというと、「vimで全角文字を表示するため」という理由でした

正直利用ケースは皆無なのですが、何故か入っていた、、、、その結果非常に大きな苦痛を強いられていたので、これからは定期的にvimrcを育てていこう、と思える痛い経験でした🙇‍♂️

参考記事

[Vim] 文字崩れ, 文字が残ってしまうのを防ぐ

Xcodeで実機デバッグ試すとThis operation can fail if the version of the OS on the device is incompatible…と出る

Xcodeで実機デバッグを試みたところ、下記のエラーが発生しました

Failed to prepare device for development.

This operation can fail if the version of the OS on the device is incompatible with the installed version of Xcode. You may also need to restart your mac and device in order to correctly detect compatibility.

こちらの問題ですが、どうやらXcodeでバージョンの対応があるかないか?で判断してるようで、対応していないとエラーが出るみたいです

ちなみに対応しているかどうかは下記のコマンドで件検証可能

cd /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/DeviceSupport

ls
10.0	10.2	11.0	11.2	11.4	12.1	12.3	13.0	13.2	13.4	13.6	14.0	14.2	14.4	15.0	9.0	9.2
10.1	10.3	11.1	11.3	12.0	12.2	12.4	13.1	13.3	13.5	13.7	14.1	14.3	14.5	15.2	9.1	9.3

私が検証したいのは15.5で一覧にないので、下記のプロジェクトからpullして上記dirに移動させます

プロジェクトのURL

ダウンロードが完了したらターミナルで下記を実行していきます

# Downloads dirに移動
cd Downloads

# zipをunzip
unzip 15.5.zip
Archive:  15.5.zip
   creating: 15.5/
  inflating: 15.5/DeveloperDiskImage.dmg.signature
  inflating: 15.5/DeveloperDiskImage.dmg

# 解凍したdirをiOSバージョンのdirへ移動. 権限エラーが発生するのでsudoをつけてます
sudo mv 15.5 /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/DeviceSupport

# 該当のdirへ移動して確認
cd /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/DeviceSupport
ls
10.0	10.2	11.0	11.2	11.4	12.1	12.3	13.0	13.2	13.4	13.6	14.0	14.2	14.4	15.0	15.5	9.1	9.3
10.1	10.3	11.1	11.3	12.0	12.2	12.4	13.1	13.3	13.5	13.7	14.1	14.3	14.5	15.2	9.0	9.2

15.5を入れた後は問題なく起動しました!

参考記事

filsv/iOSDeviceSupport

Xcode: iOS 15.4 実機ビルドができない!?

XcodeでimageLiteralが出てこない

Xcode 13.2.1でimageLiteralがサジェストで出てこなく、調べてみたら記述方法が変わったとのことです

class ViewController: UIViewController {
    @IBOutlet weak var hoge: UIImageView!
    override func viewDidLoad() {
        super.viewDidLoad()
        hoge.image = #imageLiteral()
    }
}

#imageLiteralと記述し、その後に括弧を入れることで画像選択UIが発火します

参考記事

Color Literal behaves inconsistently in Xcode 13.2.1

[WIP]NeoVim + iOSでUIKitやSwiftUIの補完を可能にする

sourcekit lspを追加するところまでは全く問題なかったのですが、

import UIKit
import SwiftUI

などのiOSのUI frameworkが動かない問題が発生し、そのエラーを解消するための方法の記事になります

従って、最初からstep by stepで説明するわけではないので、初期設定を済ませていない方は下記の記事など参考に準備してください🙏

導入がまだの場合はこちらなどを参考に:Vim(NeoVim)でSwift -プラグイン, LSPの導入まで

具体的な方法

とりあえず結論

※注意!!!!M1だとx86_64アーキテクチャで動かない場合があり、arm64に変更が必要かもです

swift build -c release -Xswiftc -sdk -Xswiftc /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk -Xswiftc -target -Xswiftc x86_64-apple-ios15.2-simulator

参考記事

UIKitに依存するSwift PackageをVSCodeで開発する

iOS Development on VSCode

How to use SourceKit-lsp with iOS projects

no such module ‘UIKit’