22時に寝ようと思って2時に寝る。

備忘録や日記を書いてます。きょうは早く寝よう。

CircleCI - macOS イメージを使用するときに Ruby のバージョンを指定する

はじめに

CircleCI が用意している macOS イメージを使用する際に、任意のバージョンの Ruby を使いたい場合があると思います。この記事では、macOS イメージにプリインストールされている chruby を用いて、Ruby バージョンを切り替える方法を紹介します。

macOS Container の Ruby 環境について

既に複数バージョンの Ruby が用意されている

macOS が用意しているコンテナイメージには、コンテナがビルドされた時点で最新である安定版の Ruby と、その他にもいくつかのバージョンの Ruby が用意されています。各イメージのマニフェストを確認することで、それぞれのイメージで用意されている Ruby の一覧を確認できます。

例えば、サポートされている Xcode Versions 一覧 から 11.3.0 (Build 11C29) の Installed Software を確認すると、

  • デフォルトで使用される System Rubyruby 2.6.3p62
  • chruby で使用可能な Ruby2.5.72.6.5

が、それぞれ用意されていることが分かります。

使用するイメージによって chruby の Auto-Switching 機能の設定が違う

chruby には、各プロジェクトに配置された .ruby-version を見て、Ruby のバージョンを自動で切り替えてくれる Auto-Switching 機能があります。

macOS 10.14 (Mojave) / Xcode 11.1 とそれ以前のイメージでは、この Auto-Switching 機能がデフォルトで有効に設定されていましたが、macOS 10.15 (Catalina) / Xcode 11.2 とそれ以降のイメージでは、自分で Auto-Switching 機能を有効に設定する必要があります。

なお、この仕様変更の理由が気になったので調べてみると、こちらのディスカッションで言及されていて、

The reason that I decided to leave it off by default, is that it’s easier for users who don’t expect it to understand the system. There is no “magic” happening in the default image. To enable autoswitching, you will have to add an explicit step to your build.

In my opinion (and I’m not claiming to be right), it’s easier to explain to someone that they need to enable the autoswitcher to select different versions of Ruby, than to explain to someone that they need to remove the autoswitcher from their bash profile to disable it.

要約すると、以下のような理由から変更したようです。

  • Auto-Switching 機能をデフォルトで有効でなくした理由は、Auto-Switching することを期待していないユーザーにもよりシステムを理解してもらいやすい
  • Auto-Switching しないように ~/.bash_profile から Auto-Switching するコードを削除する必要があることを説明するよりも、有効にする方法を説明するほうが簡単

この記事では、macOS 10.15 (Catalina) / Xcode 11.2 とそれ以降のイメージを使用することを想定して説明します。

chruby を使用して任意のバージョンの Ruby を指定する

chruby の Auto-Switching 機能を有効にする

chruby の README.md を参考に Auto-Switching を有効にするコマンドを追加しました。

commands:
  fix_ruby_version:
    steps:
      - run:
          name: Auto-Switching ruby version with chruby
          command: |
            echo 'source /usr/local/share/chruby/chruby.sh' >> $BASH_ENV
            echo 'source /usr/local/share/chruby/auto.sh' >> $BASH_ENV
            source $BASH_ENV
      - run:
          name: Install Bundler gem
          command: gem install -N bundler

また、切り替えた先の Ruby に Bundler がインストールされていないことがあるので、このタイミングで一緒にインストールするようにしています。

Ruby バージョンを指定するための .ruby-version を用意する

プロジェクトのルートディレクトリに、.ruby-version ファイルを作成し、イメージで用意されている任意の Ruby のバージョンを記述します。

ruby-2.6.5

各ジョブからコマンドを呼び出す

各ジョブで Ruby を使用する(Ruby gems をインストールするなど)より前に fix_ruby_version コマンドを呼び出すと良いと思います。

さいごに

ふとした時、なぜか .ruby-version で指定したバージョンに切り替わっていないことに気づき、調べてみると macOS 10.15 (Catalina) / Xcode 11.2 以降は Auto-Switching 機能の設定が必要だったということが分かった、、、ということがあり、今回記事にしてみました。

ちなみに、CircleCI の日本語ドキュメントにはいまのところ仕様変更への言及はなかったので、まずは英語ドキュメントをあたってみる心構えが大事なのかも、という気付きもありました。

参考

CircleCI - iOSシミュレーター向け(x86_64)にアーカイブをビルドする

はじめに

