committee-railsを使ってスキーマ駆動開発を始める

フロントエンドエンジニアとの連携

わたしはRailsでバックエンドAPIを実装しているのですが、開発当初、フロントエンドエンジニアから以下のような提案がありました。

「OpenAPIでスキーマを記述してお互いコンセンサスを取ってから実装しませんか?」

スキーマ駆動開発のことを言っているんだなと思い、

「いいと思うよ、それでいこう」

と答えました。答えたものの、やり方がわかっていないので、調べたことを記載していきます。

committee-railsを使う

“Rails スキーマ駆動開発”と調べると、いろんな例が出てきます。大体の事例がcommittee-railsを使っているので、これが良さそうです。早速インストールします。(テストのときにしか使わないのでtest groupに追加します)

1
2
3
4
5
6
7
8
9
10
$ git diff Gemfile
diff --git a/api/Gemfile b/api/Gemfile
--- a/api/Gemfile
+++ b/api/Gemfile
@@ -76,6 +76,7 @@ group :test do
gem "capybara"
gem "selenium-webdriver"
gem "webdrivers"
+ gem "committee-rails"
end
1
2
3
4
5
6
7
8
9
10
11
$ docker-compose run --rm api ./bin/bundle install
Using devise-i18n 1.10.2
Using devise_token_auth 1.2.1
Installing openapi_parser 0.15.0
Installing json_schema 0.21.0
Fetching committee 4.4.0
Installing committee 4.4.0
Fetching committee-rails 0.6.1
Installing committee-rails 0.6.1
Bundle complete! 29 Gemfile dependencies, 114 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.

インストールが完了しました。

committee-railsの設定

公式のGitHubにも記載がありますが、spec/rails_helper.rbにcommittee-rails用の設定を追記します。
この設定を行うことで、assert_response_schema_confirmメソッドなどが利用できるようになります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ git diff spec/rails_helper.rb
diff --git a/api/spec/rails_helper.rb b/api/spec/rails_helper.rb
--- a/api/spec/rails_helper.rb
+++ b/api/spec/rails_helper.rb
@@ -60,4 +60,12 @@ RSpec.configure do |config|
config.filter_rails_from_backtrace!
# arbitrary gems may also be filtered via:
# config.filter_gems_from_backtrace("gem name")
+
+ config.include Committee::Rails::Test::Methods
+ config.add_setting :committee_options
+ config.committee_options = {
+ schema_path: Rails.root.join("open_api.yaml").to_s,
+ }

specの追加

committee-railsが正しく動作するかどうか確認するための簡単なspecを用意します。devise_token_authでメールアドレス登録のAPIをテストします。(OpenAPIのスキーマは記載済です)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
require "rails_helper"

RSpec.describe "API::Auth::RegistrationsController", type: :request do
describe "POST /api/v1/auth" do
it "returns 200" do
post "/api/v1/auth", params: { email: "example@example.com" }
assert_response_schema_confirm(200)
end

it "returns 422" do
post "/api/v1/auth", params: { password: "password" }
assert_response_schema_confirm(422)
end
end
end

specの実行

では実行します。

1
2
3
4
5
6
7
8
9
10
$ docker-compose run --rm api ./bin/rspec spec/api/v1/auth/registrations_controller_spec.rb

(省略)

Failures:
1) API::Auth::RegistrationsController POST /api/v1/auth return 200
Failure/Error: <p><%= link_to t('.confirm_account_link'), confirmation_url(@resource, {confirmation_token: @token, config: message['client-config'].to_s, redirect_url: message['redirect-url']}).html_safe %></p>

ActionView::Template::Error:
Missing host to link to! Please provide the :host parameter, set default_url_options[:host], or set :only_path to true

エラーが出てしまいました。どうやらメールのドメインが未設定だったようで、以下のように設定しました。
config/environments/test.rb

1
+  config.action_mailer.default_url_options = { host: 'localhost:3000'}

再度実行します。

1
2
3
4
5
6
7
8
$ docker-compose run --rm api ./bin/rspec spec/api/v1/auth/registrations_controller_spec.rb

(省略)

API::Auth::RegistrationsController
POST /api/v1/auth
[DEPRECATION] Committee: please set query_hash_key = rack.request.query_hash because we'll change default value in next major version.
[DEPRECATION] Committee: please set parse_response_by_content_type = false because we'll change default value in next major version.

正常に実行はできましたが、DEPRECATIONの警告が出ています。
公式サイトをよく見てみると、https://github.com/willnet/committee-rails#normal-usage にちゃんと書かれています

1
2
3
4
5
6
7
def committee_options
@committee_options ||= {
schema_path: Rails.root.join('schema', 'schema.json').to_s,
query_hash_key: 'rack.request.query_hash',
parse_response_by_content_type: false,
}
end

query_hash_keyparse_response_by_content_typeの設定を追加します。

1
2
3
4
5
6
7
+  config.include Committee::Rails::Test::Methods
+ config.add_setting :committee_options
+ config.committee_options = {
+ schema_path: Rails.root.join("open_api.yaml").to_s,
+ query_hash_key: "rack.request.query_hash",
+ parse_response_by_content_type: false,
+ }

再度実行します。

1
2
3
4
5
6
7
8
9
10
11
$ docker-compose run --rm api ./bin/rspec spec/api/v1/auth/registrations_controller_spec.rb

(省略)

API::Auth::RegistrationsController
POST /api/v1/auth
returns 200
returns 422

Finished in 0.5116 seconds (files took 9.9 seconds to load)
2 examples, 0 failures

無事実行できました。

おまけ

OpenAPIのschemaでresponse200と422の場合を記述していますが、422のときには値が設定されない場合があります。その場合はそのパラメータにnullableを設定しないといけないのですが、nullableかどうかだけの違いのためにschemaを全て書く必要があり、大変だと思いました。allOfでoverrideできればいいのですが、できないようで、現実的には変更のある箇所のみを別で定義して、allOfで組み合わせるのが良さそうです。

まとめ

今回はcommittee-railsを使ってスキーマ駆動開発の環境を準備し、動作確認用のサンプルを実行するとこまで行いました。

今後スキーマ駆動開発を進めていくとより難しい問題に直面しそうな気がします。その時にはまた記載したいと思います。

参考図書

ローカル開発環境でメールを送信する

ローカルでメール送信がしたい

Djangoでメール送信機能を実装するために、ローカルでテストする方法を調べました。

Railsだとletter_opener_webがあるので楽ですが、Djangoではどうなのでしょうか?

MailCatcherを利用

Djangoではletter_opener_webのようなパッケージは見つかりませんでした。

どうしようかなと考えていたところ、以前、別のシステムのステージング環境でMailCatcherを利用していたことを思い出しました。

MailCatcher用のコンテナを立てて、そのコンテナをSMTPサーバとして指定することで簡単にメール送信環境が構築できると思います。

MailCatcherの設定

ではMailCatcherを利用するための設定を行っていきます。

MailCatcherコンテナイメージ

DockerHubで検索してみるとMailCatcherのコンテナイメージがありました。

検索結果の一番上に出てきたこちらを利用しようと思います。

docker-compose.ymlの設定

ローカルでコンテナを起動するようdocker-compose.ymlを設定していきます。

MailCatcherコンテナの設定

設定自体はとてもシンプルです。現在はwebとdbのコンテナを起動するようにしていますが、そのならびにsmtpというホスト名でコンテナを追加するだけです。

1
2
3
4
5
6
7
8
9
10
services:
web:
(省略)
db:
(省略)
smtp:
image: schickling/mailcatcher
ports:
- "1080:1080"
- "1025:1025"

設定したこととしては、imageの指定と、ポートの指定のみです。1080はメールを確認する際のWebインターフェースのポート番号、1025はSMTPで利用するポート番号になっています。

webコンテナの設定

webコンテナでは、SMTPのエンドポイントとポート番号を指定する必要があります。

STMPのエンドポイントはsmtp、ポート番号は1025になります。

1
2
3
4
web:
environment:
EMAIL_HOST: smtp
EMAIL_PORT: 1025

これでコンテナの設定は完了です。

Djangoの設定

Djangoでメール送信設定を行います。

Djangoのメール送信設定については、SendGridのページがよくまとまっていたので参考にしました。

settings.pyに以下を追記します。

1
2
3
4
5
EMAIL_HOST = os.getenv('EMAIL_HOST', '')
EMAIL_HOST_USER = os.getenv('EMAIL_HOST_USER', '')
EMAIL_HOST_PASSWORD = os.getenv('EMAIL_HOST_PASSWORD', '')
EMAIL_PORT = os.getenv('EMAIL_PORT', 587)
EMAIL_USE_TLS = os.getenv('EMAIL_USE_TLS', False)

