Repository: geerlingguy/ansible-collection-mac Branch: master Commit: 6ddb5c4cb1ea Files: 32 Total size: 40.1 KB Directory structure: gitextract_x5j9spe2/ ├── .ansible-lint ├── .github/ │ ├── FUNDING.yml │ ├── stale.yml │ └── workflows/ │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .yamllint ├── LICENSE ├── README.md ├── galaxy-deploy.yml ├── galaxy.yml ├── meta/ │ └── runtime.yml ├── plugins/ │ └── README.md ├── roles/ │ ├── dock/ │ │ ├── README.md │ │ ├── defaults/ │ │ │ └── main.yml │ │ └── tasks/ │ │ ├── dock-add.yml │ │ ├── dock-position.yml │ │ ├── dock-remove.yml │ │ └── main.yml │ ├── homebrew/ │ │ ├── README.md │ │ ├── defaults/ │ │ │ └── main.yml │ │ ├── handlers/ │ │ │ └── main.yml │ │ ├── tasks/ │ │ │ └── main.yml │ │ └── vars/ │ │ └── main.yml │ └── mas/ │ ├── README.md │ ├── defaults/ │ │ └── main.yml │ └── tasks/ │ └── main.yml └── tests/ ├── ansible.cfg ├── inventory ├── requirements.yml ├── test.yml └── uninstall-homebrew.sh ================================================ FILE CONTENTS ================================================ ================================================ FILE: .ansible-lint ================================================ --- exclude_paths: - galaxy-deploy.yml - .ansible/roles/* skip_list: - risky-file-permissions - yaml - fqcn-builtins - fqcn[action] - name[template] - var-naming[no-role-prefix] - no-changed-when - galaxy[no-changelog] ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms --- github: geerlingguy patreon: geerlingguy ================================================ FILE: .github/stale.yml ================================================ # Configuration for probot-stale - https://github.com/probot/stale # Number of days of inactivity before an Issue or Pull Request becomes stale daysUntilStale: 90 # Number of days of inactivity before an Issue or Pull Request with the stale label is closed. # Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale. daysUntilClose: 30 # Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled) onlyLabels: [] # Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable exemptLabels: - bug - pinned - security - planned # Set to true to ignore issues in a project (defaults to false) exemptProjects: false # Set to true to ignore issues in a milestone (defaults to false) exemptMilestones: false # Set to true to ignore issues with an assignee (defaults to false) exemptAssignees: false # Label to use when marking as stale staleLabel: stale # Limit the number of actions per hour, from 1-30. Default is 30 limitPerRun: 30 pulls: markComment: |- This pull request has been marked 'stale' due to lack of recent activity. If there is no further activity, the PR will be closed in another 30 days. Thank you for your contribution! Please read [this blog post](https://www.jeffgeerling.com/blog/2020/enabling-stale-issue-bot-on-my-github-repositories) to see the reasons why I mark pull requests as stale. unmarkComment: >- This pull request is no longer marked for closure. closeComment: >- This pull request has been closed due to inactivity. If you feel this is in error, please reopen the pull request or file a new PR with the relevant details. issues: markComment: |- This issue has been marked 'stale' due to lack of recent activity. If there is no further activity, the issue will be closed in another 30 days. Thank you for your contribution! Please read [this blog post](https://www.jeffgeerling.com/blog/2020/enabling-stale-issue-bot-on-my-github-repositories) to see the reasons why I mark issues as stale. unmarkComment: >- This issue is no longer marked for closure. closeComment: >- This issue has been closed due to inactivity. If you feel this is in error, please reopen the issue or file a new issue with the relevant details. ================================================ FILE: .github/workflows/ci.yml ================================================ --- name: CI 'on': pull_request: push: branches: - master schedule: - cron: "0 5 * * 0" defaults: run: working-directory: ansible_collections/geerlingguy/mac jobs: lint: name: Lint runs-on: ubuntu-latest steps: - name: Check out the codebase. uses: actions/checkout@v4 with: path: ansible_collections/geerlingguy/mac - name: Set up Python 3. uses: actions/setup-python@v5 with: python-version: '3.x' - name: Install test dependencies. run: pip3 install yamllint ansible-core ansible-lint - name: Lint code. run: | yamllint . ansible-lint integration: name: Integration runs-on: ${{ matrix.os }} strategy: matrix: os: - macos-latest steps: - name: Check out the codebase. uses: actions/checkout@v4 with: path: ansible_collections/geerlingguy/mac - name: Uninstall GitHub Actions' built-in Homebrew. run: tests/uninstall-homebrew.sh - name: Uninstall GitHub Actions' built-in browser installs. run: | sudo rm -rf /Applications/Firefox.app sudo rm -rf /Applications/Google\ Chrome.app - name: Install test dependencies. run: | sudo pip3 install --upgrade pip sudo pip3 install ansible-core - name: Set up the test environment. run: | cp tests/ansible.cfg ./ansible.cfg cp tests/inventory ./inventory ansible-galaxy collection install community.general ansible-galaxy role install elliotweiser.osx-command-line-tools - name: Test the playbook's syntax. run: ansible-playbook tests/test.yml --syntax-check - name: Test the playbook. run: ansible-playbook tests/test.yml env: ANSIBLE_FORCE_COLOR: '1' - name: Idempotence check. run: | idempotence=$(mktemp) ansible-playbook tests/test.yml | tee -a ${idempotence} tail ${idempotence} | grep -q 'changed=0.*failed=0' && (echo 'Idempotence test: pass' && exit 0) || (echo 'Idempotence test: fail' && exit 1) env: ANSIBLE_FORCE_COLOR: '1' ================================================ FILE: .github/workflows/release.yml ================================================ --- name: Release 'on': push: tags: - '*' defaults: run: working-directory: ansible_collections/geerlingguy/mac jobs: release: name: Release runs-on: ubuntu-latest env: ANSIBLE_GALAXY_TOKEN: ${{ secrets.ANSIBLE_GALAXY_TOKEN }} ANSIBLE_FORCE_COLOR: 1 steps: - name: Check out the codebase. uses: actions/checkout@v2 with: path: ansible_collections/geerlingguy/mac - name: Set up Python 3. uses: actions/setup-python@v2 with: python-version: '3.x' - name: Install Ansible. run: pip3 install ansible-core - name: Release to Ansible Galaxy. run: ansible-playbook -i 'localhost,' galaxy-deploy.yml -e "github_tag=${{ github.ref }}" ================================================ FILE: .gitignore ================================================ roles/.DS_Store ================================================ FILE: .yamllint ================================================ --- extends: default rules: line-length: max: 180 level: warning comments: min-spaces-from-content: 1 comments-indentation: false braces: min-spaces-inside: 0 max-spaces-inside: 1 octal-values: forbid-implicit-octal: true forbid-explicit-octal: true ignore: | .github/stale.yml ================================================ FILE: LICENSE ================================================ Copyright 2021 Jeff Geerling Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Mac Collection for Ansible [![MIT licensed][badge-license]][link-license] [![Galaxy Collection][badge-collection]][link-galaxy] [![CI][badge-gh-actions]][link-gh-actions] This collection includes helpful Ansible roles and content to help with macOS automation. For a good example of the collection's usage, see the [Mac Dev Playbook](https://github.com/geerlingguy/mac-dev-playbook). Roles included in this collection (click on the link to see the role's README and documentation): - `geerlingguy.mac.homebrew` ([documentation](https://github.com/geerlingguy/ansible-collection-mac/blob/master/roles/homebrew/README.md)) - `geerlingguy.mac.mas` ([documentation](https://github.com/geerlingguy/ansible-collection-mac/blob/master/roles/mas/README.md)) - `geerlingguy.mac.dock` ([documentation](https://github.com/geerlingguy/ansible-collection-mac/blob/master/roles/dock/README.md)) ## Installation Install via Ansible Galaxy: ``` ansible-galaxy collection install geerlingguy.mac ``` Or include this collection in your playbook's `requirements.yml` file: ``` --- collections: - name: geerlingguy.mac ``` For a real-world example, see my [Mac Dev Playbook's requirements file](https://github.com/geerlingguy/mac-dev-playbook/blob/master/requirements.yml). ### Role Requirements Requires separate installation of the `elliotweiser.osx-command-line-tools` role. Because Ansible collections are not able to depend on roles, you will need to make sure that role is installed either by manually installing it with the `ansible-galaxy` command, or adding it under the `roles` section of your `requirements.yml` file: ```yaml --- roles: - name: elliotweiser.osx-command-line-tools collections: - name: geerlingguy.mac ``` ## Usage Here's an example playbook which installs some Mac Apps (assuming you are signed into the App Store), CLI tools via Homebrew, and Cask Apps using Homebrew: ```yaml - hosts: localhost connection: local gather_facts: false vars: mas_installed_app_ids: - 424389933 # Final Cut Pro - 497799835 # Xcode homebrew_installed_packages: - node - nvm - redis - ssh-copy-id - pv homebrew_cask_apps: - docker - firefox - google-chrome - vlc roles: - geerlingguy.mac.homebrew - geerlingguy.mac.mas ``` For a real-world usage example, see my [Mac Dev Playbook](https://github.com/geerlingguy/mac-dev-playbook). See the full documentation for each role in the role's README, linked above. ## License MIT ## Author This collection was created by [Jeff Geerling](https://www.jeffgeerling.com), author of [Ansible for DevOps](https://www.ansiblefordevops.com). [badge-gh-actions]: https://github.com/geerlingguy/ansible-collection-mac/workflows/CI/badge.svg?event=push [link-gh-actions]: https://github.com/geerlingguy/ansible-collection-mac/actions?query=workflow%3ACI [badge-collection]: https://img.shields.io/badge/collection-geerlingguy.mac-blue [link-galaxy]: https://galaxy.ansible.com/geerlingguy/mac [badge-license]: https://img.shields.io/github/license/geerlingguy/ansible-collection-mac.svg [link-license]: https://github.com/geerlingguy/ansible-collection-mac/blob/master/LICENSE [badge-gh-actions]: https://github.com/geerlingguy/ansible-role-homebrew/workflows/CI/badge.svg?event=push ================================================ FILE: galaxy-deploy.yml ================================================ --- # Automated release playbook for Ansible Collections. # # Originally based on Ericsysmin's 2020 blog post. Meant to be used in a GitHub # Actions CI environment. # # Requires a ANSIBLE_GALAXY_TOKEN secret to be configured on the GitHub repo. # # Usage: # ansible-playbook -i 'localhost,' galaxy-deploy.yml \ # -e "github_tag=${{ github.ref }}" - name: Deploy new Collection version to Galaxy. hosts: localhost connection: local gather_facts: false vars: namespace: geerlingguy collection: mac # Requires github_tag to be set when calling playbook. release_tag: "{{ github_tag.split('/')[-1] }}" pre_tasks: - name: Ensure ANSIBLE_GALAXY_TOKEN is set. fail: msg: A valid ANSIBLE_GALAXY_TOKEN must be set. when: "lookup('env', 'ANSIBLE_GALAXY_TOKEN') | length == 0" - name: Ensure the ~/.ansible directory exists. file: path: ~/.ansible state: directory - name: Write the Galaxy token to ~/.ansible/galaxy_token copy: content: | token: {{ lookup('env', 'ANSIBLE_GALAXY_TOKEN') }} dest: ~/.ansible/galaxy_token tasks: - name: Ensure the galaxy.yml tag is up to date. lineinfile: path: galaxy.yml regexp: "^version:" line: 'version: "{{ release_tag }}"' - name: Build the collection. command: ansible-galaxy collection build changed_when: true - name: Publish the collection. command: > ansible-galaxy collection publish ./{{ namespace }}-{{ collection }}-{{ release_tag }}.tar.gz changed_when: true ================================================ FILE: galaxy.yml ================================================ --- namespace: geerlingguy name: mac version: "1.2.0" readme: README.md authors: - geerlingguy description: Collection of macOS automation tools for Ansible. # Either a single license or a list of licenses for content inside of a collection. Ansible Galaxy currently only # accepts L(SPDX,https://spdx.org/licenses/) licenses. This key is mutually exclusive with 'license_file' license: - MIT # A list of tags you want to associate with the collection for indexing/searching. A tag name has the same character # requirements as 'namespace' and 'name' tags: - mac - macos - tools - osx - homebrew - brew - workstation - dev - mas - appstore - apps - configure - development - apple - ios - macbook - imac - macmini - server dependencies: community.general: ">=3.0.0" repository: https://github.com/geerlingguy/ansible-collection-mac documentation: https://github.com/geerlingguy/ansible-collection-mac homepage: https://www.jeffgeerling.com issues: https://github.com/geerlingguy/ansible-collection-mac/issues build_ignore: [] ================================================ FILE: meta/runtime.yml ================================================ --- requires_ansible: ">=2.15.0" ================================================ FILE: plugins/README.md ================================================ # Collections Plugins Directory This directory can be used to ship various plugins inside an Ansible collection. Each plugin is placed in a folder that is named after the type of plugin it is in. It can also include the `module_utils` and `modules` directory that would contain module utils and modules respectively. Here is an example directory of the majority of plugins currently supported by Ansible: ``` └── plugins ├── action ├── become ├── cache ├── callback ├── cliconf ├── connection ├── filter ├── httpapi ├── inventory ├── lookup ├── module_utils ├── modules ├── netconf ├── shell ├── strategy ├── terminal ├── test └── vars ``` A full list of plugin types can be found at [Working With Plugins](https://docs.ansible.com/ansible/2.11/plugins/plugins.html). ================================================ FILE: roles/dock/README.md ================================================ # Ansible Role: macOS Dock Automation This role automates the use of `dockutil` to manage the items in your macOS Dock. You can add, remove, and arrange Dock items. ## Requirements - **Homebrew**: Requires `homebrew` already installed (you can use `geerlingguy.mac.homebrew` to install it on your Mac). ## Role Variables Available variables are listed below, along with example values (see `defaults/main.yml`): ```yaml dockitems_remove: [] ``` Dock items to remove. ```yaml dockitems_persist: [] ``` Dock items to add. `pos` parameter is optional and will place the Dock item in a particular position in the Dock. ```yaml dockutil_install: true ``` Whether to install dockutil or not. If set to false you'll need to have installed dockutil prior to the execution of this role. See [this issue](https://github.com/geerlingguy/ansible-collection-mac/issues/42) for alternate installation methods, which may be necessary depending on your version of macOS. ## Dependencies - (Soft dependency) `geerlingguy.homebrew` ## Example Playbook ```yaml - hosts: localhost vars: dockitems_remove: - Launchpad - TV - Podcasts - 'App Store' dockitems_persist: - name: Messages path: "/Applications/Messages.app/" - name: Safari path: "/Applications/Safari.app/" pos: 2 - name: Sublime Text path: "/Applications/Sublime Text.app/" pos: 3 roles: - geerlingguy.mac.homebrew - geerlingguy.mac.dock ``` See the [Mac Development Ansible Playbook](https://github.com/geerlingguy/mac-dev-playbook) for an example of this role's usage. ## License MIT / BSD ## Author Information This role was created in 2021 by [Jeff Geerling](https://www.jeffgeerling.com/), author of [Ansible for DevOps](https://www.ansiblefordevops.com/). The contents of this role were originally created by [@dspolleke](https://github.com/dspolleke) as part of the [`mac-dev-playbook`](https://github.com/geerlingguy/mac-dev-playbook). ================================================ FILE: roles/dock/defaults/main.yml ================================================ --- # Dock items to remove. dockitems_remove: [] # - Launchpad # - TV # - Podcasts # - 'App Store' # Dock items to add. `pos` parameter is optional and will place the Dock item # in a particular position in the Dock. dockitems_persist: [] # - name: "Sublime Text" # path: "/Applications/Sublime Text.app/" # pos: 5 # Whether to install dockutil or not dockutil_install: true ================================================ FILE: roles/dock/tasks/dock-add.yml ================================================ --- - name: "See if Dock item {{ item.name | default(item) }} exists." ansible.builtin.command: "dockutil --find '{{ item.name }}'" register: dockitem_exists failed_when: > "No such file or directory" in dockitem_exists.stdout or "command not found" in dockitem_exists.stdout changed_when: false tags: ['dock'] - name: Get current dock section from output. set_fact: current_section: "{{ dockitem_exists.stdout | regex_replace('^.*was found in (.*) at slot.*$', '\\1') }}" when: dockitem_exists.rc == 0 tags: ['dock'] - name: Ensure Dock item {{ item.name | default(item) }} exists. ansible.builtin.command: | dockutil --add '{{ item.path }}' --label '{{ item.name }}' {% if not ansible_loop.last %}--no-restart{% endif %} when: dockitem_exists.rc > 0 or dockitem_exists.rc == 0 and current_section == 'recent-apps' tags: ['dock'] - name: Pause for 7 seconds between dock changes. ansible.builtin.pause: seconds: 7 when: - dockitem_exists.rc > 0 or dockitem_exists.rc == 0 and current_section == 'recent-apps' - ansible_loop.last tags: ['dock'] ================================================ FILE: roles/dock/tasks/dock-position.yml ================================================ --- - name: "Check the current Dock position of {{ item.name | default(item) }}." ansible.builtin.command: cmd: dockutil --find '{{ item.name | default(item) }}' register: dock_item_position failed_when: '"command not found" in dock_item_position.stdout' changed_when: false - name: Get current dock item position from output. set_fact: current_position: "{{ dock_item_position | regex_replace('^.*slot (.*) in.*$', '\\1') }}" - name: Move dock item to the correct position. ansible.builtin.command: cmd: | dockutil --move '{{ item.name | default(item) }}' --position '{{ item.pos }}' {% if not ansible_loop.last %}--no-restart{% endif %} when: current_position|int != item.pos|int ================================================ FILE: roles/dock/tasks/dock-remove.yml ================================================ --- - name: "See if {{ item }} is in the Dock." ansible.builtin.command: cmd: dockutil --find '{{ item }}' register: dockitem_exists changed_when: false failed_when: > "No such file or directory" in dockitem_exists.stdout or "command not found" in dockitem_exists.stdout tags: ['dock'] - name: Ensure Dock item {{ item }} is removed. ansible.builtin.command: cmd: | dockutil --remove '{{ item }}' {% if not ansible_loop.last %}--no-restart{% endif %} when: dockitem_exists.rc == 0 tags: ['dock'] - name: Pause for 7 seconds between dock changes. ansible.builtin.pause: seconds: 7 when: - dockitem_exists.rc == 0 - ansible_loop.last tags: ['dock'] ================================================ FILE: roles/dock/tasks/main.yml ================================================ --- - name: Install dockutil. community.general.homebrew: name: dockutil state: present notify: - Clear homebrew cache when: dockutil_install tags: ['dock'] - name: Remove configured Dock items. ansible.builtin.include_tasks: dock-remove.yml loop: "{{ dockitems_remove }}" loop_control: extended: true extended_allitems: false tags: ['dock'] - name: Ensure required dock items exist. ansible.builtin.include_tasks: dock-add.yml loop: "{{ dockitems_persist }}" loop_control: extended: true extended_allitems: false tags: ['dock'] - name: Ensure dock items are in correct position. ansible.builtin.include_tasks: dock-position.yml when: - item.pos is defined - item.pos > 0 loop: "{{ dockitems_persist }}" loop_control: extended: true extended_allitems: false tags: ['dock'] ================================================ FILE: roles/homebrew/README.md ================================================ # Ansible Role: Homebrew Installs [Homebrew][homebrew] on MacOS, and configures packages, taps, and cask apps according to supplied variables. ## Requirements None. ## Role Variables Available variables are listed below, along with default values (see [`defaults/main.yml`](defaults/main.yml)): homebrew_repo: https://github.com/Homebrew/brew The GitHub repository for Homebrew core. homebrew_prefix: "{{ (ansible_facts.machine == 'arm64') | ternary('/opt/homebrew', '/usr/local') }}" homebrew_install_path: "{{ homebrew_prefix }}{{ '/Homebrew' if ansible_facts.machine != 'arm64' }}" The path where Homebrew will be installed (`homebrew_prefix` is the parent directory). It is recommended you stick to the default, otherwise Homebrew might have some weird issues. If you change this variable, you should also manually create a symlink back to `/opt/homebrew` (or `/usr/local` if you're on an Intel-mac) so things work as Homebrew expects. homebrew_brew_bin_path: {{ homebrew_prefix }}/bin The path where `brew` will be installed. homebrew_installed_packages: - ssh-copy-id - pv - { name: vim, install_options: "with-luajit,override-system-vi" } Packages you would like to make sure are installed via `brew install`. You can optionally add flags to the install by setting an `install_options` property, and if used, you need to explicitly set the `name` for the package as well. By default, no packages are installed (`homebrew_installed_packages: []`). homebrew_uninstalled_packages: [] Packages you would like to make sure are _uninstalled_. homebrew_upgrade_all_packages: false Whether to upgrade homebrew and all packages installed by homebrew. If you prefer to manually update packages via `brew` commands, leave this set to `false`. homebrew_taps: - { name: my_company/internal_tap, url: 'https://example.com/path/to/tap.git' } Taps you would like to make sure Homebrew has tapped. homebrew_cask_apps: - firefox - { name: virtualbox, install_options:"debug,appdir=/Applications" } Apps you would like to have installed via `cask`. [Search][caskroom] for popular apps to see if they're available for install via Cask. Cask will not be used if it is not included in the list of taps in the `homebrew_taps` variable. You can optionally add flags to the install by setting an `install_options` property, and if used, you need to explicitly set the `name` for the package as well. By default, no Cask apps will be installed (`homebrew_cask_apps: []`). homebrew_cask_accept_external_apps: true Default value is `false` and would result in interruption of further processing of the whole role (and ansible play) in case any app given in `homebrew_cask_apps` is already installed without `cask`. Good for a tightly managed system. Specify as `true` instead if you prefer to silently continue if any App is already installed without `cask`. Generally good for a system that is managed with `cask` / `Ansible` as well as other install methods (like manually) at the same time. homebrew_cask_uninstalled_apps: - google-chrome Apps you would like to make sure are _uninstalled_. homebrew_cask_appdir: /Applications Directory where applications installed via `cask` should be installed. ansible_become_password: "" Set this to your account password if casks you want installed need elevated privileges while installing (like `microsoft-office`), preferably [encrypted via `ansible-vault`][link-vault-doc]. homebrew_use_brewfile: true Whether to install via a Brewfile. If so, you will need to install the `homebrew/bundle` tap, which could be done within `homebrew_taps`. homebrew_brewfile_dir: '~' The directory where your Brewfile is located. homebrew_clear_cache: false Set to `true` to remove the Hombrew cache after any new software is installed. homebrew_user: "{{ ansible_facts.user_id }}" The user that you would like to install Homebrew as. homebrew_group: "{{ ansible_facts.user_gid }}" The group that you would like to use while installing Homebrew. homebrew_folders_additional: [] Any additional folders inside `homebrew_prefix` for which to ensure homebrew user/group ownership. ## Dependencies - [elliotweiser.osx-command-line-tools][dep-osx-clt-role] ## Example Playbook - hosts: localhost vars: homebrew_installed_packages: - mysql roles: - geerlingguy.mac.homebrew See the `tests/local-testing` directory for an example of running this role over Ansible's `local` connection. See also: [Mac Development Ansible Playbook][mac-dev-playbook]. ## License [MIT][link-license] ## Author Information This role was created in 2014 by [Jeff Geerling][author-website], author of [Ansible for DevOps][ansible-for-devops]. #### Maintainer(s) - [Jeff Geerling](https://github.com/geerlingguy) - [Elliot Weiser](https://github.com/elliotweiser) [ansible-for-devops]: https://www.ansiblefordevops.com/ [author-website]: https://www.jeffgeerling.com/ [caskroom]: https://caskroom.github.io/search [homebrew]: http://brew.sh/ [dep-osx-clt-role]: https://galaxy.ansible.com/elliotweiser/osx-command-line-tools/ [link-galaxy]: https://galaxy.ansible.com/geerlingguy/homebrew/ [link-license]: https://raw.githubusercontent.com/geerlingguy/ansible-role-homebrew/master/LICENSE [link-gh-actions]: https://github.com/geerlingguy/ansible-role-homebrew/actions?query=workflow%3ACI [mac-dev-playbook]: https://github.com/geerlingguy/mac-dev-playbook [link-vault-doc]: https://docs.ansible.com/ansible/latest/user_guide/vault.html#creating-encrypted-variables ================================================ FILE: roles/homebrew/defaults/main.yml ================================================ --- homebrew_repo: https://github.com/Homebrew/brew homebrew_prefix: "{{ (ansible_facts.machine == 'arm64') | ternary('/opt/homebrew', '/usr/local') }}" homebrew_install_path: "{{ homebrew_prefix }}{{ '/Homebrew' if ansible_facts.machine != 'arm64' }}" homebrew_brew_bin_path: "{{ homebrew_prefix }}/bin" homebrew_installed_packages: [] homebrew_uninstalled_packages: [] homebrew_upgrade_all_packages: false homebrew_taps: [] homebrew_cask_apps: [] homebrew_cask_uninstalled_apps: [] homebrew_cask_appdir: /Applications homebrew_cask_accept_external_apps: false # Set this to your account password if casks need elevated privileges. # ansible_become_password: '' homebrew_use_brewfile: true homebrew_brewfile_dir: '~' homebrew_clear_cache: false homebrew_folders_additional: [] ================================================ FILE: roles/homebrew/handlers/main.yml ================================================ --- # handlers for ansible-role-homebrew - name: Clear homebrew cache file: path: "{{ homebrew_cache_path.stdout | trim }}" state: absent when: 'homebrew_clear_cache | bool' become: "{{ (homebrew_user != ansible_facts.user_id) | bool }}" become_user: "{{ homebrew_user }}" ================================================ FILE: roles/homebrew/tasks/main.yml ================================================ --- - name: Determine Homebrew ownership variables set_fact: homebrew_user: '{{ homebrew_user | default(ansible_facts.user_id) }}' homebrew_group: '{{ homebrew_group | default(ansible_facts.user_gid) }}' # Homebrew setup prerequisites. - name: Ensure Homebrew parent directory has correct permissions (Apple Silicon). file: path: "{{ homebrew_prefix }}" owner: "{{ homebrew_user }}" state: directory become: true when: ansible_facts.machine == 'arm64' - name: Ensure Homebrew parent directory has correct permissions (Intel). when: ansible_facts.machine == 'x86_64' block: - name: Ensure Homebrew parent directory has correct permissions (MacOS >= 10.13). file: path: "{{ homebrew_prefix }}" owner: root state: directory become: true when: "ansible_facts.distribution_version is version('10.13', '>=')" - name: Ensure Homebrew parent directory has correct permissions (MacOS < 10.13). file: path: "{{ homebrew_prefix }}" owner: root group: admin state: directory mode: "0775" become: true when: "ansible_facts.distribution_version is version('10.13', '<')" - name: Check if homebrew already exists. stat: path: "{{ homebrew_brew_bin_path }}/brew" register: pre_installed_brew - name: Ensure Homebrew directory exists. file: path: "{{ homebrew_install_path }}" owner: "{{ homebrew_user }}" group: "{{ homebrew_group }}" state: directory mode: "0775" become: true # Clone Homebrew. - name: Ensure Homebrew is installed. git: repo: "{{ homebrew_repo }}" version: master dest: "{{ homebrew_install_path }}" update: false depth: 1 become: true become_user: "{{ homebrew_user }}" when: not pre_installed_brew.stat.exists # Adjust Homebrew permissions. - name: Ensure proper permissions and ownership on homebrew_brew_bin_path dirs. file: path: "{{ homebrew_brew_bin_path }}" state: directory owner: "{{ homebrew_user }}" group: "{{ homebrew_group }}" mode: "0775" become: true - name: Ensure proper ownership on homebrew_install_path subdirs. file: path: "{{ homebrew_install_path }}" state: directory owner: "{{ homebrew_user }}" group: "{{ homebrew_group }}" recurse: true follow: false become: true # Place brew binary in proper location and complete setup. - name: Check if homebrew binary is already in place. stat: path: "{{ homebrew_brew_bin_path }}/brew" register: homebrew_binary check_mode: false - name: Symlink brew to homebrew_brew_bin_path. file: src: "{{ homebrew_install_path }}/bin/brew" dest: "{{ homebrew_brew_bin_path }}/brew" state: link when: not homebrew_binary.stat.exists become: true - name: Add missing folder if not on Apple-chipset set_fact: homebrew_folders_base: "{{ homebrew_folders_base + ['Homebrew'] }}" when: ansible_facts.machine != 'arm64' - name: Ensure proper homebrew folders are in place. file: path: "{{ homebrew_prefix }}/{{ item }}" state: directory owner: "{{ homebrew_user }}" group: "{{ homebrew_group }}" become: true loop: "{{ homebrew_folders_base + homebrew_folders_additional }}" - name: Collect package manager fact. setup: filter: ansible_facts.pkg_mgr - name: Perform brew installation. # Privilege escalation is only required for inner steps when # the `homebrew_user` doesn't match the `ansible_facts.user_id` become: "{{ (homebrew_user != ansible_facts.user_id) | bool }}" become_user: "{{ homebrew_user }}" block: - name: Force update brew after installation. command: "{{ homebrew_brew_bin_path }}/brew update --force" when: not pre_installed_brew.stat.exists - name: Where is the cache? command: "{{ homebrew_brew_bin_path }}/brew --cache" register: homebrew_cache_path changed_when: false check_mode: false # Tap. - name: Ensure configured taps are tapped. homebrew_tap: tap: '{{ item.name | default(item) }}' url: '{{ item.url | default(omit) }}' state: present loop: "{{ homebrew_taps }}" # Cask. - name: Ensure blacklisted cask applications are not installed. homebrew_cask: name: "{{ item }}" state: absent sudo_password: "{{ ansible_become_password | default(omit) }}" loop: "{{ homebrew_cask_uninstalled_apps }}" - name: Install configured cask applications. homebrew_cask: name: "{{ item.name | default(item) }}" state: present install_options: "{{ item.install_options | default('appdir=' + homebrew_cask_appdir) }}" accept_external_apps: "{{ homebrew_cask_accept_external_apps }}" sudo_password: "{{ ansible_become_password | default(omit) }}" loop: "{{ homebrew_cask_apps }}" notify: - Clear homebrew cache # Brew. - name: Ensure blacklisted homebrew packages are not installed. homebrew: name: "{{ item }}" state: absent loop: "{{ homebrew_uninstalled_packages }}" - name: Ensure configured homebrew packages are installed. homebrew: path: "{{ homebrew_brew_bin_path }}" name: "{{ item.name | default(item) }}" install_options: "{{ item.install_options | default(omit) }}" state: "{{ item.state | default('present') }}" loop: "{{ homebrew_installed_packages }}" notify: - Clear homebrew cache - name: Upgrade all homebrew packages (if configured). homebrew: update_homebrew: true upgrade_all: true when: homebrew_upgrade_all_packages notify: - Clear homebrew cache - name: Check for Brewfile. stat: path: "{{ homebrew_brewfile_dir }}/Brewfile" register: homebrew_brewfile check_mode: false - name: Install from Brewfile. command: "{{ homebrew_brew_bin_path }}/brew bundle" args: chdir: "{{ homebrew_brewfile_dir }}" when: homebrew_brewfile.stat.exists and homebrew_use_brewfile ================================================ FILE: roles/homebrew/vars/main.yml ================================================ --- homebrew_folders_base: - Cellar - Frameworks - Caskroom - bin - etc - include - lib - opt - sbin - share - share/zsh - share/zsh/site-functions - var ================================================ FILE: roles/mas/README.md ================================================ # Ansible Role: Mac App Store CLI (mas) Installs [mas](https://github.com/mas-cli/mas) on macOS, and installs macOS apps from the Mac App Store. ## Requirements - **Homebrew**: Requires `homebrew` already installed (you can use `geerlingguy.mac.homebrew` to install it on your Mac). - **Mac App Store account**: You can either sign into the Mac App Store via the GUI before running this role, or you can set the `mas_email` and `mas_password` prior to running the role. For security reasons, if you're going to use this role to sign in, you should use `vars_prompt` for at least the password; don't store unencrypted passwords with your playbooks! ## Role Variables Available variables are listed below, along with default values (see `defaults/main.yml`): mas_email: "" mas_password: "" The credentials for your Mac App Store account. The Apps you install should already be purchased by/registered to this account. If setting these variables statically (e.g. in an included vars file), you should encrypt the inventory using [Ansible Vault](http://docs.ansible.com/ansible/playbooks_vault.html). Otherwise you can use [`vars_prompt`](http://docs.ansible.com/ansible/playbooks_prompts.html) to prompt for the password at playbook runtime. If you leave both blank, and don't prompt for them, the role assumes you've already signed in via other means (e.g. via GUI or `mas signin [email]`), and will not attempt to sign in again. mas_signin_dialog: false Fallback to the built-in Mac App Store dialog to complete sign in. If set to yes, you must specify the aforementioned `mas_email` variable which will be autofilled in the dialog and prompt you to enter your password, followed by the 2FA authorization code if enabled on the account. ### Install apps mas_installed_apps: - { id: 425264550, name: "Blackmagic Disk Speed Test (3.0)" } - { id: 411643860, name: "DaisyDisk (4.3.2)" } - { id: 498486288, name: "Quick Resizer (1.9)" } - { id: 497799835, name: "Xcode (8.1)" } A list of apps to ensure are installed on the computer using the Mac App Store. You can get IDs for all your existing installed apps with `mas list`, and you can search for IDs with `mas search [App Name]`. The `name` attribute is not authoritative and only used to provide better information in the playbook output. mas_upgrade_all_apps: false Whether to run `mas upgrade`, which will upgrade all installed Mac App Store apps. mas_path: "{{ '/opt/homebrew/bin/mas' if ansible_facts.architecture == 'arm64' else '/usr/local/bin/mas' }}" When targeting a remote Mac, the PATH isn't always correct leading to the `mas` binary not being found. This variable allows you to override the path to the `mas` binary. The defaults above work for most installations, but you may need to override this if you have a custom Homebrew installation path or other custom setup. ### Remove installed apps mas_uninstalled_apps: - { id: 408981434, name: "iMovie" } - { id: 409183694, name: "Keynote" } - { id: 409201541, name: "Pages" } - { id: 409203825, name: "Numbers" } - { id: 682658836, name: "GarageBand" } A list of apps to uninstall from the computer, which were installed using the Mac App Store. You can get IDs for all your existing installed apps with `mas list`. The `name` attribute is not authoritative and only used to provide better information in the playbook output. ## Dependencies - (Soft dependency) `geerlingguy.homebrew` ## Example Playbook - hosts: localhost vars: mas_installed_apps: - { id: 497799835, name: "Xcode (8.1)" } roles: - geerlingguy.mac.homebrew - geerlingguy.mac.mas See the [Mac Development Ansible Playbook](https://github.com/geerlingguy/mac-dev-playbook) for an example of this role's usage. ## License MIT / BSD ## Author Information This role was created in 2016 by [Jeff Geerling](https://www.jeffgeerling.com/), author of [Ansible for DevOps](https://www.ansiblefordevops.com/). ================================================ FILE: roles/mas/defaults/main.yml ================================================ --- mas_email: "" mas_password: "" mas_uninstalled_apps: [] mas_installed_app_ids: [] # Deprecated mas_installed_apps: - { id: 425264550, name: "Blackmagic Disk Speed Test (3.0)" } - { id: 411643860, name: "DaisyDisk (4.3.2)" } - { id: 498486288, name: "Quick Resizer (1.9)" } - { id: 497799835, name: "Xcode (8.1)" } mas_upgrade_all_apps: false mas_signin_dialog: false mas_path: "{{ '/opt/homebrew/bin/mas' if ansible_facts.architecture == 'arm64' else '/usr/local/bin/mas' }}" ================================================ FILE: roles/mas/tasks/main.yml ================================================ --- - name: Ensure MAS is installed. homebrew: name: mas state: present - name: Get MAS account status command: '"{{ mas_path }}" account' register: mas_account_result failed_when: mas_account_result.rc > 1 check_mode: false changed_when: false when: - ansible_facts.distribution_version is version('12', '<') - name: Sign in to MAS when email and password are provided. command: '"{{ mas_path }}" signin "{{ mas_email }}" "{{ mas_password }}"' register: mas_signin_result when: - ansible_facts.distribution_version is version('10.13', '<') - mas_account_result.rc == 1 - mas_email is truthy - mas_password is truthy - not mas_signin_dialog - name: Sign in to MAS when email is provided, and complete password and 2FA using dialog. command: '"{{ mas_path }}" signin "{{ mas_email }}" "{{ mas_password }}" --dialog' register: mas_signin_result when: - ansible_facts.distribution_version is version('10.13', '<') - mas_signin_dialog - mas_account_result.rc == 1 - mas_email is truthy # - name: List installed MAS apps. # command: '"{{ mas_path }}" list' # register: mas_list # check_mode: false # changed_when: false # failed_when: mas_list.rc not in [0,1] - name: Ensure unwanted MAS apps are uninstalled. become: true environment: PATH: "{{ mas_path | dirname }}:{{ ansible_facts.env.PATH }}" community.general.mas: id: "{{ item.id | default(item) }}" state: absent loop: "{{ mas_uninstalled_apps }}" loop_control: label: "{{ item.name | default(item.id | default(item)) }}" - name: Ensure configured MAS apps are installed. become: true environment: PATH: "{{ mas_path | dirname }}:{{ ansible_facts.env.PATH }}" community.general.mas: id: "{{ item.id | default(item) }}" state: present loop: "{{ mas_installed_apps + mas_installed_app_ids }}" loop_control: label: "{{ item.name | default(item.id | default(item)) }}" - name: Upgrade all apps (if configured). become: true environment: PATH: "{{ mas_path | dirname }}:{{ ansible_facts.env.PATH }}" community.general.mas: upgrade_all: true when: mas_upgrade_all_apps ================================================ FILE: tests/ansible.cfg ================================================ [defaults] inventory = inventory collections_path = ../../../ ================================================ FILE: tests/inventory ================================================ [local] localhost ansible_connection=local ================================================ FILE: tests/requirements.yml ================================================ --- roles: - name: elliotweiser.osx-command-line-tools collections: - name: community.general ================================================ FILE: tests/test.yml ================================================ --- - name: Test the collection. hosts: localhost vars: homebrew_clear_cache: true homebrew_installed_packages: - ssh-copy-id # from homebrew/core - nginx-full # from dengi/nginx homebrew_cask_apps: - firefox # from hombrew/cask - google-chrome # from hombrew/cask homebrew_taps: - name: denji/nginx url: 'https://github.com/denji/homebrew-nginx.git' mas_uninstalled_apps: [] mas_installed_app_ids: [] mas_installed_apps: [] dockitems_remove: - Launchpad - TV dockitems_persist: - name: Safari path: "/Applications/Safari.app/" pos: 1 roles: - elliotweiser.osx-command-line-tools - geerlingguy.mac.homebrew - geerlingguy.mac.mas - geerlingguy.mac.dock ================================================ FILE: tests/uninstall-homebrew.sh ================================================ #!/bin/bash # # Uninstalls Homebrew using the official uninstall script. # Download and run the uninstall script. curl -sLO https://raw.githubusercontent.com/Homebrew/install/master/uninstall.sh chmod +x ./uninstall.sh sudo ./uninstall.sh --force # Clean up Homebrew directories. sudo rm -rf /usr/local/Homebrew sudo rm -rf /usr/local/Caskroom sudo rm -rf /usr/local/bin/brew sudo rm -rf /opt/homebrew