CircleCI 上で、x86_64 アーキテクチャで動作する iOS シミュレーター向けにアーカイブをビルドする方法について紹介します。結論としては、fastlane の xcodebuild コマンド を用いて、SDK のパラメータに Simulator用 SDK を指定してあげることで実現できます。

背景

今回、手元に iPhone の実機が手元にない開発メンバーでも動作確認できるように iOS Simulator で動くアーカイブ.app ファイル)が必要になった、という経緯がありました。

以前から、CI上で App Store へアップロードするための実機向けのアーカイブ.ipaファイル)をビルドして Artifacts に置いて共有する運用としており、この .ipa ファイルを QA メンバーが参照して、そのビルド時点での動作を検証するなどしていました。

ただ、ここで共有されるアーカイブは実機の iPhone で動作する arm64 アーキテクチャ向けにビルドされたものであり、x86_64 アーキテクチャで動いている iOS Simulator では動作しません。そんな訳で、今回新しく iOS Simulator でも動作するアーカイブを生成し、共有する必要が出てきました。

iOS のデバイスごとのアーキテクチャの違いについてはこちらの記事が詳しいですので、ご参照下さい。

Fastfile に iOS Simulator をアーカイブする lane を追加する

以下の lane を追加しました。中身はシンプルで、xcodebuild コマンド を呼び出しているだけです。配信する予定のないアーカイブのため、署名作業は省いています。

lane :archive_for_simulator do
  xcodebuild(
    scheme: "MyApp",
    workspace: "MyApp.xcworkspace",
    configuration: "Release",
    sdk: "iphonesimulator",
    derivedDataPath: "build"
  )
end

指定しているパラメータ

パラメータ名 説明
configuration 構築する際に使用する Build Configuration を指定する。今回は、リリース時と同様の動作を見たいので、"Release"を指定。
sdk プロジェクトの構築する際に使用するSDKの名前またはパスを指定する。今回は、"iphonesimulator"を指定。
derivedDataPath 成果物を配置するパスを指定する。今回は、"build"ディレクトリを指定。

使用できる SDK の一覧を取得する

sdk パラメータで指定できるSDK は、xcodebuild -showsdks で調べることができます。 iphonesimulator13.4 というようにバージョン名も併せて指定することもできますが、このバージョンは CircleCI で指定している macOS イメージに依存しているため、あえて指定しないことで、自動的に追従できるようにしておくと良いと思います。

$ xcodebuild -showsdks
iOS SDKs:
    iOS 13.4                          -sdk iphoneos13.4

iOS Simulator SDKs:
    Simulator - iOS 13.4              -sdk iphonesimulator13.4

macOS SDKs:
    DriverKit 19.0                    -sdk driverkit.macosx19.0
    macOS 10.15                       -sdk macosx10.15

tvOS SDKs:
    tvOS 13.4                         -sdk appletvos13.4

tvOS Simulator SDKs:
    Simulator - tvOS 13.4             -sdk appletvsimulator13.4

watchOS SDKs:
    watchOS 6.2                       -sdk watchos6.2

watchOS Simulator SDKs:
    Simulator - watchOS 6.2           -sdk watchsimulator6.2

成果物が配置されるパス

上記の例だと、derivedDataPath にはbuild ディレクトリを指定していますが、この場合は build/Build/Products/Release-iphonesimulator/ 配下に MyApp.app が配置されるようになります。

gym コマンドを使っていない理由

余談ですが、もともと xcodebuild コマンドではなく、 gym コマンド を用いてビルドしようとしていましたが、パッケージングの途中に発生するエラーが回避できなかったため、代替手段として xcodebuild コマンドを使用しています。

.circle/config.ymliOS Simulator 向けのアーカイブを生成するジョブを追加する

最終的には以下のような設定としました。

version: 2.1

executors:
  default:
    macos:
      xcode: "11.3.0"
    shell: /bin/bash --login -eo pipefail
    environment:
      FL_OUTPUT_DIR: /Users/distiller/project/output

jobs:
  archive_for_simulator:
    executor: default
    steps:
      - run: mkdir $FL_OUTPUT_DIR
      - checkout
      - setup_ruby_gems
      - setup_cocoapods
      - run:
          name: Archive for simulator
          command: bundle exec fastlane archive_for_simulator
      - run:
          name: Compress the archive
          command: |
            cd build/Build/Products/Release-iphonesimulator/
            zip -r MyApp.app.zip MyApp.app
            mv MyApp.app.zip $FL_OUTPUT_DIR/
      - store_artifacts:
          path: /Users/distiller/project/output

ジョブの最後で、生成されたアーカイブを Artifacts にストアするようにしています。