これでMailCatcherを利用するための設定は完了です。

メールの送信テスト

ではメール送信ができるかどうかテストを行います。

コンテナの起動

docker-compose upでsmtpコンテナを起動しておきます。

1
$ docker-compose up

次に実際にメールを送信します。shell_plusを利用して、メール送信関数を実行します。

1
2
3
4
$ docker-compose run --rm web python manage.py shell_plus
>>> from django.core.mail import send_mail
>>> send_mail('subject', 'message', 'mail_from@example.com', ['mail_to@example.com'])
1

1がかえってきました。ドキュメントをみると、

1
The return value will be the number of successfully delivered messages (which can be 0 or 1 since it can only send one message).

と書かれていて、配送できたメールの数が戻り値になっているようです。今回は一通しか送っていないため、1がかえってきています。受信者が増えても配送できたメッセージの数なので、1メッセージであれば1が返ってきます。

WebUIでメールの確認

無事メールが配信されているかを確認します。 http://localhost:1080 にアクセスします。

メールが配送できていることを確認できました。

まとめ

Djangoのローカル開発環境でメール送信とその確認を行う方法を記載しました。

コンテナのおかげでローカル開発環境は簡単に拡張できて本当にありがたいです。

次回はメール本文の記述方法について記載したいと思います。

参考図書

Djangoで特定のドメインで発生したエラーについて

特定のドメインでの起動でエラー発生

ローカルでの開発もある程度終わったので、特定のドメインで稼働するようDjangoアプリケーションをデプロイしました。

デプロイ後、そのドメインでアクセスを試みるといくつかエラーが発生したのでまとめておきます。

Disallowed Hostエラー

ブラウザにドメインのURLを入力してアクセスすると、Disallowed Host Errorが発生しました。

エラーメッセージをよくみると、このドメインをALLOWED_HOSTSに追加してくれと書かれています。

よって、settings.pyに以下の記述を行いました。

1
ALLOWED_HOSTS = ['特定のドメイン', 'localhost', '127.0.0.1']

再度デプロイを行うと、無事エラーが解消されていることが確認できました。

CSRF検証エラー

次に、admin管理画面にログインするために、管理画面ログインページにアクセスしました。

IDとパスワードを入力し、ログインボタンを押下すると、CSRF検証エラーと表示され、ドメイン名 does not match any trusted origins.となってしまいます。

原因調査

先程のエラーメッセージで検索すると、それっぽいものがひっかかりました。

https://stackoverflow.com/questions/70285834/forbidden-403-csrf-verification-failed-request-aborted-reason-given-for-fail

この記事にあるように、Django4.0からこのエラーは起こるようになったようです。

対応

対応方法としては、settings.pyにCSRF_TRUSTED_ORIGINSの設定をすれば良さそうです。早速行います。

1
CSRF_TRUSTED_ORIGINS = ['https://特定のドメイン']

再度デプロイしアクセスすると、無事admin管理画面でログインすることができました。

まとめ

Djangoのローカル開発環境以外で動作させるときのエラーについて記載しました。

まだまだ経験が少ないため、プロダクション環境やステージング環境での動作時に予期せぬことが起こるかもしれません。

環境ごとでの設定ファイル(settings.py)の指定も行う必要が出てきます。その辺りも対応していければと思います。

参考図書

Flutterでのパッケージ管理

パッケージの追加

パッケージの追加はflutter pub addコマンドで行えます。

試しによく使われるだろうshared_preferencesを追加してみます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
$ flutter pub add shared_preferences
╔════════════════════════════════════════════════════════════════════════════╗
║ A new version of Flutter is available! ║
║ ║
║ To update to the latest version, run "flutter upgrade". ║
╚════════════════════════════════════════════════════════════════════════════╝

Resolving dependencies...
async 2.8.2 (2.9.0 available)
collection 1.15.0 (1.16.0 available)
fake_async 1.2.0 (1.3.0 available)
+ ffi 1.1.2
+ file 6.1.2
flutter_lints 1.0.4 (2.0.0 available)
+ flutter_web_plugins 0.0.0 from sdk flutter
+ js 0.6.3 (0.6.4 available)
lints 1.0.1 (2.0.0 available)
material_color_utilities 0.1.3 (0.1.4 available)
path 1.8.0 (1.8.1 available)
+ path_provider_linux 2.1.5
+ path_provider_platform_interface 2.0.3
+ path_provider_windows 2.0.5
+ platform 3.1.0
+ plugin_platform_interface 2.1.2
+ process 4.2.4
+ shared_preferences 2.0.13
+ shared_preferences_android 2.0.11
+ shared_preferences_ios 2.1.0
+ shared_preferences_linux 2.1.0
+ shared_preferences_macos 2.0.3
+ shared_preferences_platform_interface 2.0.0
+ shared_preferences_web 2.0.3
+ shared_preferences_windows 2.1.0
source_span 1.8.1 (1.8.2 available)
test_api 0.4.8 (0.4.9 available)
vector_math 2.1.1 (2.1.2 available)
+ win32 2.5.1
+ xdg_directories 0.2.0+1
Downloading shared_preferences 2.0.13...
Downloading shared_preferences_platform_interface 2.0.0...
Downloading shared_preferences_macos 2.0.3...
Downloading shared_preferences_windows 2.1.0...
Downloading shared_preferences_web 2.0.3...
Downloading shared_preferences_linux 2.1.0...
Downloading shared_preferences_ios 2.1.0...
Downloading shared_preferences_android 2.0.11...
Downloading win32 2.5.1...
Changed 20 dependencies!

Flutter自体の更新を促されましたが、一旦はインストールできたようです。

パッケージの追加による変更内容の確認

ソースコード管理上差分として上がってくるのは以下の2ファイルでした。

1
2
3
4
5
6
7
$ git status
On branch master
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: pubspec.lock
modified: pubspec.yaml

pubspec.yaml

こちらには必要なパッケージの一覧が書かれています。今回追加したパッケージが差分として表示されています。

1
2
3
4
5
6
7
8
9
10
11
12
13
$ git diff pubspec.yaml 
diff --git a/pubspec.yaml b/pubspec.yaml
index 037db97..2c583dc 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -34,6 +34,7 @@ dependencies:
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.2
+ shared_preferences: ^2.0.13

dev_dependencies:
flutter_test:

pubspec.lock

こちらはpubspec.yamlで指定したパッケージをインストールする場合の依存パッケージが具体的なバージョンとともに記載されています。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ git diff pubspec.lock 
diff --git a/pubspec.lock b/pubspec.lock
index 7e78bae..1da871d 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -57,6 +57,20 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
+ ffi:
+ dependency: transitive
+ description:
+ name: ffi
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.1.2"
+ file:
+ dependency: transitive
+ description:
...(省略)...

つまるところ、bundlerやpoetry、npmと同じような仕組みですね。

lib/generated_plugin_registrant.dart

.gitignoreに記載されているので差分には出てこないですが、libディレクトリの下にファイルが追加されています。このファイルを調べてみると、webアプリケーション向けのファイルのようです。

では一旦このままの状態でflutter runすると、問題なく起動しました。

次にファイルを削除してから起動してみます。すると、起動しないのかと思いきや問題なく起動しています。

なぜだろうと思ったのですが、みてみるとlibディレクトリの下に削除したgenerated_plugin_registrant.dartが作成されていました。自動的に作成されるので.gitignoreに記載されているということですね。

Flutterの更新

最初に指摘されていたFlutter自体の更新も行っておきます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
$ flutter upgrade
Upgrading Flutter to 2.10.4 from 2.10.3 in /Users/user/flutter...
Downloading Dart SDK from Flutter engine 57d3bac3dd5cb5b0e464ab70e7bc8a0d8cf083ab...
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed

