Skip to content

Commit

Permalink
Apple m1 opt homebrew support (Fixes #153) (#154)
Browse files Browse the repository at this point in the history
* Add rspec tests for Apple M1 silicon Homebrew path helper

* Add Homebrew.install_path() helper for Apple M1 silicon (arm64)

* Add HomebrewWrapper class to use install_path() M1 detection function

* Convert /usr/local to use install_path() for M1 /opt/homebrew support

* Fixing InSpec tests for macOS M1 / arm64 and x86_64

Note: InSpec running as root will not have PATH to Homebrew set up.  Thus, package checks will fail if `inspec --sudo` is used

* Remove deprecated full option for Homebrew (Breaking upstream change!)

Error was:

    Error: Calling the `--full` switch is disabled! There is no replacement.

* Use private macOS Vagrant boxes w/up-to-date Chef Infra & InSpec for testing this cookbook

* Add Homebrew.repository_path() helper for Apple M1 silicon (arm64)

* Use HomebrewWrapper.repository_path() for homebrew_tap resource idempotency on Apple M1 silicon (arm64)

* Revert "Use private macOS Vagrant boxes [...]" for upstream pull request

This reverts commit 5a9f462.

* CHANGELOG.md: Update Unreleased section w/Apple M1 changes

* CHANGELOG.md: Fix markdownlint issues

* Autocorrect Cookstyle issues
  • Loading branch information
trinitronx authored Dec 21, 2021
1 parent 8e02dce commit 2980305
Show file tree
Hide file tree
Showing 10 changed files with 149 additions and 13 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@ This file is used to list changes made in each version of the homebrew cookbook.

## Unreleased

- Update to support Apple M1 silicon (arm64) Homebrew install location (`/opt/homebrew`)
- Add HomebrewWrapper.repository_path() for homebrew_tap resource idempotency
- Add HomebrewWrapper.repository_path() helper for Apple M1 silicon (arm64)
- Remove deprecated `--full` option for Homebrew (Breaking upstream CLI change!)
- Add chefspec tests for Apple M1 silicon Homebrew path helper
- Add InSpec tests for macOS M1 / arm64 and x86_64
- Set `use_sudo: false` for InSpec tests to work properly
- Convert hardcoded /usr/local to use install_path() for M1 /opt/homebrew support
- Add Homebrew.install_path() helper for Apple M1 silicon (arm64)

## 5.2.2 - *2021-08-30*

- Standardise files with files in sous-chefs/repo-management
Expand Down
1 change: 1 addition & 0 deletions kitchen.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ provisioner:

verifier:
name: inspec
sudo: false

platforms:
- name: macos-10.12
Expand Down
31 changes: 30 additions & 1 deletion libraries/helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,34 @@ class HomebrewUserWrapper
module Homebrew
extend self

require 'mixlib/shellout'
include Chef::Mixin::ShellOut

def self.included(base)
base.extend(Homebrew)
end

def install_path
arm64_test = shell_out('sysctl -n hw.optional.arm64')

This comment has been minimized.

Copy link
@tas50

tas50 Dec 21, 2021

Contributor

This could be done with an ohai attribute to avoid another shellout every time the resource runs

if arm64_test.stdout.chomp == '1'
'/opt/homebrew'
else
'/usr/local'
end
end

def repository_path
arm64_test = shell_out('sysctl -n hw.optional.arm64')
if arm64_test.stdout.chomp == '1'
'/opt/homebrew'
else
'/usr/local/Homebrew'
end
end

def exist?
Chef::Log.debug('Checking to see if the homebrew binary exists')
::File.exist?('/usr/local/bin/brew')
::File.exist?("#{HomebrewWrapper.new.install_path}/bin/brew")
end

def owner
Expand Down Expand Up @@ -68,3 +93,7 @@ def current_user
ENV['USER']
end
end unless defined?(Homebrew)

class HomebrewWrapper
include Homebrew
end
6 changes: 3 additions & 3 deletions recipes/default.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@
execute 'set analytics' do
environment lazy { { 'HOME' => ::Dir.home(Homebrew.owner), 'USER' => Homebrew.owner } }
user Homebrew.owner
command "/usr/local/bin/brew analytics #{node['homebrew']['enable-analytics'] ? 'on' : 'off'}"
only_if { shell_out('/usr/local/bin/brew analytics state', user: Homebrew.owner).stdout.include?('enabled') != node['homebrew']['enable-analytics'] }
command lazy { "#{HomebrewWrapper.new.install_path}/bin/brew analytics #{node['homebrew']['enable-analytics'] ? 'on' : 'off'}" }
only_if { shell_out("#{HomebrewWrapper.new.install_path}/bin/brew analytics state", user: Homebrew.owner).stdout.include?('enabled') != node['homebrew']['enable-analytics'] }
end

if node['homebrew']['auto-update']
Expand All @@ -51,6 +51,6 @@
execute 'update homebrew from github' do
environment lazy { { 'HOME' => ::Dir.home(Homebrew.owner), 'USER' => Homebrew.owner } }
user Homebrew.owner
command '/usr/local/bin/brew update || true'
command lazy { "#{HomebrewWrapper.new.install_path}/bin/brew update || true" }
end
end
1 change: 0 additions & 1 deletion recipes/install_taps.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
raise unless tap.key?('tap')
homebrew_tap tap['tap'] do
url tap['url'] if tap.key?('url')
full tap['full'] if tap.key?('full')
end
else
raise
Expand Down
2 changes: 1 addition & 1 deletion resources/cask.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
property :cask_name, String, regex: %r{^[\w/-]+$}, name_property: true
property :options, String
property :install_cask, [true, false], default: true
property :homebrew_path, String, default: '/usr/local/bin/brew'
property :homebrew_path, String, default: lazy { "#{HomebrewWrapper.new.install_path}/bin/brew" }
property :owner, String, default: lazy { Homebrew.owner } # lazy to prevent breaking compilation on non-macOS platforms

action :install do
Expand Down
4 changes: 2 additions & 2 deletions resources/tap.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
property :tap_name, String, name_property: true, regex: %r{^[\w-]+(?:\/[\w-]+)+$}
property :url, String
property :full, [true, false], default: false
property :homebrew_path, String, default: '/usr/local/bin/brew'
property :homebrew_path, String, default: lazy { "#{HomebrewWrapper.new.install_path}/bin/brew" }
property :owner, String, default: lazy { Homebrew.owner } # lazy to prevent breaking compilation on non-macOS platforms

action :tap do
Expand All @@ -51,5 +51,5 @@

def tapped?(name)
tap_dir = name.gsub('/', '/homebrew-')
::File.directory?("/usr/local/Homebrew/Library/Taps/#{tap_dir}")
::File.directory?("#{HomebrewWrapper.new.repository_path}/Library/Taps/#{tap_dir}")
end
3 changes: 2 additions & 1 deletion spec/recipes/default_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
before do
stubs_for_resource('execute[set analytics]') do |resource|
allow(resource).to receive_shell_out('/usr/local/bin/brew analytics state', user: 'vagrant')
allow(resource).to receive_shell_out('/opt/homebrew/bin/brew analytics state', user: 'vagrant')
end

allow(Homebrew).to receive(:exist?).and_return(true)
Expand Down Expand Up @@ -33,7 +34,7 @@
end
end

context '/usr/local/bin/brew exists' do
context 'brew script exists' do
cached(:chef_run) do
ChefSpec::SoloRunner.new.converge(described_recipe)
end
Expand Down
60 changes: 60 additions & 0 deletions spec/unit/libraries/homebrew_helpers_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# frozen_string_literal: true

require 'spec_helper'
require 'chef/mixin/shell_out'

describe Homebrew do
let(:opt_homebrew_path) { '/opt/homebrew' }
let(:usr_local_homebrew_path) { '/usr/local' }
let(:usr_local_repository_path) { '/usr/local/Homebrew' }

let(:shellout) do
double(command: 'sysctl -n hw.optional.arm64', run_command: nil, error!: nil, stdout: arm64_test_output,
stderr: stderr, exitstatus: exitstatus, live_stream: nil)
end

before(:each) do
allow(Mixlib::ShellOut).to receive(:new).and_return(shellout)
allow(shellout).to receive(:live_stream=).and_return(nil)
end

context 'when on Apple arm64 Silicon' do
let(:arm64_test_output) { "1\n" }
let(:exitstatus) { 0 }
let(:stderr) { double(empty?: true) }

describe '#install_path' do
let(:dummy_class) { Class.new { include Homebrew } }
it 'returns /opt/homebrew path' do
expect(dummy_class.new.install_path).to eq opt_homebrew_path
end
end

describe '#repository_path' do
let(:dummy_class) { Class.new { include Homebrew } }
it 'returns /opt/homebrew path' do
expect(dummy_class.new.repository_path).to eq opt_homebrew_path
end
end
end

context 'when on Apple Intel Silicon' do
let(:arm64_test_output) { '' }
let(:exitstatus) { 1 }
let(:stderr) { 'sysctl: unknown oid \'hw.optional.arm64\'' }

describe '#install_path' do
let(:dummy_class) { Class.new { include Homebrew } }
it 'returns /usr/local path' do
expect(dummy_class.new.install_path).to eq usr_local_homebrew_path
end
end

describe '#install_path' do
let(:dummy_class) { Class.new { include Homebrew } }
it 'returns /usr/local/Homebrew path' do
expect(dummy_class.new.repository_path).to eq usr_local_repository_path
end
end
end
end
44 changes: 40 additions & 4 deletions test/integration/default/default_spec.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,45 @@
describe command('brew info redis --json=v1 | jq ".[].installed[0].installed_on_request"') do
its('stdout') { should match('true') }
# Check both x86_64 and arm64 locations for brew
# Depending on shell setup in the box:
# InSpec commands want a full path, but this depends on arch + platform_version
# macOS default shell may be /bin/bash or /bin/zsh ... back to basics: /bin/sh
describe command('/bin/sh -c "PATH=/opt/homebrew/bin:/usr/local/bin:$PATH brew --version"') do
its('stdout') { should match('^Homebrew.*$') }
end

describe file('/usr/local/Caskroom/caffeine') do
brew_prefix = command('/bin/sh -c "PATH=/opt/homebrew/bin:/usr/local/bin:$PATH brew --prefix"').stdout.chomp
describe brew_prefix do
it { should match('^(\/usr\/local|\/opt\/homebrew)$') }
end

describe file(brew_prefix) do
it { should be_directory }
end

brew_caskroom = command('/bin/sh -c "PATH=/opt/homebrew/bin:/usr/local/bin:$PATH brew --caskroom"').stdout.chomp
describe brew_caskroom do
it { should match('^(\/usr\/local|\/opt\/homebrew)/Caskroom$') }
end

describe file(brew_caskroom) do
it { should be_directory }
end

describe package('redis') do
it { should be_installed }
end

describe package('jq') do
it { should be_installed }
end

# CI agnostic caskroom owner check
# InSpec may be running commands w/sudo
ci_user = command('whoami').stdout.chomp
if ci_user == 'root'
ci_user = command("/bin/sh -c 'echo $SUDO_USER'").stdout.chomp
end
describe file("#{brew_caskroom}/caffeine") do
it { should be_directory }
its('mode') { should cmp '0755' }
it { should be_owned_by 'runner' }
it { should be_owned_by ci_user }
end

0 comments on commit 2980305

Please sign in to comment.