Ansibleを使ってmacのデフォルトシェルをBash5.0にしてみた

慣れ親しんだbash派です。
2020.08.01

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

Catalina以降macOSのデフォルトシェルはzshになりましたが、私は慣れ親しんだBashを使っています。

プリインストールされた /bin/bash はありますがバージョンが3.2と古く
かつHomebrewで最新版をインストールするのも万一Homebrew不調時にBashごと動作しなくなってしまったら…という不安があり
PCセットアップのたびBash5.0のソースコードをGNU bashのページからダウンロード/パッチ当て/ビルド/インストールした上でデフォルトシェルの変更をしていました。

先日私用PCでmacOSの再インストールを行う機会があり、
せっかくなのでこの作業をAnsible Playbookで自動化してみました。

動作確認環境

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.15.6
BuildVersion:   19G73
$ ansible --version
ansible 2.9.10
  (中略)
  python version = 3.8.3 (default, May 27 2020, 20:54:22) [Clang 11.0.3 (clang-1103.0.32.59)]

構成

フォルダ構成

最低限ローカル端末で動作させるための環境はこんな感じで
全部で3つのyamlファイルを作成しています。

$ tree
.
├── playbook.yml
└── roles
    └── bash
        ├── tasks
        │   └── main.yml
        └── vars
            └── main.yml

ファイルの中身

playbook.yaml

ローカル端末で今回作成したroleを動かす設定を入れます。

---
- hosts: localhost
  connection: local
  gather_facts: no
  become: no
  roles:
    - bash

roles/以下

vars/main.yml はこんな感じ。
patch_versionsで適用するパッチのバージョン一覧を指定していますが、いい感じの書き方がわからずこんなのになってしまいました。スマートな書き方ご存知でしたらぜひ教えてください…!

---
bash_bin: "/usr/local/bin/bash"
tmp_dir: "/tmp/bash/"
version: 5.0
patch_versions:
  - "001"
  - "002"
  - "003"
  - "004"
  - "005"
  - "006"
  - "007"
  - "008"
  - "009"
  - "010"
  - "011"
  - "012"
  - "013"
  - "014"
  - "015"
  - "016"
  - "017"
  - "018"

tasks/main.yml は以下のような形になりました。

---
# 既存の(非プリインストールの)Bash存在チェック
- name: bash existing check
  stat: 
    path: "{{ bash_bin }}"
  register: result

- block:
  # 作業用ディレクトリ作成
  - name: create tmp dir
    file: 
      path: "{{ tmp_dir }}"
      state: directory
      mode: 0755
  # ソースコードのダウンロード・展開
  - name: download bash {{ version }}
    get_url: 
      url: "http://ftp.gnu.org/gnu/bash/bash-{{version}}.tar.gz"
      dest: "{{ tmp_dir }}/bash-{{version}}.tar.gz"
  - name: unarchive bash {{ version }}
    command:
      cmd: "tar -zxvf {{ tmp_dir }}/bash-{{version}}.tar.gz -C {{ tmp_dir }}/"

  # パッチ適用
  - block:
      - name: download patches
        get_url: 
          url: "http://ftp.gnu.org/gnu/bash/bash-{{version}}-patches/bash50-{{ item }}"
          dest: "{{ tmp_dir }}/bash-{{ version }}"
        with_items: "{{ patch_versions }}"
      - name: apply patches
        patch:
          src: "{{ tmp_dir }}/bash-{{ version }}/bash50-{{ item }}"
          basedir: "{{ tmp_dir }}/bash-{{ version }}"
        with_items: "{{ patch_versions }}"
    when: patch_versions is defined
 
  # インストール
  - name: configure
    command: 
      chdir: "{{ tmp_dir }}/bash-{{ version }}"
      cmd: ./configure
  - name: make
    make:
      chdir: "{{ tmp_dir }}/bash-{{ version }}"
  - name: make install
    make:
      chdir: "{{ tmp_dir }}/bash-{{ version }}"
      target: install
    become: yes
  # /etc/shells編集
  - name: edit /etc/shells
    lineinfile:
      dest: /etc/shells
      line: "{{ bash_bin }}"
    become: yes
  # デフォルトシェルの変更
  - name: chsh
    command:
      "chsh -s {{ bash_bin }}"
  # 作業用ファイル削除
  - name: remove tmp dir
    file:
      state: absent
      path: "{{ tmp_dir }}/"
  when: result.stat.exists == False