0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0
1 209M 1 2270k 0 0 3404k 0 0:01:03 --:--:-- 0:01:03 3399k
7 209M 7 15.8M 0 0 9722k 0 0:00:22 0:00:01 0:00:21 9717k
13 209M 13 29.1M 0 0 10.9M 0 0:00:19 0:00:02 0:00:17 10.9M
20 209M 20 43.2M 0 0 11.7M 0 0:00:17 0:00:03 0:00:14 11.7M
27 209M 27 56.6M 0 0 12.1M 0 0:00:17 0:00:04 0:00:13 12.1M
33 209M 33 70.0M 0 0 12.3M 0 0:00:16 0:00:05 0:00:11 13.5M
39 209M 39 83.2M 0 0 12.4M 0 0:00:16 0:00:06 0:00:10 13.4M
45 209M 45 94.9M 0 0 12.3M 0 0:00:16 0:00:07 0:00:09 13.1M
50 209M 50 106M 0 0 12.3M 0 0:00:17 0:00:08 0:00:09 12.6M
56 209M 56 118M 0 0 12.2M 0 0:00:17 0:00:09 0:00:08 12.3M
62 209M 62 130M 0 0 12.2M 0 0:00:17 0:00:10 0:00:07 12.0M
67 209M 67 141M 0 0 12.1M 0 0:00:17 0:00:11 0:00:06 11.6M
73 209M 73 154M 0 0 12.2M 0 0:00:17 0:00:12 0:00:05 11.9M
80 209M 80 168M 0 0 12.2M 0 0:00:17 0:00:13 0:00:04 12.2M
86 209M 86 181M 0 0 12.3M 0 0:00:16 0:00:14 0:00:02 12.5M
93 209M 93 194M 0 0 12.4M 0 0:00:16 0:00:15 0:00:01 12.9M
99 209M 99 208M 0 0 12.4M 0 0:00:16 0:00:16 --:--:-- 13.2M
100 209M 100 209M 0 0 12.4M 0 0:00:16 0:00:16 --:--:-- 13.3M
Building flutter tool...

Upgrading engine...
Downloading android-arm-profile/darwin-x64 tools... 628ms
Downloading android-arm-release/darwin-x64 tools... 480ms
Downloading android-arm64-profile/darwin-x64 tools... 500ms
Downloading android-arm64-release/darwin-x64 tools... 477ms
Downloading android-x64-profile/darwin-x64 tools... 492ms
Downloading android-x64-release/darwin-x64 tools... 455ms
Downloading android-x86 tools... 1,246ms
Downloading android-x64 tools... 1,583ms
Downloading android-arm tools... 1,240ms
Downloading android-arm-profile tools... 618ms
Downloading android-arm-release tools... 452ms
Downloading android-arm64 tools... 1,600ms
Downloading android-arm64-profile tools... 662ms
Downloading android-arm64-release tools... 478ms
Downloading android-x64-profile tools... 666ms
Downloading android-x64-release tools... 489ms
Downloading android-x86-jit-release tools... 705ms
Downloading ios tools... 4.8s
Downloading ios-profile tools... 4.4s
Downloading ios-release tools... 18.3s
Downloading Web SDK... 4.6s
Downloading CanvasKit... 1,524ms
Downloading package sky_engine... 228ms
Downloading flutter_patched_sdk tools... 366ms
Downloading flutter_patched_sdk_product tools... 756ms
Downloading darwin-x64 tools... 3.3s
Downloading darwin-x64/font-subset tools... 338ms

Flutter 2.10.4 • channel stable • https://github.com/flutter/flutter.git
Framework • revision c860cba910 (13 days ago) • 2022-03-25 00:23:12 -0500
Engine • revision 57d3bac3dd
Tools • Dart 2.16.2 • DevTools 2.9.2

Running flutter doctor...
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 2.10.4, on macOS 11.6.4 20G417 darwin-x64, locale ja-JP)
[✓] Android toolchain - develop for Android devices (Android SDK version 32.1.0-rc1)
[✓] Xcode - develop for iOS and macOS (Xcode 13.2.1)
[✓] Chrome - develop for the web
[✓] Android Studio (version 2021.1)
[✓] VS Code (version 1.66.0)
[✓] Proxy Configuration
[✓] Connected device (1 available)
[✓] HTTP Host Availability

• No issues found!

最後にflutter doctorを実行して確認してくれていますね。

まとめ

Flutterのバージョン管理は確立した仕組みがあり、とても頼もしいです。このあたりで特に問題になることはなさそうですね。

参考図書

linterで指摘された点まとめ

背景

サンプルアプリを作成するため、多分1週間くらいでできるFlutter入門に載っていたToDoアプリを作成してみようと思い、せっせと写経していました。

この本は2021年11月発売で、それほど古くないはずなのですが、VSCodeのプラグインからStyleの警告がいくつか表示されます…

今後も増えてきそうなのでこちらでまとめていきたいと思います。

final_not_initialized

ToDoアプリのモデル(/lib/model/todo.dart)の写経を上から進めていくと、以下のコードでエラーが表示されました。

1
2
3
4
5
enum TodoStatus { to, doing, done }