f:id:azuuun:20200427154917p:plain
Artifacts タブからストアしたアーカイブを確認できる

このジョブをリリースする際の Workflow から呼び出してあげることで AppStore にアップロードされるアーカイブ相当、かつ、iOS Simulator で実行・確認できるアーカイブを生成・共有できます。

さいごに

昨今の情勢では、開発チーム全員が在宅勤務のため開発に使用できる実機デバイスが手元になく、QAメンバーが試験できないというケースも少なくないと思います。その際、暫定対策的に iOS Simulator を用いて試験を実施する場合は、この記事のような方法が応用できると思います。

参考

CircleCI - macOS コンテナ上で利用できる iOS Simulator を調べる

はじめに

CircleCI 上で UI Test を実行する際に任意の iOS Simulator を指定することがあると思います。指定するにあたって、macOS のコンテナ上で事前に用意されている iOS Simulator を把握する方法について紹介します。

結論としては、CircleCI のジョブに SSH 接続して、 xcrun simctl list devices コマンドを実行することで利用できる Simulator の一覧を取得できます。

指定するXcode バージョンによって利用できる Simulator のラインナップが違う

ここから各macOSコンテナインストールされているソフトウェアを確認できます。

RuntimesDevices を確認すると、利用できるOS バージョンとデバイスが分かります。ここで注意したいのは、デバイスによっては使えるOSが限定されているので(例えば、iPhone SE と iOS13.3 の組み合わせは存在しない)、それらの組み合わせを具体的に把握したい場合は、xcrun コマンドで調べる必要があります。

xcrun simctl list devices で、利用できる Simulator を一覧する

xcrun simctl は、Xcode に含まれる標準CLIで、主に Simulator をコマンドラインで操作・管理するための機能を備えています。

xcrun simctl list devices を実行してみます。

$ xcrun simctl list devices
== Devices ==
-- iOS 12.2 --
    iPhone 5s (1AF03B24-D667-4FA0-ABAD-6B5D255D7C42) (Shutdown)
    iPhone 6 Plus (31733952-2624-41B9-9CB9-BBF9A6A1BE8B) (Shutdown)
    iPhone 6 (75133BAE-AE13-405F-AB5A-F0BB0A3B1E98) (Shutdown)
    iPhone 6s (2692F9AD-1EBE-4FA0-B4DB-4A3F47ECA097) (Shutdown)
    iPhone 6s Plus (2EDEC588-1BC1-465C-BE77-1D58EE9ECE03) (Shutdown)
    iPhone SE (28CD4003-D4AC-4F97-BFDF-00AFDFE3F08E) (Shutdown)
    iPhone 7 (7339D766-17AB-4A8D-8214-E93657B0200B) (Shutdown)
    iPhone 7 Plus (671C4D3C-AB9E-481C-9D7D-3BCA62CAF5D9) (Shutdown)
    iPhone 8 (C43A0A30-A28D-4647-8B37-CD17B027E119) (Shutdown)
    iPhone 8 Plus (4CE0ADB9-F373-4723-9EAD-235BB7AA7690) (Shutdown)
    iPhone X (302D33E0-44B1-4105-8914-3C3BAF964AC3) (Shutdown)
    iPhone Xs (3200B852-8926-43DA-B4EC-D5A75F9E0FB6) (Shutdown)
    iPhone Xs Max (FCB63BD0-4FAC-4A0C-812E-80BD83CDD8C3) (Shutdown)
    iPhone Xʀ (D2B8D5FB-7100-47A1-BCF5-7017B372FF7A) (Shutdown)
    iPad Air (FFBA7D98-F642-4254-9372-423C3E48E43D) (Shutdown)
    iPad Air 2 (F5D49A9C-C2A9-4124-B988-5139A2401F58) (Shutdown)
    iPad Pro (9.7-inch) (50EABE28-EF6D-4AF3-B9A1-7B93F019112B) (Shutdown)
    iPad Pro (12.9-inch) (7CE07B4D-0A2F-44E2-A7BC-DC55EC35FB72) (Shutdown)
    iPad (5th generation) (48C502FB-7BC7-42C0-A07D-86D965A2914F) (Shutdown)
    iPad Pro (12.9-inch) (2nd generation) (CA495D19-CAEE-4885-9F81-99585E6926CA) (Shutdown)
    iPad Pro (10.5-inch) (86133C43-0127-444B-9EB7-2BD67A938306) (Shutdown)
    iPad (6th generation) (4125706C-FA8C-4720-B6E0-7283F29DE081) (Shutdown)
    iPad Pro (11-inch) (FAAA7E3B-DA38-4999-A89B-FC25D113F8B9) (Shutdown)
    iPad Pro (12.9-inch) (3rd generation) (CFDC9512-38A2-4CA3-AD7D-435456355D8C) (Shutdown)
    iPad Air (3rd generation) (38894DEB-5BCE-42EA-9AF9-D4548EDAC881) (Shutdown)