ソースコード展開時にはunarchiveモジュールを使いたかったのですが、
unarchiveモジュールはmacにデフォルトで入っているBSD tarではなくGNU tarの存在を前提にしている様子で、私の環境では動作しませんでした。

実行

-Kオプションにより、最初にsudo用のパスワードを要求されます。あと、chsh実行時にもユーザのパスワードが必要です。
ok, changedの数は状況に応じて変わると思います。
Warningも赤裸々に見せちゃうぜ

$ ansible-playbook playbook.yml -K
BECOME password: 
[WARNING]: No inventory was parsed, only implicit localhost is available
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'

PLAY [localhost] **********************************************************************************************************************************

TASK [\bash existing check] ************************************************************************************************************************
ok: [localhost]

TASK [\bash : create tmp dir] **********************************************************************************************************************
ok: [localhost]

TASK [download bash 5.0] **************************************************************************************************************************
ok: [localhost]

TASK [unarchive bash 5.0] *************************************************************************************************************************
[WARNING]: Consider using the unarchive module rather than running 'tar'.  If you need to use command because unarchive is insufficient you can
add 'warn: false' to this command task or set 'command_warnings=False' in ansible.cfg to get rid of this message.
changed: [localhost]

TASK [\bash : download patches] ********************************************************************************************************************
ok: [localhost] => (item=001)
ok: [localhost] => (item=002)
ok: [localhost] => (item=003)
ok: [localhost] => (item=004)
ok: [localhost] => (item=005)
ok: [localhost] => (item=006)
ok: [localhost] => (item=007)
ok: [localhost] => (item=008)
ok: [localhost] => (item=009)
ok: [localhost] => (item=010)
ok: [localhost] => (item=011)
ok: [localhost] => (item=012)
ok: [localhost] => (item=013)
ok: [localhost] => (item=014)
ok: [localhost] => (item=015)
ok: [localhost] => (item=016)
ok: [localhost] => (item=017)
ok: [localhost] => (item=018)

TASK [\bash : apply patches] ***********************************************************************************************************************
changed: [localhost] => (item=001)
changed: [localhost] => (item=002)
changed: [localhost] => (item=003)
changed: [localhost] => (item=004)
changed: [localhost] => (item=005)
changed: [localhost] => (item=006)
changed: [localhost] => (item=007)
changed: [localhost] => (item=008)
changed: [localhost] => (item=009)
changed: [localhost] => (item=010)
changed: [localhost] => (item=011)
changed: [localhost] => (item=012)
changed: [localhost] => (item=013)
changed: [localhost] => (item=014)
changed: [localhost] => (item=015)
changed: [localhost] => (item=016)
changed: [localhost] => (item=017)
changed: [localhost] => (item=018)

TASK [\bash : configure] ***************************************************************************************************************************
changed: [localhost]

TASK [\bash : make] ********************************************************************************************************************************
changed: [localhost]

TASK [\bash : make install] ************************************************************************************************************************
changed: [localhost]

TASK [\bash : edit /etc/shells] ********************************************************************************************************************
ok: [localhost]

TASK [\bash : chsh] ********************************************************************************************************************************
Password for kato.saori: 
changed: [localhost]

TASK [\bash : remove tmp dir] **********************************************************************************************************************
changed: [localhost]

PLAY RECAP ****************************************************************************************************************************************
localhost                  : ok=12   changed=7    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

シェルを読み込み直すと、パッチ適用された最新のBashが動いてることの確認がとれますぞ!

$ bash --version
GNU bash, バージョン 5.0.18(2)-release (x86_64-apple-darwin19.6.0)
Copyright (C) 2019 Free Software Foundation, Inc.
ライセンス GPLv3+: GNU GPL バージョン 3 またはそれ以降 <http://gnu.org/licenses/gpl.html>

This is free software; you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

おわりに

これで今後のmacのセットアップが楽になるので嬉しいです!