class TodoContent {
final String title;
final String content;

title,contentの両方でエラーがでてしまい、final_not_initializedと表示されていました。

final_not_initializedで検索すると以下のページが見つかりました。

https://dart.dev/tools/diagnostic-messages#final_not_initialized

final修飾子を指定した場合は初期化しないとダメですよということのようです。

困ったなと思ったのですが、一旦先に進むことにしました。次はコンストラクタを定義するところでした。

1
2
TodoContent({required this.title, required this.content, TodoStatus? state}) {
}

コンストラクタを定義した途端、先程のfinal_not_initializedのエラーは消えました。コンストラクタで初期化が必須になったからだと思います。

unnecessary_this

以下のコードで警告が表示されました。

1
2
3
void setTo() {
this.state = TodoStatus.to;
}

警告を見るとunnecessary_thisと表示されていました。詳細は以下のリンクに記載がありました。

https://dart-lang.github.io/linter/lints/unnecessary_this.html

リンクに記載の通り、thisを削除することで、警告はなくなります。thisを書かなくても、インスタンスのプロパティと判断できる場合はthisは省略した方が良いということですね。

1
this.state = state;

このようなコードの場合はthisは必要です。

unnecessary_brace_in_string_interps

以下のコードで警告が表示されました。

1
2
3
4
@override
String toString() {
return "${title}, ${content}, ${state}";
}

toStringをオーバーライドしているメソッドですが、title, content,stateにそれぞれ警告が表示されています。unnecessary_brace_in_string_interpsと表示されており、リンクをクリックすると以下のページに遷移します。

https://dart-lang.github.io/linter/lints/unnecessary_brace_in_string_interps.html

単純な文字列展開だったら{}は省いてねっていうことだと思います。

1
2
3
4
@override
String toString() {
return "$title, $content, $state";
}

このように修正することで、警告はなくなりました。

まとめ

Dartについてはまだ始めたばかりなので、いろいろ躓くところが出てくると思いますが、一つずつ着実に理解して進めていこうと思います。

参考図書

DjangoでPDFを出力する

背景

業務用WebでPDFを出力する機能が必要になり、経験がなかったのでいろいろ調べてみました。

検索結果によく表示されているのがwkhtmltopdfというツールでした。django用のプラグインもあったので、一旦こちらで実装を進めていこうと思います。

wkhtmltopdfとは?

wkhtmltopdfはその名の通りhtmlをPDFとして出力するツールです。

公式サイトはこちら

今回はWebブラウザに表示されている内容をそのままPDFで出力できるようにしようと思っているので、要件にはあっています。また、Django用のプラグインもあり、容易にPDF出力できそうです。

wkhtmltopdfのインストール

まずコンテナにwkhtmltopdfをインストールします。apt installでインストールも可能ですが、パッケージが新規で200ほど、容量で500Mほど増えてしまうので、一旦wkhtmltopdfのバイナリパッケージのみインストールしてみます。

wkhtmltopdfのダウンロード

こちらからダウンロードできます。debianはbullseyeを使っていますが、bullseyeのパッケージがないので、一番近いbusterのパッケージをダウンロードします。

Dockerfileの修正

次にDockerfileを修正します。ダウンロードしたパッケージをコピーするディレクトリ(wkhtmltopdfディレクトリを作成しその下)に置き、apt installコマンドを追加します。

1
2
3
WORKDIR /code
ADD . /code/
RUN apt install ./wkhtmltopdf/wkhtmltox_0.12.6-1.buster_amd64.deb

イメージを再ビルドし、無事コンテナがビルドできることを確認します。

Djangoプラグインのインストール

次にDjangoプラグイン(django-wkhtmltopdf)を追加します。

1
$ poetry add django-wkhtmltopdf

pyproject.tomlとpoetry.lockが更新されます。

Djangoの設定

settings.pyにwkhtmltopdfの実行ファイルの場所などを指定します。

1
2
3
4
5
STATIC_ROOT = '/code/static/'
WKHTMLTOPDF_CMD = '/usr/local/bin/wkhtmltopdf'
WKHTMLTOPDF_CMD_OPTIONS = {
'quiet': True,
}

テスト用Viewの実装

最後に動作確認用のViewを仮で実装します。

1
2
3
4
5
6
7
8
9
10
from wkhtmltopdf.views import PDFTemplateView

class SamplePDFView(PDFTemplateView):
filename = "sample.pdf"
template_name = "sample.html"

def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["sample_data"] = SampleData.objects.get(pk=kwargs["pk"])
return context

ViewはPDFTemplateViewを継承して作成します。

filenameでダウンロードするときのファイル名を指定します。

PDFを出力する場合、ほとんどが動的に設定したい箇所だと思います。動的に設定したい箇所については、contextでデータを渡すことで対応できます。

日本語対応

先程作成したViewにアクセスできるURLを設定し、アクセスしてみます。

すると無事PDFをダウンロードできました。早速ファイルの中身を確認すると、日本語の部分が□になってしまっています…

フォントのインストール

日本語のフォントが入っていないDockerイメージでは日本語のフォントをインストールしないと日本語表示はできません。

幸い日本語フォントのパッケージがあったので、それをインストールします。Dockerfileを再度書き換えます。

1
RUN apt install -y fonts-takao-gothic

インストール後コンテナを再起動して、無事日本語が表示されました。

まとめ

Djangoの出力するHTMLをPDFに変換することができました。

いろんな選択肢があるようですが、今回利用したwkhtmltopdfは比較的利用しやすいものだと思います。

ヘッダー、フッターなどカスタマイズする場合があればまた記載したいと思います。

FlutterアプリケーションをiOS, Androidエミュレーターで起動する

背景

前回、Flutterのプロジェクトを作成し、デモアプリケーションが起動するところまで確認しました。

しかし、アプリケーションが起動したのはChromeでした。本来はiOSとAndroidのエミュレーターで動作させたいです。

今回は、デモアプリケーションをそれぞれのエミュレーターで動作させる方法を確認します。

iOSエミュレーターでの起動

iOSエミュレーターは

1
/Applications/Xcode.app/Contents/Developer/Applications

ディレクトリの下にあるSimulator.appです。一度上記のディレクトリを開いて、アイコンをダブルクリックしてみます。

iOSエミュレーター

無事エミュレーターが起動しました。

Flutterアプリケーションの実行

この状態でflutter runを実行してみます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ flutter run
Launching lib/main.dart on iPhone 13 in debug mode...
Running Xcode build...
└─Compiling, linking and signing... 4.3s
Xcode build done. 12.6s
Syncing files to device iPhone 13... 90ms

Flutter run key commands.
r Hot reload. 🔥🔥🔥
R Hot restart.
h List all available interactive commands.
d Detach (terminate "flutter run" but leave application running).
c Clear the screen
q Quit (terminate the application on the device).

💪 Running with sound null safety 💪

An Observatory debugger and profiler on iPhone 13 is available at: http://127.0.0.1:57320/EBquF8FZZxI=/
The Flutter DevTools debugger and profiler on iPhone 13 is available at: http://127.0.0.1:9100?uri=http://127.0.0.1:57320/EBquF8FZZxI=/

iOSエミュレーターでFlutterデモアプリケーション起動

エミュレーター上でデモアプリケーションが起動しました。

今回はアイコンをダブルクリックで起動しましたが、Spotlightから起動できることも確認しました。こちらの方が便利ですね。

Androidエミュレーターでの起動

次にAndroidのエミュレーターで起動してみます。

公式ドキュメントを参考にコマンドラインからの起動を試してみます。

インストールされているエミュレーターの確認

コマンドラインで起動する際には、仮想デバイスを指定しないといけません(-avdオプション)。なので、インストールされているエミュレーターを確認する必要があります。

先程の公式ドキュメントによると、emulator -list-avdsというコマンドでインストールされている仮想デバイスの一覧が表示できるようです。emulatorのパスは

1
/Users/user/Library/Android/sdk/emulator/emulator

なので、パス指定して実行してみます。

1
$ ~/Library/Android/sdk/emulator/emulator -list-avds

なにも表示されません。ということは仮想デバイスがインストールされていないということですね。早速インストールしてみます。

仮想デバイスの追加

公式ドキュメントの仮想デバイスを作成して管理するを参考にデバイスを追加してみます。

Android Studioを起動し、More Actions->Virtual Device Managerをクリックします。

No virtual Device addedとなっており、やはり仮想デバイスは一つも登録されていません。左上のCreate Deviceボタンをクリックしてみます。

すると、デバイスを選ぶ画面が表示されます。左のCategoryPhoneのままにしておき、右の一覧からデバイスを選択します。今回はPixel 5にしてみます。Pixel 5を選択し、Nextをクリックします。

次にSystem Imageの選択画面になります。ダウンロードしないと次に進めないので、推奨されたバージョンのダウンロードリンクをクリックしてダウンロードします。

ダウンロードが完了すると、Nextがクリックできるようになるので、次に進みます。

最後に仮想デバイス名を入力して完了です。デフォルトで名前が入っているので今回はそのままにしておきます。

追加した仮想デバイスの確認

再度コマンドラインで仮想デバイスの一覧を確認します。

1
2
$ ~/Library/Android/sdk/emulator/emulator -list-avds
Pixel_5_API_30

追加されていることが確認できました。

エミュレーターの起動

ではコマンドラインで起動します。仮想デバイスの指定以外のオプションはなしで起動してみます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ ~/Library/Android/sdk/emulator/emulator -avd Pixel_5_API_30
INFO | Android emulator version 31.2.8.0 (build_id 8143646) (CL:N/A)
WARNING | unexpected system image feature string, emulator might not function correctly, please try updating the emulator.
INFO | configAndStartRenderer: setting vsync to 60 hz
WARNING | cannot add library /Users/user/Library/Android/sdk/emulator/qemu/darwin-x86_64/lib64/vulkan/libvulkan.dylib: failed
INFO | added library /Users/user/Library/Android/sdk/emulator/lib64/vulkan/libvulkan.dylib
INFO | Started GRPC server at 127.0.0.1:8554, security: Local
INFO | Advertising in: /Users/user/Library/Caches/TemporaryItems/avd/running/pid_77656.ini
INFO | Your emulator is out of date, please update by launching Android Studio:
- Start Android Studio
- Select menu "Tools > Android > SDK Manager"
- Click "SDK Tools" tab
- Check "Android Emulator" checkbox
- Click "OK"

Androidエミュレーター

起動できました。

flutter runの実行

ではエミュレーターが起動した状態でflutter runを実行します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
$ flutter run
Using hardware rendering with device Android SDK built for x86. If you notice graphics
artifacts, consider enabling software rendering with "--enable-software-rendering".
Launching lib/main.dart on Android SDK built for x86 in debug mode...
Checking the license for package Android SDK Tools in /Users/user/Library/Android/sdk/licenses
License for package Android SDK Tools accepted.
Preparing "Install Android SDK Tools (revision: 26.1.1)".
"Install Android SDK Tools (revision: 26.1.1)" ready.
Installing Android SDK Tools in /Users/user/Library/Android/sdk/tools
"Install Android SDK Tools (revision: 26.1.1)" complete.
"Install Android SDK Tools (revision: 26.1.1)" finished.
Checking the license for package Android SDK Build-Tools 29.0.2 in /Users/user/Library/Android/sdk/licenses
License for package Android SDK Build-Tools 29.0.2 accepted.
Preparing "Install Android SDK Build-Tools 29.0.2 (revision: 29.0.2)".
"Install Android SDK Build-Tools 29.0.2 (revision: 29.0.2)" ready.
Installing Android SDK Build-Tools 29.0.2 in /Users/user/Library/Android/sdk/build-tools/29.0.2
"Install Android SDK Build-Tools 29.0.2 (revision: 29.0.2)" complete.
"Install Android SDK Build-Tools 29.0.2 (revision: 29.0.2)" finished.
Checking the license for package Android SDK Platform 31 in /Users/user/Library/Android/sdk/licenses
License for package Android SDK Platform 31 accepted.
Preparing "Install Android SDK Platform 31 (revision: 1)".
"Install Android SDK Platform 31 (revision: 1)" ready.
Installing Android SDK Platform 31 in /Users/user/Library/Android/sdk/platforms/android-31
"Install Android SDK Platform 31 (revision: 1)" complete.
"Install Android SDK Platform 31 (revision: 1)" finished.
Running Gradle task 'assembleDebug'... 157.2s
✓ Built build/app/outputs/flutter-apk/app-debug.apk.
Installing build/app/outputs/flutter-apk/app.apk... 1,035ms
Syncing files to device Android SDK built for x86... 116ms

Flutter run key commands.
r Hot reload. 🔥🔥🔥
R Hot restart.
h List all available interactive commands.
d Detach (terminate "flutter run" but leave application running).
c Clear the screen
q Quit (terminate the application on the device).

💪 Running with sound null safety 💪

An Observatory debugger and profiler on Android SDK built for x86 is available at:
http://127.0.0.1:58754/V-rtq-7NtAg=/
The Flutter DevTools debugger and profiler on Android SDK built for x86 is available at:
http://127.0.0.1:9100?uri=http://127.0.0.1:58754/V-rtq-7NtAg=/

AndroidエミュレーターでFlutterデモアプリケーション起動

初回なのでちょっと時間がかかりましたが、無事起動できました。

まとめ

Android, iOSそれぞれのエミュレーターでFlutterでもアプリケーションを実行することができました。それぞれのエミュレーターを起動した状態でflutter runすることで、エミュレーター上でアプリケーションを動作させることができるということがわかりました。

エミュレーターで起動するとモバイルアプリを開発するんだなという実感が湧いてきます。

次回はプロジェクトの中のファイルがどういったものなのかを確認していきたいと思います。

参考図書

Flutterでプロジェクトを作成する

背景

前回flutter doctorでのチェックが通りました。

今回はプロジェクトを作成して開発を始めたいと思います。

プロジェクトの作成

プロジェクトの作成方法は2つありまして、コマンドラインでの作成とAndroid Studioを使ってのGUIでの作成です。

再現性が高いようにコマンドラインで作成してみます。

コマンドラインでのプロジェクトの作成

1
2
3
4
$ flutter create -i swift -a kotlin --androidx --org org.book-reviews --description 'flutter sample app' flutter_sample_app
Could not find an option named "androidx".

Run 'flutter -h' (or 'flutter <command> -h') for available flutter commands and options.

androidxというオプションがないと言われています。書籍やWebサイトで調べてみると、最新の状態にすることでこのエラーを回避することができるようです。

まずは安定版に切り替えます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ flutter channel stable
Running "flutter pub get" in flutter_tools... 23.4s
Switching to flutter channel 'stable'...
git: From https://github.com/flutter/flutter
git: + b101bfe32f...680962aa75 beta -> origin/beta (forced update)
git: + b101bfe32f...680962aa75 dev -> origin/dev (forced update)
git: 79a0aad87c..db5b5c7dbb flutter-2.12-candidate.3 -> origin/flutter-2.12-candidate.3
git: * [new branch] flutter-2.12-candidate.4 -> origin/flutter-2.12-candidate.4
git: * [new branch] flutter-2.12-candidate.5 -> origin/flutter-2.12-candidate.5
git: * [new branch] flutter-2.12-candidate.6 -> origin/flutter-2.12-candidate.6
git: * [new branch] flutter-2.12-candidate.7 -> origin/flutter-2.12-candidate.7
git: 940986e00a..5e6a653865 master -> origin/master
git: * [new branch] update_console_link -> origin/update_console_link
git: * [new tag] 2.12.0-4.1.pre -> 2.12.0-4.1.pre
git: * [new tag] 2.12.0-4.0.pre -> 2.12.0-4.0.pre
git: Already on 'stable'
git: Your branch is up to date with 'origin/stable'.
Successfully switched to flutter channel 'stable'.
To ensure that you're on the latest build from this channel, run 'flutter upgrade'

flutter channelについては、ドキュメントで確認できます。

次にメッセージにあるようにflutter upgradeを実行します。

1
2
3
4
5
6
$ flutter upgrade
Flutter is already up to date on channel stable
Flutter 2.10.3 • channel stable • https://github.com/flutter/flutter.git
Framework • revision 7e9793dee1 (2 weeks ago) • 2022-03-02 11:23:12 -0600
Engine • revision bd539267b4
Tools • Dart 2.16.1 • DevTools 2.9.2

もともと最新だったようです。再度実行してみます。

1
2
3
4
$ flutter create -i swift -a kotlin --androidx --org org.book-reviews --description 'flutter sample app' flutter_sample_app
Could not find an option named "androidx".

Run 'flutter -h' (or 'flutter <command> -h') for available flutter commands and options.

結果は変わりません…-hオプションを指定して実行してみます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
$ flutter create -h
Create a new Flutter project.

If run on a project that already exists, this will repair the project, recreating any files that are missing.

Global options:
-h, --help Print this usage information.
-v, --verbose Noisy logging, including all shell commands executed.
If used with "--help", shows hidden options. If used with "flutter doctor", shows additional diagnostic information. (Use "-vv"
to force verbose logging in those cases.)
-d, --device-id Target device id or name (prefixes allowed).
--version Reports the version of this tool.
--suppress-analytics Suppress analytics reporting when this command runs.

Usage: flutter create <output directory>
-h, --help Print this usage information.
--[no-]pub Whether to run "flutter pub get" after the project has been created.
(defaults to on)
--[no-]offline When "flutter pub get" is run by the create command, this indicates whether to run it in offline mode or not. In offline mode,
it will need to have all dependencies already available in the pub cache to succeed.
--[no-]overwrite When performing operations, overwrite existing files.
--description The description to use for your new Flutter project. This string ends up in the pubspec.yaml file.
(defaults to "A new Flutter project.")
--org The organization responsible for your new Flutter project, in reverse domain name notation. This string is used in Java package
names and as prefix in the iOS bundle identifier.
(defaults to "com.example")
--project-name The project name for this new Flutter project. This must be a valid dart package name.
-i, --ios-language The language to use for iOS-specific code, either ObjectiveC (legacy) or Swift (recommended).
[objc, swift (default)]
-a, --android-language The language to use for Android-specific code, either Java (legacy) or Kotlin (recommended).
[java, kotlin (default)]
--platforms The platforms supported by this project. Platform folders (e.g. android/) will be generated in the target project. This
argument only works when "--template" is set to app or plugin. When adding platforms to a plugin project, the pubspec.yaml will
be updated with the requested platform. Adding desktop platforms requires the corresponding desktop config setting to be
enabled.
[ios (default), android (default), windows (default), linux (default), macos (default), web (default)]
-t, --template=<type> Specify the type of project to create.

[app] (default) Generate a Flutter application.
[module] Generate a project to add a Flutter module to an existing Android or iOS application.
[package] Generate a shareable Flutter project containing modular Dart code.
[plugin] Generate a shareable Flutter project containing an API in Dart code with a platform-specific implementation for Android, for
iOS code, or for both.
[skeleton] Generate a List View / Detail View Flutter application that follows community best practices.

-s, --sample=<id> Specifies the Flutter code sample to use as the "main.dart" for an application. Implies "--template=app". The value should be
the sample ID of the desired sample from the API documentation website (http://docs.flutter.dev/). An example can be found at:
https://api.flutter.dev/flutter/widgets/SingleChildScrollView-class.html
--list-samples=<path> Specifies a JSON output file for a listing of Flutter code samples that can be created with "--sample".

Run "flutter help" to see global options.

見たところ、--androidxというオプションはなさそうなので、なしで実行してみます。

1
2
3
4
5
6
7
8
9
10
11
12
$ flutter create -i swift -a kotlin --org org.book-reviews --description 'flutter sample app' flutter_sample_app 
Creating project flutter_sample_app...
Running "flutter pub get" in flutter_sample_app... 2,764ms
Wrote 96 files.

All done!
In order to run your application, type:

$ cd flutter_sample_app
$ flutter run

Your application code is in flutter_sample_app/lib/main.dart.

作成できました!

アプリケーションの起動

プロジェクト作成後のメッセージにアプリケーションの起動方法が書かれていたので早速実行してみます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ cd flutter_sample_app
$ flutter run
Launching lib/main.dart on Chrome in debug mode...
Waiting for connection from debug service on Chrome... 15.4s
This app is linked to the debug service: ws://127.0.0.1:52600/qyihwl33cdE=/ws
Debug service listening on ws://127.0.0.1:52600/qyihwl33cdE=/ws

💪 Running with sound null safety 💪

🔥 To hot restart changes while running, press "r" or "R".
For a more detailed help message, press "h". To quit, press "q".

An Observatory debugger and profiler on Chrome is available at: http://127.0.0.1:52600/qyihwl33cdE=
A message on the flutter/keyevent channel was discarded before it could be handled.
This happens when a plugin sends messages to the framework side before the framework has had an opportunity to register a listener. See the ChannelBuffers
API documentation for details on how to configure the channel to expect more messages, or to expect messages to get discarded:
https://api.flutter.dev/flutter/dart-ui/ChannelBuffers-class.html

Chromeが起動し、Flutter Demo Home Pageが表示されました。右下の+ボタンをクリックすると、真ん中に表示されている数字が増えていきます。

これでデモ画面が表示されるところまで確認できました。

まとめ

今回はプロジェクトの作成とアプリケーションの起動まで行いました。

それほどスムーズにはできなかったですが、モバイルアプリ開発の準備をすることができました。

これからアプリケーションの設定と実装を行っていきたいと思います。

参考図書

flutter doctorを使って開発環境を構築する

背景

前回flutter doctorの実行で躓いてしまいました。今回はflutter doctorで状況を確認し、開発環境を構築するところまで行いたいと思います。

flutter doctorの実行

まずflutter doctorを実行して状況を確認します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
$ ./bin/flutter doctor
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 2.10.3, on macOS 11.6 20G165 darwin-x64, locale ja-JP)
[✗] Android toolchain - develop for Android devices
✗ Unable to locate Android SDK.
Install Android Studio from: https://developer.android.com/studio/index.html
On first launch it will assist you in installing the Android SDK components.
(or visit https://flutter.dev/docs/get-started/install/macos#android-setup for
detailed instructions).
If the Android SDK has been installed to a custom location, please use
`flutter config --android-sdk` to update to that location.

[✗] Xcode - develop for iOS and macOS
✗ Xcode installation is incomplete; a full installation is necessary for iOS
development.
Download at: https://developer.apple.com/xcode/download/
Or install Xcode via the App Store.
Once installed, run:
sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer
sudo xcodebuild -runFirstLaunch
✗ CocoaPods not installed.
CocoaPods is used to retrieve the iOS and macOS platform side's plugin code that
responds to your plugin usage on the Dart side.
Without CocoaPods, plugins will not work on iOS or macOS.
For more info, see https://flutter.dev/platform-plugins
To install see https://guides.cocoapods.org/using/getting-started.html#installation
for instructions.
[✓] Chrome - develop for the web
[!] Android Studio (not installed)
[✓] VS Code (version 1.65.0)
[!] Proxy Configuration
! NO_PROXY does not contain 127.0.0.1
! NO_PROXY does not contain ::1
[✓] Connected device (1 available)
[✓] HTTP Host Availability

! Doctor found issues in 4 categories.

バツと!になっている箇所について対応が必要そうです。

Xcodeのインストール

まずは簡単そうなXcodeのインストールから行います。

App Storeからダウンロード

まずApp StoreからXcodeをインストールします。

ライセンスに同意する

ライセンスに同意する必要があるので、以下のコマンドを実行します

1
$ sudo xcodebuild -license

するとライセンスに関する文面が表示されます。スペースキーでページ送りをして、最後に以下の表示になったらagreeとタイプしてエンターキーを押します。

1
2
3
By typing 'agree' you are agreeing to the terms of the software license agreements. Type 'print' to print them or anything else to cancel, [agree, print, cancel] agree

You can view the license agreements in Xcode's About Box, or at /Applications/Xcode.app/Contents/Resources/English.lproj/License.rtf

状況確認

Xcodeがインストールできたので、再度flutter doctorで確認します。

1
2
3
4
5
6
7
8
9
10
11
$ flutter doctor
...(省略)...
[!] Xcode - develop for iOS and macOS (Xcode 13.2.1)
✗ CocoaPods not installed.
CocoaPods is used to retrieve the iOS and macOS platform side's plugin code that
responds to your plugin usage on the Dart side.
Without CocoaPods, plugins will not work on iOS or macOS.
For more info, see https://flutter.dev/platform-plugins
To install see https://guides.cocoapods.org/using/getting-started.html#installation
for instructions.
...(省略)...

ということで、CocoaPodsのインストールが必要でした。

CocoaPodsのインストール

先程のflutter doctorのメッセージにあるインストールのドキュメントを見てみます。

冒頭にYou can use a Ruby Version manager, however we recommend that you use the standard Ruby available on macOS unless you know what you're doing.とあるので、グローバルにインストールされているRubyを利用しようと思います。

1
2
3
4
5
6
7
8
$ which ruby
/Users/user/.anyenv/envs/rbenv/shims/ruby
$ ruby -v
ruby 2.6.3p62 (2019-04-16 revision 67580) [universal.x86_64-darwin20]
$ rbenv versions
* system
2.4.10
3.1.0

グローバルにインストールされているRubyが2.6.3ということですね。こちらにインストールします。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
$ sudo /usr/bin/gem install cocoapods
Password:
Fetching concurrent-ruby-1.1.9.gem
Fetching tzinfo-2.0.4.gem
Fetching zeitwerk-2.5.4.gem
Fetching activesupport-6.1.5.gem
Fetching fuzzy_match-2.0.4.gem
Fetching i18n-1.10.0.gem
Fetching nap-1.1.0.gem
Fetching httpclient-2.8.3.gem
Fetching algoliasearch-1.27.5.gem
Fetching ffi-1.15.5.gem
Fetching ethon-0.15.0.gem
Fetching typhoeus-1.4.0.gem
Fetching netrc-0.11.0.gem
Fetching public_suffix-4.0.6.gem
Fetching addressable-2.8.0.gem
Fetching cocoapods-core-1.11.3.gem
Fetching claide-1.1.0.gem
Fetching cocoapods-deintegrate-1.0.5.gem
Fetching cocoapods-downloader-1.5.1.gem
Fetching cocoapods-plugins-1.0.0.gem
Fetching cocoapods-search-1.0.1.gem
Fetching cocoapods-trunk-1.6.0.gem
Fetching cocoapods-try-1.2.0.gem
Fetching molinillo-0.8.0.gem
Fetching atomos-0.1.3.gem
Fetching colored2-3.1.2.gem
Fetching nanaimo-0.3.0.gem
Fetching rexml-3.2.5.gem
Fetching xcodeproj-1.21.0.gem
Fetching escape-0.0.4.gem
Fetching fourflusher-2.3.1.gem
Fetching gh_inspector-1.1.3.gem
Fetching ruby-macho-2.5.1.gem
Fetching cocoapods-1.11.3.gem
Successfully installed concurrent-ruby-1.1.9
Successfully installed i18n-1.10.0
Successfully installed tzinfo-2.0.4
Successfully installed zeitwerk-2.5.4
Successfully installed activesupport-6.1.5
Successfully installed nap-1.1.0
Successfully installed fuzzy_match-2.0.4
Successfully installed httpclient-2.8.3
A new major version is available for Algolia! Please now use the https://rubygems.org/gems/algolia gem to get the latest features.
Successfully installed algoliasearch-1.27.5
Building native extensions. This could take a while...
ERROR: Error installing cocoapods:
ERROR: Failed to build gem native extension.

current directory: /Library/Ruby/Gems/2.6.0/gems/ffi-1.15.5/ext/ffi_c
/System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/bin/ruby -I /System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0 -r ./siteconf20220317-44199-1ctmdly.rb extconf.rb
checking for ffi.h... *** extconf.rb failed ***
Could not create Makefile due to some reason, probably lack of necessary
libraries and/or headers. Check the mkmf.log file for more details. You may
need configuration options.

Provided configuration options:
--with-opt-dir
--without-opt-dir
--with-opt-include
--without-opt-include=${opt-dir}/include
--with-opt-lib
--without-opt-lib=${opt-dir}/lib
--with-make-prog
--without-make-prog
--srcdir=.
--curdir
--ruby=/System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/bin/$(RUBY_BASE_NAME)
--with-ffi_c-dir
--without-ffi_c-dir
--with-ffi_c-include
--without-ffi_c-include=${ffi_c-dir}/include
--with-ffi_c-lib
--without-ffi_c-lib=${ffi_c-dir}/lib
--enable-system-libffi
--disable-system-libffi
--with-libffi-config
--without-libffi-config
--with-pkg-config
--without-pkg-config
/System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0/mkmf.rb:467:in `try_do': The compiler failed to generate an executable file. (RuntimeError)
You have to install development tools first.
from /System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0/mkmf.rb:585:in `block in try_compile'
from /System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0/mkmf.rb:534:in `with_werror'
from /System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0/mkmf.rb:585:in `try_compile'
from /System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0/mkmf.rb:1109:in `block in have_header'
from /System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0/mkmf.rb:959:in `block in checking_for'
from /System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0/mkmf.rb:361:in `block (2 levels) in postpone'
from /System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0/mkmf.rb:331:in `open'
from /System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0/mkmf.rb:361:in `block in postpone'
from /System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0/mkmf.rb:331:in `open'
from /System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0/mkmf.rb:357:in `postpone'
from /System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0/mkmf.rb:958:in `checking_for'
from /System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0/mkmf.rb:1108:in `have_header'
from extconf.rb:10:in `system_libffi_usable?'
from extconf.rb:42:in `<main>'

To see why this extension failed to compile, please check the mkmf.log which can be found here:

/Library/Ruby/Gems/2.6.0/extensions/universal-darwin-20/2.6.0/ffi-1.15.5/mkmf.log

extconf failed, exit code 1

Gem files will remain installed in /Library/Ruby/Gems/2.6.0/gems/ffi-1.15.5 for inspection.
Results logged to /Library/Ruby/Gems/2.6.0/extensions/universal-darwin-20/2.6.0/ffi-1.15.5/gem_make.out

エラーが発生してしまいました。メッセージを確認すると、ffi.hが見つからないということのようです。

libffiがインストールされていないからかもしれないので、インストールしてみます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
$ brew install libffi
Running `brew update --preinstall`...
...(省略)...
==> Downloading https://ghcr.io/v2/homebrew/core/libffi/manifests/3.4.2
######################################################################## 100.0%
==> Downloading https://ghcr.io/v2/homebrew/core/libffi/blobs/sha256:a461f6ad21a23a725691385dbbec3eff958cf61d5282e84dc3f0483e307e1875
==> Downloading from https://pkg-containers.githubusercontent.com/ghcr1/blobs/sha256:a461f6ad21a23a725691385dbbec3eff958cf61d5282e84dc3f0483e307e1875?se=202
######################################################################## 100.0%
==> Pouring libffi--3.4.2.big_sur.bottle.tar.gz
==> Caveats
libffi is keg-only, which means it was not symlinked into /usr/local,
because macOS already provides this software and installing another version in
parallel can cause all kinds of trouble.

For compilers to find libffi you may need to set:
export LDFLAGS="-L/usr/local/opt/libffi/lib"
export CPPFLAGS="-I/usr/local/opt/libffi/include"

==> Summary
🍺 /usr/local/Cellar/libffi/3.4.2: 17 files, 599.8KB
==> `brew cleanup` has not been run in the last 30 days, running now...
Disable this behaviour by setting HOMEBREW_NO_INSTALL_CLEANUP.
Hide these hints with HOMEBREW_NO_ENV_HINTS (see `man brew`).
Removing: /Users/user/Library/Caches/Homebrew/icu4c--69.1... (27.2MB)
Removing: /Users/user/Library/Caches/Homebrew/nkf--2.1.5... (167KB)
Removing: /Users/user/Library/Caches/Homebrew/nkf_bottle_manifest--2.1.5... (7.9KB)
Removing: /Users/user/Library/Logs/Homebrew/telnet... (64B)

インストールできました。しかし、再度cocoapodsをインストールしようとしても同じエラーで止まります。

エラーログを見てみると、ruby/config.hが見つからないということで、シンボリックリンクを作成してみました。

1
2
$ cd /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX12.1.sdk/System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/include/ruby-2.6.0/ruby
$ sudo ln -sf ../../../../Headers/ruby/config.h

すると、内容は変わったのですが、いまだにエラーは出ます。

1
2
3
4
5
6
7
8
current directory: /Library/Ruby/Gems/2.6.0/gems/ffi-1.15.5/ext/ffi_c
make "DESTDIR="
make: *** No rule to make target `/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX12.1.sdk/System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/include/ruby-2.6.0/universal-darwin20/ruby/config.h', needed by `AbstractMemory.o'. Stop.

make failed, exit code 2

Gem files will remain installed in /Library/Ruby/Gems/2.6.0/gems/ffi-1.15.5 for inspection.
Results logged to /Library/Ruby/Gems/2.6.0/extensions/universal-darwin-20/2.6.0/ffi-1.15.5/gem_make.out

よくみると、universal-darwin20と書かれています。Rubyのバージョンにもそのように書いてありました。しかし、実際に存在するディレクトリはuniversal-darwin21です。この記事を参考に、シンボリックリンクを作成します。

1
2
$ cd /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX12.1.sdk/System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/include/ruby-2.6.0/
$ sudo ln -s universal-darwin21 universal-darwin20

再度試します

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
$ sudo gem install cocoapods
Password:
Building native extensions. This could take a while...
Successfully installed ffi-1.15.5
Successfully installed ethon-0.15.0
Successfully installed typhoeus-1.4.0
Successfully installed netrc-0.11.0
Successfully installed public_suffix-4.0.6
Successfully installed addressable-2.8.0
Successfully installed cocoapods-core-1.11.3
Successfully installed claide-1.1.0
Successfully installed cocoapods-deintegrate-1.0.5
Successfully installed cocoapods-downloader-1.5.1
Successfully installed cocoapods-plugins-1.0.0
Successfully installed cocoapods-search-1.0.1
Successfully installed cocoapods-trunk-1.6.0
Successfully installed cocoapods-try-1.2.0
Successfully installed molinillo-0.8.0
Successfully installed atomos-0.1.3
Successfully installed colored2-3.1.2
Successfully installed nanaimo-0.3.0
Successfully installed rexml-3.2.5
Successfully installed xcodeproj-1.21.0
Successfully installed escape-0.0.4
Successfully installed fourflusher-2.3.1
Successfully installed gh_inspector-1.1.3
Successfully installed ruby-macho-2.5.1
Successfully installed cocoapods-1.11.3
Parsing documentation for ffi-1.15.5
Installing ri documentation for ffi-1.15.5
Parsing documentation for ethon-0.15.0
Installing ri documentation for ethon-0.15.0
Parsing documentation for typhoeus-1.4.0
Installing ri documentation for typhoeus-1.4.0
Parsing documentation for netrc-0.11.0
Installing ri documentation for netrc-0.11.0
Parsing documentation for public_suffix-4.0.6
Installing ri documentation for public_suffix-4.0.6
Parsing documentation for addressable-2.8.0
Installing ri documentation for addressable-2.8.0
Parsing documentation for cocoapods-core-1.11.3
Installing ri documentation for cocoapods-core-1.11.3
Parsing documentation for claide-1.1.0
Installing ri documentation for claide-1.1.0
Parsing documentation for cocoapods-deintegrate-1.0.5
Installing ri documentation for cocoapods-deintegrate-1.0.5
Parsing documentation for cocoapods-downloader-1.5.1
Installing ri documentation for cocoapods-downloader-1.5.1
Parsing documentation for cocoapods-plugins-1.0.0
Installing ri documentation for cocoapods-plugins-1.0.0
Parsing documentation for cocoapods-search-1.0.1
Installing ri documentation for cocoapods-search-1.0.1
Parsing documentation for cocoapods-trunk-1.6.0
Installing ri documentation for cocoapods-trunk-1.6.0
Parsing documentation for cocoapods-try-1.2.0
Installing ri documentation for cocoapods-try-1.2.0
Parsing documentation for molinillo-0.8.0
Installing ri documentation for molinillo-0.8.0
Parsing documentation for atomos-0.1.3
Installing ri documentation for atomos-0.1.3
Parsing documentation for colored2-3.1.2
Installing ri documentation for colored2-3.1.2
Parsing documentation for nanaimo-0.3.0
Installing ri documentation for nanaimo-0.3.0
Parsing documentation for rexml-3.2.5
Installing ri documentation for rexml-3.2.5
Parsing documentation for xcodeproj-1.21.0
Installing ri documentation for xcodeproj-1.21.0
Parsing documentation for escape-0.0.4
Installing ri documentation for escape-0.0.4
Parsing documentation for fourflusher-2.3.1
Installing ri documentation for fourflusher-2.3.1
Parsing documentation for gh_inspector-1.1.3
Installing ri documentation for gh_inspector-1.1.3
Parsing documentation for ruby-macho-2.5.1
Installing ri documentation for ruby-macho-2.5.1
Parsing documentation for cocoapods-1.11.3
Installing ri documentation for cocoapods-1.11.3
Done installing documentation for ffi, ethon, typhoeus, netrc, public_suffix, addressable, cocoapods-core, claide, cocoapods-deintegrate, cocoapods-downloader, cocoapods-plugins, cocoapods-search, cocoapods-trunk, cocoapods-try, molinillo, atomos, colored2, nanaimo, rexml, xcodeproj, escape, fourflusher, gh_inspector, ruby-macho, cocoapods after 26 seconds
25 gems installed

無事インストールできました。

flutter doctorで確認します。

1
2
3
4
$ ./bin/flutter doctor
...(省略)...
[✓] Xcode - develop for iOS and macOS (Xcode 13.2.1)
...(省略)...

Xcodeはクリアしました。

Android toolchainのインストール

次にAndroid toolchainをインストールします。flutter doctorの出力にあるサイトからダウンロードしてインストールします。

ダウンロードしたdmgをダブルクリックし、アプリケーションアイコンをアプリケーションディレクトリにドラッグ&ドロップします。

インストールが終わったら、On first launch it will assist you in installing the Android SDK components.と書かれているので、Android Studioを起動します。

利用規約に同意してその他のコンポーネントをインストールします。

ここまで終わったらflutter doctorで確認します。

1
2
3
4
5
6
7
8
9
10
11
$ ./bin/flutter doctor
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 2.10.3, on macOS 11.6.4 20G417 darwin-x64, locale ja-JP)
[!] Android toolchain - develop for Android devices (Android SDK version 32.1.0-rc1)
✗ cmdline-tools component is missing
Run `path/to/sdkmanager --install "cmdline-tools;latest"`
See https://developer.android.com/studio/command-line for more details.
✗ Android license status unknown.
Run `flutter doctor --android-licenses` to accept the SDK licenses.
See https://flutter.dev/docs/get-started/install/macos#android-setup for more details.
...(省略)...