-- iOS 12.4 --
    iPhone 5s (046A9C21-AFD5-434D-9B61-EE18A73D674A) (Shutdown)
    iPhone 6 Plus (C9D65D15-39CD-45E3-9964-1C05F5A153AB) (Shutdown)
    iPhone 6 (B21C342B-B25E-4B3F-A836-6C9D514E0903) (Shutdown)
    iPhone 6s (FD0E061D-67E3-489B-9E36-4D41E2795EBD) (Shutdown)
    iPhone 6s Plus (601197A5-AF38-4937-B66C-E7AC63F7A982) (Shutdown)
    iPhone SE (825FCF5E-E65E-4D72-A549-D08BDE6560B6) (Shutdown)
    iPhone 7 (6C11E32B-1DB7-4BEA-BCF7-FD296E0298C9) (Shutdown)
    iPhone 7 Plus (6B704ABA-AF68-48CF-9359-509E1A3D29B3) (Shutdown)
    iPhone 8 (44955F91-CE61-44E8-AF4E-DBD00ECFDB61) (Shutdown)
    iPhone 8 Plus (7C58677A-B157-4D3A-A6A6-05D67C76B856) (Shutdown)
    iPhone X (014774AE-D600-451D-BF0B-300E6A2FF9E1) (Shutdown)
    iPhone Xs (76431C2E-B0E1-434F-AF14-9BA0F9F8EEBA) (Shutdown)
    iPhone Xs Max (A05F76CD-98E8-4847-91E6-4252A039B1B3) (Shutdown)
    iPhone Xʀ (A0C5B344-26B8-493F-BCD2-58DA441EC5A8) (Shutdown)
    iPad Air (8533D0C7-BF40-4F92-808F-8F06E1A698A3) (Shutdown)
    iPad Air 2 (07690BC7-DA6A-46A4-940E-EC057B83B29E) (Shutdown)
    iPad Pro (9.7-inch) (AE20F740-74C5-4DAC-8BAB-8C1332F1F55D) (Shutdown)
    iPad Pro (12.9-inch) (17CA1C31-5A2B-4E2F-B5BC-C9800A8F1678) (Shutdown)
    iPad (5th generation) (DA04D45E-D445-4998-B928-E3AE410AC062) (Shutdown)
    iPad Pro (12.9-inch) (2nd generation) (10C0C185-B40B-416C-A2D7-4A0B72AAB49F) (Shutdown)
    iPad Pro (10.5-inch) (4EB6A598-7D54-49D9-AAC1-B6A093C8A214) (Shutdown)
    iPad (6th generation) (8576A2FC-2A4D-4939-94A8-949E4970EB00) (Shutdown)
    iPad Pro (11-inch) (BCD341B2-F33D-4D98-940E-B423500FA95B) (Shutdown)
    iPad Pro (12.9-inch) (3rd generation) (B6B052AF-65C1-43AB-8D3A-D8EB7A9BFF06) (Shutdown)
    iPad Air (3rd generation) (C7A80B69-5CA9-4A08-A9A8-471DBCABE502) (Shutdown)
-- iOS 13.3 --
    iPhone 8 (24B098E9-40FA-4FAE-A84B-CAB08F19FEDE) (Shutdown)
    iPhone 8 Plus (06AD8C58-1FE4-4696-BA9A-B4E44D36FAC7) (Shutdown)
    iPhone 11 (B755A75C-FF5D-4392-A97D-7C8C6A272579) (Shutdown)
    iPhone 11 Pro (651EA440-C365-4043-BE5F-AAEA79A32469) (Shutdown)
    iPhone 11 Pro Max (43308B24-0726-46A7-B8F5-584682FBC83D) (Shutdown)
    iPad Pro (9.7-inch) (49AA7AB9-4BD2-441B-9BE9-9A1BF2002CA3) (Shutdown)
    iPad (7th generation) (97E6DA3C-984B-4720-8AAF-BE17A427085C) (Shutdown)
    iPad Pro (11-inch) (2AD0CF74-FD8D-4A23-BD3B-22A4C0FEAB44) (Shutdown)
    iPad Pro (12.9-inch) (3rd generation) (5CC57CFA-AE46-4FB7-B656-E7FD1E9994EC) (Shutdown)
    iPad Air (3rd generation) (9F710327-DAFB-4D6B-8197-445DCCAAFFB7) (Shutdown)