まだ足りないようでした。メッセージに従ってコマンドを実行していきます。

Android SDK Command-line Toolsのインストール

sdkmanagerのパスがわからなかったので、GUIで設定しました。

Android Studioを起動し、メニューのAndroid Studio->Preferenceを選択し、左メニューのAppearance & Behavior->System Setting->Android SDKを選択します。

右のタブでSDK Toolsを選択し、下に表示される一覧の中にAndroid SDK Command-line Tools (latest)にチェックを入れ、右下のApplyボタンをクリックします。

するとダウンロードとインストールが始まります。終わったらOKをクリックします。

ライセンスの同意

次にライセンスに同意します。メッセージに記載のあるコマンドを実行します。

1
2
3
4
5
6
7
8
9
10
11
12
13
$ ./bin/flutter doctor --android-licenses 
Warning: Failed to download any source lists! Fetch remote repository...
Warning: Still waiting for package manifests to be fetched remotely.
Warning: Still waiting for package manifests to be fetched remotely.y...
Warning: Still waiting for package manifests to be fetched remotely.y...
Warning: Still waiting for package manifests to be fetched remotely.y...
Warning: Still waiting for package manifests to be fetched remotely.y...
Warning: Still waiting for package manifests to be fetched remotely.y...
Warning: Still waiting for package manifests to be fetched remotely.y...
Warning: IO exception while downloading manifesttch remote repository...
Warning: IO exception while downloading manifest
Warning: Still waiting for package manifests to be fetched remotely.y...
All SDK package licenses accepted.======] 100% Computing updates...