-- tvOS 12.4 --
    Apple TV (F0706864-3933-4D65-87F5-93E4EB602074) (Shutdown)
    Apple TV 4K (0403E229-3B97-4F69-96B1-5CD5077A4A50) (Shutdown)
    Apple TV 4K (at 1080p) (CFA38739-EB05-4972-8991-A1E42219704A) (Shutdown)
-- tvOS 13.3 --
    Apple TV (6A22CD8B-CD2F-48A8-974F-2ADB31C3F9D9) (Shutdown)
    Apple TV 4K (47432A98-CAA1-425B-A7FD-2D9D07E5C2A2) (Shutdown)
    Apple TV 4K (at 1080p) (EAF9A428-4A71-4062-8963-20E5465A540C) (Shutdown)
-- watchOS 5.3 --
    Apple Watch Series 2 - 38mm (665441D3-061D-4F3E-8D18-EC544060D43D) (Shutdown)
    Apple Watch Series 2 - 42mm (1D9027F4-73CA-4559-B247-19FC32BE3D5B) (Shutdown)
    Apple Watch Series 3 - 38mm (99C85FB8-5E99-4664-B1A6-D96BFD67888F) (Shutdown)
    Apple Watch Series 3 - 42mm (12A4DE0A-ABCF-40BF-B79E-9EBB33E06B21) (Shutdown)
    Apple Watch Series 4 - 40mm (63AB7682-E481-4A3C-84A9-3732EB4355B3) (Shutdown)
    Apple Watch Series 4 - 44mm (6AF06CD2-F876-484A-BCB6-92891A186AC0) (Shutdown)
-- watchOS 6.1 --
    Apple Watch Series 4 - 40mm (5DC1AF52-C03B-4D58-BA4E-21E3DB926BAF) (Shutdown)
    Apple Watch Series 4 - 44mm (7C1CB39D-A7D4-4A1A-BE58-A021A4DEFDB5) (Shutdown)
    Apple Watch Series 5 - 40mm (93698ADC-D708-4BDB-958B-2CD8FBFC14C2) (Shutdown)
    Apple Watch Series 5 - 44mm (6D4B4A49-2C32-4FB0-88E1-90DD5DF829F9) (Shutdown)

各 OS バージョンで利用可能なデバイスが一覧されました。端末名の後ろに続く文字列は、Simulator を一意に特定するための UDID と現在の状態です。

fastlane の scan 機能の run_tests に端末を指定する devices パラメーターがありますが、ここでは iPhone 11 Pro Max (13.2) のように #{device} (#{version}) という規則で端末を指定して、配列で渡してあげると良いと思います。

さいごに

当初、 UI Test を実行する際に「この端末のこのバージョンの組み合わせはあるだろ〜」と雰囲気で Simulator を指定して、'iPhone 11 Pro Max (12.4)', couldn’t find matching simulator と怒られたという経緯があり、ちゃんと調べることに至りました。何事も、雰囲気でやってはいけないですね。。

参考

CircleCI - コミットメッセージに特定の文字列が含まれていれば、ある処理を行う

はじめに

この記事では、CircleCI で「コミットメッセージに特定の文字列が含まれていれば、ある処理を行いたい」場合の実現方法について紹介します。結論としては、circleci-agent step halt コマンドを利用することで比較的シンプルに実現できます。

今回は、CircleCI 2.1 で動作確認を行っています。

実現したかったこと

これまで、iOS プロジェクトにおいて以下のルールでワークフローを定義していました。

  • master 以外のブランチは、Unit test を実行する
  • master ブランチは、Unit test に加えて、UI testApp Store Connect へのデプロイ を実行する

これに加えて、新たに以下のルールを追加しました。

  • master 以外のブランチは、コミットメッセージに「kick-ui-test」という文字列が含まれていれば、 Unit test のあとに UI test を実行する

定義したワークフローの全体像

f:id:azuuun:20200427105238p:plain
図: master ブランチ以外の Workflow
f:id:azuuun:20200427105329p:plain
図: master ブランチの Workflow

yaml の全体像

※ 各ジョブ内で行っている処理(呼び出している commands)については省略して例示します。

version: 2.1