だいぶ時間がかかりますし、うまくいってないようなメッセージが出ますが、結果的に全てのライセンスに同意できています。

それではflutter doctorで確認します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ ./bin/flutter doctor                   
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 2.10.3, on macOS 11.6.4 20G417 darwin-x64, locale ja-JP)
[✓] Android toolchain - develop for Android devices (Android SDK version 32.1.0-rc1)
[✓] Xcode - develop for iOS and macOS (Xcode 13.2.1)
[✓] Chrome - develop for the web
[✓] Android Studio (version 2021.1)
[✓] VS Code (version 1.65.2)
[!] Proxy Configuration
! NO_PROXY does not contain 127.0.0.1
! NO_PROXY does not contain ::1
[✓] Connected device (1 available)
[!] HTTP Host Availability
✗ HTTP host https://maven.google.com/ is not reachable. Reason: An error occurred while checking the HTTP host: Operation timed out
✗ HTTP host https://pub.dev/ is not reachable. Reason: An error occurred while checking the HTTP host: Operation timed out
✗ HTTP host https://cloud.google.com/ is not reachable. Reason: An error occurred while checking the HTTP host: Operation timed out

! Doctor found issues in 2 categories.

もう少しですね…

NO_PROXYの設定

NO_PROXYの設定はコマンドを実行するシェルで環境変数を設定するのだろうと思うので設定してみます。