jobs:
  unit_test:
    <<: *defaults
    steps:
      - unit-test

  run_ui_test_as_needed:
    <<: *defaults
    steps:
      - checkout
      - run:
          name: Decide whether to run ui-test, depending on the content of the commit message.
          command: |
            COMMIT_MESSAGE=$(git log -1 HEAD --pretty=format:%s)
            TRIGGER_MATCH_PATTERN="^.*kick-ui-test.*$"
            if [[ ${COMMIT_MESSAGE} =~ ${TRIGGER_MATCH_PATTERN} ]]; then
              echo "Continue to run ui_test, as the commit message contains kick-ui-test."
            else
              echo "Since the commit message does not include kick-ui-test, this job will be successfully terminated."
              circleci-agent step halt
            fi
      - ui_test

  ui_test:
    <<: *defaults
    steps:
      - ui_test

  deploy_appstore:
    <<: *defaults
    steps:
      - deploy

workflows:
  build-workflow:
    jobs:
      - unit_test
      - run_ui_test_as_needed:
          requires:
            - unit_test
          filters:
            branches:
              ignore: 
                - master
      - ui_test:
          filters:
            branches:
              only:
                - master
      - deploy_appstore:
          requires:
            - unit_test
            - ui_test
          filters:
            branches:
              only:
               - master

run_ui_test_as_needed ジョブについて

このジョブで 「コミットメッセージに「kick-ui-test」という文字列が含まれていれば、 Unit test のあとに UI test を実行する」という制御を実現しています。

run_ui_test_as_needed:
  <<: *defaults
  steps:
    - checkout
    - run:
        name: Decide whether to run ui-test, depending on the content of the commit message.
        command: |
          COMMIT_MESSAGE=$(git log -1 HEAD --pretty=format:%s)
          TRIGGER_MATCH_PATTERN="^.*kick-ui-test.*$"
          if [[ ${COMMIT_MESSAGE} =~ ${TRIGGER_MATCH_PATTERN} ]]; then
            echo "Continue to run ui_test, as the commit message contains kick-ui-test."
          else
            echo "Since the commit message does not include kick-ui, this job will be successfully terminated."
            circleci-agent step halt
          fi
    - ui_test

主に shell script で以下の処理を行っています。

  1. 直近のコミットメッセージを取得
  2. 正規表現で、特定したい文字列(今回であれば kick-ui-test)を定義
  3. 1. で取得したコミットメッセージに特定文字列が含まれているかを判定
    1. もし、含まれていれば何もしないことを echo するだけで、次のステップへ進む
    2. もし、含まれていなければその時点でジョブを正常終了する
  4. ui_test を実行する

3.2 で、ジョブを正常終了させる際には、circleci-agent step halt コマンドを利用しています。このコマンドは、ジョブを失敗させずに終了させることができるもので、ジョブを条件付きで実行する必要がある今回のようなケースに便利です。

Ending a Job from within a step | CircleCI

さいごに

今回は、master ブランチ以外(機能の実装途中など)でも手軽に ui_test を実行したいケースがあり、run_ui_test_as_needed job を追加しました。

CircleCI 2.0 から次のジョブを続行する前に手動による承認操作を待つ制御が可能になりましたが、仕組み上、手動で承認する手間が発生するため、今回のようにコミットする時点で特定ジョブの実行を制御できると便利だと感じました。

参考

fish - chruby で最新の Ruby をインストールする

f:id:azuuun:20191126192019p:plain

fish 環境に chruby をインストールする方法をまとめます。

github.com

実行環境

> sw_vers
ProductName:    Mac OS X
ProductVersion: 10.14.6
BuildVersion:   18G1012
> brew -v
Homebrew 2.1.16
Homebrew/homebrew-core (git revision 92599; last commit 2019-11-26)
> fish -v
fish, version 3.0.2

homebrew で chruby を導入

> brew install chruby

chruby-fish を導入

github.com

fish で chruby を利用するためのプラグインをインストールする。

> brew install chruby-fish

設定ファイルを読み込むために ~/.config/fish/config.fish に以下を追記。

source /usr/local/share/chruby/chruby.fish
source /usr/local/share/chruby/auto.fish

設定を反映させる。

> source ~/.config/fish/config.fish

ruby-install で最新の Ruby をインストールする

記事執筆時点(2019年11月26日)での最新の Ruby を導入します。

> ruby-install ruby
Successfully installed ruby 2.6.5 into /Users/username/.rubies/ruby-2.6.5
> chruby
   ruby-2.4.9
   ruby-2.6.5

プロジェクトで利用する Ruby バージョンを指定する