~/.zshrcNO_PROXYを設定します

1
NO_PROXY=localhost,127.0.0.1,::1

シェルを再起動します

1
$ exec $SHELL -l

環境変数を確認します

1
2
$ env | grep -i no_proxy
NO_PROXY=localhost,127.0.0.1,::1

設定できました。

HTTP Host Availabilityの対応

環境変数のPROXYの設定が悪さをしていて、外部へのアクセスができてなかったので、unsetコマンドで削除しました。

最後にflutter doctorで確認します。

1
2
3
4
5
6
7
8
9
10
11
$ ./bin/flutter doctor
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 2.10.3, on macOS 11.6.4 20G417 darwin-x64, locale ja-JP)
[!] Android toolchain - develop for Android devices (Android SDK version 32.1.0-rc1)
! Some Android licenses not accepted. To resolve this, run: flutter doctor --android-licenses
[✓] Xcode - develop for iOS and macOS (Xcode 13.2.1)
[✓] Chrome - develop for the web
[✓] Android Studio (version 2021.1)
[✓] VS Code (version 1.65.2)
[✓] Connected device (1 available)
[✓] HTTP Host Availability

なぜかまたライセンスで!になっているので、コマンドを実行します。(やはり先程はエラーになっていたようです)

1
2
3
4
5
6
$ ./bin/flutter doctor --android-licenses
5 of 7 SDK package licenses not accepted. 100% Computing updates...
Review licenses that have not been accepted (y/N)? y