プロジェクトのルートディレクトリに .ruby-version ファイルを作成し、以下を追記。

2.6.5

バージョンが自動で切り替わるかを確認する

> ruby --version
ruby 2.3.7p456 (2018-03-28 revision 63024) [universal.x86_64-darwin18]
> cd example_ruby_project/
> cat .ruby-version
2.6.5
> ruby --version
ruby 2.6.5p114 (2019-10-01 revision 67812) [x86_64-darwin18]

ディレクトリに配置された .ruby-version を見て、Ruby のバージョンが切り替わっているのが分かります。

以上です。

参考

Android - FileProvider で外部アプリとファイルを共有する

FileProvider とは

Android7.0(API24, Android N)から、ファイルシステムに大きな変更が加わり、権限の仕様がより厳しいものへと変更されました。

API24 以降向けのアプリにおいて、プライベートディレクトリにアクセス制限が加わり、外部から存在、サイズ、メタデータなどの漏洩を防ぐことができます。

この権限の変更により以下のような副作用があります。

  • プライベートファイルの所有者は MODE_WORLD_READABLE および MODE_WORLD_WRITABLE を使用したパーミッションの緩和ができず、実行しようとすると SecurityException が発生する
  • 開発しているアプリのパッケージドメイン以外の file:// URIを渡すと、受け取り手がアクセスできないパスとなるため、 外部のアプリとのプライベートなファイルの共有には FileProvider の使用が推奨される

このように Android7.0 以降向けのアプリでは、Androidフレームワークによって自身のアプリ以外への file:// URIの公開ができず、 content:// URIへ変換し一時的なパーミッションを付与した上で URI をやりとりする必要があります。

ファイルに対してパーミッションを付与したり、 file:// から content://URIへ変換する最も簡単な方法は FileProvider クラスを使用することです。

FileProvider を用いてアプリ間のファイル共有を実現する

繰り返しとなりますが、自身のアプリから別のアプリにファイルを安全に共有するには、 content:// URIの形式でファイルをハンドルできるようにアプリを構成する必要があります。

Android フレームワークの FileProvider コンポーネントは、XML で指定した仕様に基づいてファイルのコンテンツ URI を生成します。具体的な手順を以下で説明していきます。

FileProvider の使用を AndroidManifest で宣言する

まず、 AndroidManifest.xml にエントリーを追加します。

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.mobileapp">
    <application
        ...>
        <provider
            android:name="android.support.v4.content.FileProvider"
            android:authorities="${applicationId}.provider"
            android:grantUriPermissions="true"
            android:exported="false">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_provider" />
        </provider>
        ...
    </application>
</manifest>

指定している各属性について、説明します。

android:authorities 属性

アプリのパッケージドメインプレフィックスとした命名URIの権限を設定します。ユニークである必要があるため、 ${applicationId}.provider などの名前が良いと思います。

android:exported 属性

FileProvider は公開する必要がないため false とします。

android:grantUriPermissions 属性

外部からのファイルへのアクセスを一時的に許可できるようにします。今回は、外のアプリとプライベートファイルを共有したいため、 true とします。

共有するディレクトリを宣言する

共有するファイルを配置するディレクトリを指定します。 res/xml 以下に file_provider.xml ファイルを作成し、以下の内容で記述します。

<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <files-path name="photos" path="photos/"/>
</paths>

この例では、 <files-path> タグでアプリの内部ストレージである files/ ディレクトリ以下のサブディレクトリを共有するようにしてます。このサブディレクトリのパスは、 Context.filesDir で取得できます。この他にも、以下のようなタグが指定可能です。

  • <cache-path>
    • 内部ストレージのキャッシュを共有でき、パスは Context.cacheDir で取得できる
  • <external-path>
    • 外部ストレージのルートにあるファイルを共有でき、ルートパスは Environment.getExternalStorageDirectory() で取得できる
  • <external-files-path>
    • 外部ストレージのルートにあるディレクトリを共有でき、パスは Context.getExternalFilesDir() で取得できる
  • <external-cache-path>
    • 外部ストレージにあるキャッシュを共有でき、パスは Context.externalCacheDir で取得できる
  • <external-media-path>
    • 外部メディアにあるディレクトリを共有でき、パスは Context.externalMediaDirs で取得できる

また、タグに含まれる属性については説明します。

name="name"

URIパスのセグメント。この値は、生成されるURIのパスに含まれるものです。

path="path"

共有するサブディレクトリ。値はサブディレクトリ名であり、個々のファイル名ではないことに注意します。ファイル名で単一のファイルを共有したり、ワイルドカードを使用して指定することもできません。