1/5: License android-googletv-license:
...(省略)...

ライセンスが表示されるのでyを押して同意します。

今度こそ最後にflutter doctorで確認します。

1
2
3
4
5
6
7
8
9
10
11
12
$ ./bin/flutter doctor                   
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 2.10.3, on macOS 11.6.4 20G417 darwin-x64, locale ja-JP)
[✓] Android toolchain - develop for Android devices (Android SDK version 32.1.0-rc1)
[✓] Xcode - develop for iOS and macOS (Xcode 13.2.1)
[✓] Chrome - develop for the web
[✓] Android Studio (version 2021.1)
[✓] VS Code (version 1.65.2)
[✓] Connected device (1 available)
[✓] HTTP Host Availability

• No issues found!

すべてクリアできました!

まとめ

flutter doctorですべてOKになるところまで実施しました。

大きく時間がかかったのはcocoapodsのインストールだけでした。こちらもMacのバージョンを上げたりしていなければ(darwin20とdarwin21の違い)、スムーズに進んだと思います。

とりあえず環境構築はできました。次からはモバイルアプリの開発を進めていきたいと思います。

参考図書

「dartが悪質なソフトウェアかどうかをAppleでは確認できないため、このソフトウェアは開けません」の対処法

背景

会社の業務でスマートフォンアプリを開発することになったので、今のスマホアプリ開発ツールを調査してみました。すると、flutterというツールが見つかりました。

flutterはgoogleが開発しているクロスプラットフォームなスマートフォンアプリ開発ツールで、現在も活発に開発が行われているようです。同じような開発ツールでReactNativeがあるようですが、”flutter ReactNative 比較”と検索してみると、

Flutterはよりシンプルで、OS(オペレーティングシステム)の更新によって行われた変更に対して耐性があります。 React Nativeはデバイスのネイティブ要素に依存しており、システムが更新された場合に iOSアプリケーションと Androidアプリケーションに個別に追加の適応作業が必要があります。

とのことなので、flutterの利用が良さそうです。ということで早速試してみました。

flutterSDKのダウンロード

まず、flutterSDKをダウンロードします。

https://docs.flutter.dev/development/tools/sdk/releases?tab=macos

最新版が今日(2022/03/03)リリースされていました。

ダウンロードしたファイルを適当な場所に展開します。

flutter doctorの実行

Flutter実践入門のチュートリアルをみていると、flutter doctorというコマンドで状況が確認できるということです。早速実行してみます。

1
$ ./bin/flutter doctor

すると、ポップアップが表示されdartが悪質なソフトウェアかどうかをAppleでは確認できないため、このソフトウェアは開けません。と表示され、OKボタンをクリックするとコンソールに

1
/Users/user/flutter/bin/internal/shared.sh: line 223: 40699 Killed: 9               "$DART" --disable-dart-dev --packages="$FLUTTER_TOOLS_DIR/.packages" $FLUTTER_TOOL_ARGS "$SNAPSHOT_PATH" "$@"

と表示され終了してしまいます。

「ソフトウェアは開けません」が表示された時の対応方法

Appleのサポートページに回答が書いてありました。

https://support.apple.com/ja-jp/guide/mac-help/mchleab3a043/mac

ポップアップが表示された時に、「Finderで開く」を選択して、Finderで表示し、開くことができないアプリケーションのアイコンを右クリック->開くを選択して一回開けば、次回から警告は表示されなくなります。

まとめ

スマートフォンアプリの開発は10年ぶりくらいで、10年前はUnityを使っていました。flutterはとても便利そうですが、新しい分野を学ぶときは勉強することが多いので、成長を感じつつも学ぶ量に圧倒されています。

flutterはこれからどんどん試していくのでまた気づきがあれば掲載します。

参考図書