生成されるURIを見てみる

以下のように、複数のパスを指定することもできます。下記の定義の場合、生成されるURIと実態のあるコンテンツのパスを見てみます。(Providerの指定は冒頭でAndroidManifestファイルで定義したものを想定)

<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <files-path name="my_images" path="images/"/>
    <files-path name="my_docs" path="docs/"/>
</paths>

共有のコンテンツのURIは、

  • content://com.example.mobileapp.provider/my_images/example.jpg
  • content://com.example.mobileapp.provider/my_docs/example.pdf

コンテンツの実態のパスは、

  • com.example.mobileapp/files/images/example.jpg
  • com.example.mobileapp/files/docs/example.pdf

となります。

ファイルからコンテンツURIを生成する

コンテンツURIを使用してファイルを他のアプリと共有する場合は、FileProvider を使用して URI を生成する必要があります。具体的には以下のステップを踏みます。

  1. 新しいファイルを生成する
  2. そのファイルを FileProvider.getUriForFile() に渡す
  3. 返された URIインテントを使って別のアプリに送信する

具体的なコードは以下のようになります。

val captureFile = this.createOutputFile()
val contentUri = FileProvider.getUriForFile(activity, BuildConfig.APPLICATION_ID + ".provider", captureFile)

getUriForFile(Context context, String authority, File file) によって、共有コンテンツURIを生成してくれます。

例えば、画像ファイルを共有したい場合は createOutPutFile() の中身は以下のようになります。

fun createOutputFile(): File {
    val timeStamp = DateFormat.format("yyyyMMdd_HHmmss", Date()).toString()
    val tempFile = File( this.activity.filesDir, "/my_images/$timeStamp.jpg")
    if (!tempFile.exists()) {
        try {
            tempFile.parentFile.mkdirs()
            tempFile.createNewFile()
        } catch (e: IOException) {
            e.printStackTrace()
        }
    }
    return tempFile
}

ここで注意したいのは、File はインスタンス化した後、実態としてストレージに空の状態で一時保存することです。

そのためにこの関数内では /my_images ディレクトリの存在を確認し、なければ親ディレクトリとして生成し、その配下にタイムスタンプをファイル名として tempFile.createNewFile() しています。

外部アプリへコンテンツURIを渡す

今回は、前項で生成した画像形式の一時ファイルをカメラアプリへ共有してみます。具体的なコードは以下です。

val cameraIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
this.cameraContentUri = createOutputUri()
cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, this.cameraContentUri)
cameraIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
this.activity.startActivityForResult(cameraIntent, REQUEST_CODE)

重要なのは、外部アプリに対して共有コンテンツへの一時的なアクセス許可を与えてあげることです。今回は書き込み権限を与えるために Intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION) を指定しています。

以上が、FileProvider を用いたアプリ間のファイル共有の手順です。

参考文献

fish - rbenv で最新の Ruby をインストールする

f:id:azuuun:20181116203145p:plain

fish 環境に ruby の開発環境を構築する方法をまとめます。

github.com

実行環境

> sw_vers
ProductName:    Mac OS X
ProductVersion: 10.14.1
BuildVersion:   18B75
> brew -v
Homebrew 1.8.2

homebrew で rbenv を導入

> brew install rbenv

fish で rbenv を使う設定

vi ~/.config/fish/config.fish に以下を追記

## rbenv init setting
status --is-interactive; and source (rbenv init -|psub)

設定を反映させる

> . ~/.config/fish/config.fish

rbenv で最新の ruby を導入する

今回は 2.4.5(2018年11月16日現在の最新)をインストールします。

> rbenv install --list
Available versions:
  1.8.5-p52
  1.8.5-p113
  1.8.5-p114
  1.8.5-p115
  1.8.5-p231
...

> rbenv install 2.4.5
ruby-build: use openssl from homebrew
Downloading ruby-2.4.5.tar.bz2...
-> https://cache.ruby-lang.org/pub/ruby/2.4/ruby-2.4.5.tar.bz2
Installing ruby-2.4.5...
Installed ruby-2.4.5 to /Users/hoge/.rbenv/versions/2.4.5

使用したい version を設定する

> rbenv versions
* system (set by /Users/hoge/.rbenv/version)
  2.4.5
> rbenv global 2.4.5
> rbenv versions
system
* 2.4.5 (set by /Users/hoge/.rbenv/version)
> ruby -v
ruby 2.4.5p335 (2018-10-18 revision 65137) [x86_64-darwin18]

以上です。