MacOS 实践之 Homebrew

前言

此篇博文是笔者所总结的 Macos 系列之一;

本文为作者的原创作品,转载需注明出处;

安装

High Sierra 之前的版本

  1. 将 /usr/local 的 ownership 设置为当前用户

    1
    $ sudo chown -R `whoami` /usr/local

    `whoami`输出的既是当前的用户名;设置的原因是,brew 的安装文件存放在 /user/local 目录下的,所以需要该目录的权限;

  2. install Homebrew
    1
    $ ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

High Sierra 之后的版本

在 High Sierra 中不允许更爱 /usr/local 的所有权利了,所以上述 #1 失效;不过好消息是,Homebrew 的安装脚本也更新了,只需要执行下面这一步操作,

1
$ ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

输出,

1
2
3
4
5
6
7
8
9
10
11
......
Press RETURN to continue or any other key to abort
==> /usr/bin/sudo /bin/mkdir -p /usr/local/bin /usr/local/etc /usr/local/include /usr/local/lib /usr/local/sbin /usr/local/share /usr/local/var /usr/local/opt /usr/local/share/zsh /usr/local/share/zsh/site-functions /usr/local/var/homebrew /usr/local/var/homebrew/linked /usr/local/Cellar /usr/local/Caskroom /usr/local/Homebrew /usr/local/Frameworks
Password:
==> /usr/bin/sudo /bin/chmod g+rwx /usr/local/bin /usr/local/etc /usr/local/include /usr/local/lib /usr/local/sbin /usr/local/share /usr/local/var /usr/local/opt /usr/local/share/zsh /usr/local/share/zsh/site-functions /usr/local/var/homebrew /usr/local/var/homebrew/linked /usr/local/Cellar /usr/local/Caskroom /usr/local/Homebrew /usr/local/Frameworks
==> /usr/bin/sudo /bin/chmod 755 /usr/local/share/zsh /usr/local/share/zsh/site-functions
==> /usr/bin/sudo /usr/sbin/chown shangyang /usr/local/bin /usr/local/etc /usr/local/include /usr/local/lib /usr/local/sbin /usr/local/share /usr/local/var /usr/local/opt /usr/local/share/zsh /usr/local/share/zsh/site-functions /usr/local/var/homebrew /usr/local/var/homebrew/linked /usr/local/Cellar /usr/local/Caskroom /usr/local/Homebrew /usr/local/Frameworks
==> /usr/bin/sudo /usr/bin/chgrp admin /usr/local/bin /usr/local/etc /usr/local/include /usr/local/lib /usr/local/sbin /usr/local/share /usr/local/var /usr/local/opt /usr/local/share/zsh /usr/local/share/zsh/site-functions /usr/local/var/homebrew /usr/local/var/homebrew/linked /usr/local/Cellar /usr/local/Caskroom /usr/local/Homebrew /usr/local/Frameworks
==> /usr/bin/sudo /bin/mkdir -p /Users/shangyang/Library/Caches/Homebrew
==> /usr/bin/sudo /bin/chmod g+rwx /Users/shangyang/Library/Caches/Homebrew
......

可以看到,安装过程中,会自动的给相关的文件夹赋予权限;

常用命令

brew isntall

比如,我要安装 php,直接,

1
brew install php

但是这样安装的弊端是,永远都是安装最新版本的;如果要安装指定版本,参考 brew search 章节;并且,相关安装后的信息,比如包路径通常是放在 /usr/local/Cellar 中的,可执行文件通常是放在 /usr/local/opt/<name>/bin 中的,这部分的详细介绍内容,同样参考 brew search 中的内容;

brew list

1
2
3
4
5
$ brew list
activemq fontconfig gobject-introspection jpeg libyaml openssl proxychains-ng tesseract
ant freetds gradle lame libzip openssl@1.1 python texi2html
apr freetype graphite2 leptonica little-cms2 p7zip qt tree
apr-util fribidi

它会显示你本地通过 brew 已安装的所有应用;

brew reinstall

brew reinstall 通常是用来修复某个已损坏的安装包的,注意,它的修复方式是用最新版本来覆盖更新;以我本地的 php 为例,

  1. 本地的 php 7.2.3 报错,

    1
    2
    3
    4
    5
    $ php -v
    dyld: Library not loaded: /usr/local/opt/icu4c/lib/libicui18n.60.dylib
    Referenced from: /usr/local/bin/php
    Reason: image not found
    Abort trap: 6
  2. 这个时候,可以尝试通过 reinstall 的方式进行修复,

    1
    $ brew reinstall php
  3. 检查,
    版本号,

    1
    2
    3
    4
    5
    $ php -v
    PHP 7.2.5 (cli) (built: Apr 26 2018 12:07:32) ( NTS )
    Copyright (c) 1997-2018 The PHP Group
    Zend Engine v3.2.0, Copyright (c) 1998-2018 Zend Technologies
    with Zend OPcache v7.2.5, Copyright (c) 1999-2018, by Zend Technologies

    info,可见,当前被激活的是 7.2.5,并且老版本 7.2.3 已经消失了;

    1
    2
    3
    4
    5
    6
    7
    8
    $ brew info php
    php: stable 7.2.5 (bottled)
    General-purpose scripting language
    https://secure.php.net/
    /usr/local/Cellar/php/7.2.5 (515 files, 79.1MB) *
    Poured from bottle on 2018-05-10 at 09:12:07
    From: https://github.com/Homebrew/homebrew-core/blob/master/Formula/php.rb
    ==> Dependencies

    查看本地安装包路径的内容,

    1
    2
    $ ls /usr/local/Cellar/php
    7.2.5

    可以看到,已经完全覆盖了了 7.2.3 包;所以,如果要保持原有版本不变,那么需要指定安装包的版本;但是依然要注意的是,使用这个命令以后,其它不同版本的安装包会被自动删除;

brew uninstall

1
brew uninstall jenkins

等价于执行 brew remove

brew info - 检查已安装包信息

检测本地是否安装过该软件,并且提示当前的最新稳定版本的信息;

  • 如果某个软件已经安装,那么会显示已安装的信息
    下面,以检查 postgresql 为例,

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    $ brew info postgresql
    postgresql: stable 9.3.2 (bottled)
    http://www.postgresql.org/
    Conflicts with: postgres-xc
    /usr/local/Cellar/postgresql/9.1.5 (2755 files, 37M)
    Built from source
    /usr/local/Cellar/postgresql/9.3.2 (2924 files, 39M) *
    Poured from bottle
    From: https://github.com/Homebrew/homebrew/commits/master/Library/Formula/postgresql.rb
    # … and some more

    可以看到,它会显示本地已安装包的信息,包括安装的路径等等;

  • 如果某个软件未安装,那么会显示该软件的最新安装包信息,并且显示 Not installed
    下面,以检查 mesos 的安装包的信息为例进行演示,

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    $ brew info mesos
    mesos: stable 1.3.0 (bottled)
    Apache cluster manager
    https://mesos.apache.org
    Not installed
    From: https://github.com/Homebrew/homebrew-core/blob/master/Formula/mesos.rb
    ==> Dependencies
    Build: apr-util ✘, maven ✘
    Required: subversion ✘
    ==> Requirements
    Required: java >= 1.7 ✔, macOS >= 10.8 ✔

    上面会显示Not installed以及相关的其它依赖的信息;

brew search - 安装指定版本软件

当我们需要安装某个特定版本的安装包的时候,该如何操作呢?下面以安装 mongodb 特定版本 3.4.6 为例进行描述,

  1. 首先,通过 brew search 检索可用的安装包信息

    1
    2
    3
    4
    5
    6
    7
    $ brew search mongodb
    ==> Formulae
    mongodb mongodb@3.0 mongodb@3.2 mongodb@3.4 mongodb@3.6 percona-server-mongodb

    ==> Casks
    homebrew/cask-versions/mongodb-compass-beta homebrew/cask/mongodb-compass homebrew/cask/mongodb-compass-isolated-edition homebrew/cask/nosqlbooster-for-mongodb
    homebrew/cask/mongodb homebrew/cask/mongodb-compass-community homebrew/cask/mongodb-compass-readonly homebrew/cask/orelord-mongodb

    可见,当前总共有 4 个版本可以安装;

  2. 然后,安装指定版本即可

    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
    $ brew install mongodb@3.4
    Updating Homebrew...
    ==> Installing dependencies for mongodb@3.4: openssl
    ==> Installing mongodb@3.4 dependency: openssl
    ==> Downloading https://homebrew.bintray.com/bottles/openssl-1.0.2p.high_sierra.bottle.tar.gz
    ######################################################################## 100.0%
    ==> Pouring openssl-1.0.2p.high_sierra.bottle.tar.gz
    ......
    ==> Summary
    🍺 /usr/local/Cellar/mongodb@3.4/3.4.17: 19 files, 285.6MB
    ==> Caveats
    ==> openssl
    A CA file has been bootstrapped using certificates from the SystemRoots
    keychain. To add additional certificates (e.g. the certificates added in
    the System keychain), place .pem files in
    /usr/local/etc/openssl/certs

    and run
    /usr/local/opt/openssl/bin/c_rehash

    openssl is keg-only, which means it was not symlinked into /usr/local,
    because Apple has deprecated use of OpenSSL in favor of its own TLS and crypto libraries.

    If you need to have openssl first in your PATH run:
    echo 'export PATH="/usr/local/opt/openssl/bin:$PATH"' >> ~/.bash_profile

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

    ==> mongodb@3.4
    mongodb@3.4 is keg-only, which means it was not symlinked into /usr/local,
    because this is an alternate version of another formula.

    If you need to have mongodb@3.4 first in your PATH run:
    echo 'export PATH="/usr/local/opt/mongodb@3.4/bin:$PATH"' >> ~/.bash_profile


    To have launchd start mongodb@3.4 now and restart at login:
    brew services start mongodb@3.4
    Or, if you don't want/need a background service you can just run:
    mongod --config /usr/local/etc/mongod.conf

    并且安装信息非常的友好,可见,不但安装了 mongodb 还顺带的安装了其依赖包 openssl,

    • openssl
      建议通过如下命令将 openssl 的信息加入环境变量,

      1
      echo 'export PATH="/usr/local/opt/openssl/bin:$PATH"' >> ~/.bash_profile
    • mongodb@3.4
      首先,可见安装路径/usr/local/Cellar/mongodb@3.4/3.4.17
      其次,可执行文件是放置在 /usr/local/opt/mongodb@3.4/bin 中的
      再次,mongodb 的配置文件路径 /usr/local/etc/mongod.conf
      最后,mongodb 提示了两种启动方式,

      • 通过 brew services start
      • 将 mongodb bin/ 导入你的环境变量中;

brew switch

如果本地安装了多个版本,告诉 brew 我需要切换至哪个版本,比如,

1
2
3
4
5
6
7
8
9
10
$ brew info postgresql
postgresql: stable 9.3.2 (bottled)
http://www.postgresql.org/
Conflicts with: postgres-xc
/usr/local/Cellar/postgresql/9.1.5 (2755 files, 37M)
Built from source
/usr/local/Cellar/postgresql/9.3.2 (2924 files, 39M) *
Poured from bottle
From: https://github.com/Homebrew/homebrew/commits/master/Library/Formula/postgresql.rb
# … and some more

可以看到,本地安装了两个版本的 postgresql,9.1.5 和 9.3.2,那么如果,我们需要切换至 9.1.5 呢?执行如下命令即可,

1
2
3
4
$ brew switch postgresql 9.1.5
Cleaning /usr/local/Cellar/postgresql/9.1.5
Cleaning /usr/local/Cellar/postgresql/9.3.2
384 links created for /usr/local/Cellar/postgresql/9.1.5

检查,

1
2
3
4
5
6
7
8
9
10
$ brew info postgresql
postgresql: stable 9.3.2 (bottled)
http://www.postgresql.org/
Conflicts with: postgres-xc
/usr/local/Cellar/postgresql/9.1.5 (2755 files, 37M) *
Built from source
/usr/local/Cellar/postgresql/9.3.2 (2924 files, 39M)
Poured from bottle
From: https://github.com/Homebrew/homebrew/commits/master/Library/Formula/postgresql.rb
# … and some more

注意,后面的 * 号,表示哪个版本目前是被激活的;

brew update

1
$ brew update

注意,这是对 brew 自身进行 update;

brew upgrade

对某个已安装的应用程序进行更新,比如

1
$ brew upgrade php

表示对 php 安装包进行更新

brew cleanup

brew doctor

该命令是用来对 brew 进行健康体检的;

1
$ brew doctor

会检测出当前 Homebrew 存在的一些警告或者异常,下面罗列一些典型的警告,

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
Warning: You have unlinked kegs in your Cellar
Leaving kegs unlinked can lead to build-trouble and cause brews that depend on
those kegs to fail to run properly once built. Run `brew link` on these:
percona-server

Warning: Homebrew's sbin was not found in your PATH but you have installed
formulae that put executables in /usr/local/sbin.
Consider setting the PATH for example like so
echo 'export PATH="/usr/local/sbin:$PATH"' >> ~/.bash_profile

Warning: Your Xcode (8.1) is outdated.
Please update to Xcode 8.2 (or delete it).
Xcode can be updated from the App Store.
Warning: Your XQuartz (2.7.8) is outdated.
Please install XQuartz 2.7.9 (or delete the current version).
XQuartz can be updated using Homebrew-Cask by running
brew cask reinstall xquartz

Warning: Some frameworks can be picked up by CMake's build system and likely
cause the build to fail. To compile CMake, you may wish to move these
out of the way:
/Library/Frameworks/libcurl.framework

Warning: Unbrewed header files were found in /usr/local/include.
If you didn't put them there on purpose they could cause problems when
building Homebrew formulae, and may need to be deleted.

Unexpected header files:
/usr/local/include/node/android-ifaddrs.h
/usr/local/include/node/ares.h
/usr/local/include/node/ares_build.h
...

brew services

安装

1
brew tap homebrew/services

原理

原理,https://robots.thoughtbot.com/starting-and-stopping-background-services-with-homebrew

作用范围 - login session

首先,要切记,brew services 必须是在用户通过 GUI 登录以后,才能够执行(这种情况在 Macos 中的术语叫做 login session);否则如果在用户未 GUI 登录的情况下,通过 SSH 执行的话,会报如下的错误,Could not find domain for

1
2
3
$ brew services start jenkins
Could not find domain for
Error: Failure while executing; `/bin/launchctl enable gui/501/homebrew.mxcl.jenkins` exited with 112.

备注,

其实追根溯源的话,这个错误是由 launchctl 抛出来的,因为 brew services 正是通过封装 launchctl 来执行的,如果使用 launchctl 在用户未 GUI 登录的情况下,通过 ssh 执行会包同样的错误;

1
2
$ launchctl bootstrap gui/501 ~/Library/LaunchAgents/homebrew.mxcl.jenkins.plist
Could not find domain for

封装 launchctl

brew services 其实背后执行的是 launchctl,只是 brew services 将其底层的复杂性给封装了;那它是怎么封装了哪些步骤呢?下面以 jenkins 为例,

  1. 首先,将 jenkins 服务的描述文件 plist 拷贝到当前用户的 LaunchAgents 目录中,

    1
    2
    To have launchd start mysql at login:
    ln -sfv /usr/local/opt/jenkins/*.plist ~/Library/LaunchAgents
  2. 然后,通过 launchctl 加载 plist 描述文件并执行,两种执行的方式
    方式一、

    1
    launchctl load ~/Library/LaunchAgents/homebrew.mxcl.jenkins.plist

    方式二、

    1
    launchctl bootstrap gui/501 ~/Library/LaunchAgents/homebrew.mxcl.jenkins.plist

可以看到,实际上 brew services 就是封装了底层的 launchctl;

plist

从上述分析可知,实际上,brew services 通过解析 plist 中的相关配置来启动的 jenkins,下面来看一下 jenkins.plist 的相关配置,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<plist version="1.0">
<dict>
<key>Label</key>
<string>homebrew.mxcl.jenkins</string>
<key>ProgramArguments</key>
<array>
<string>/usr/libexec/java_home</string>
<string>-v</string>
<string>1.8</string>
<string>--exec</string>
<string>java</string>
<string>-Dmail.smtp.starttls.enable=true</string>
<string>-jar</string>
<string>/usr/local/opt/jenkins/libexec/jenkins.war</string>
<string>--httpListenAddress=127.0.0.1</string>
<string>--httpPort=8080</string>
</array>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>

可以看到,它执行的命令,是通过 java 1.8 执行的 java -jar 命令启动位于 /usr/local/opt/jenkins/libexec/ 中的 jenkins.war 启动的,其执行的时候,有两个附带的参数,既 ip 地址 127.0.0.1 和端口 8080;

brew services list

可以通过 brew list 查看相关的服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ brew services list
Name Status User Plist
activemq stopped
boot2docker stopped
chromedriver stopped
httpd stopped
jenkins stopped
mongodb stopped
nexus stopped
nginx stopped
percona-server stopped
php stopped
redis started mac /Users/mac/Library/LaunchAgents/homebrew.mxcl.redis.plist
zookeeper stopped

Wow,这才发现,原来我的 redis 是启动了的;难道是登陆后自启动的?

注意事项

launchctl 要求,必须是 GUI logged in 的用户才能使用,也就是必须通过界面登录以后的用户才能使用;否则,如果在用户没有 GUI 登录的情况下,执行如下命令,

1
2
3
$ brew services start jenkins
Could not find domain for
Error: Failure while executing; `/bin/launchctl enable gui/501/homebrew.mxcl.jenkins` exited with 112.

将会得到 Could not find domain for 的错误;

这就使得,如果你是想把 Mac 作为纯服务器来使用的话,brew services 就不是你的最佳选择了,因为很有可能,你的账户根本没有办法 GUI log in;

相关的声明周期查看 https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/Introduction.html 文章中提到了 Startup Session 和 Login Sessinon;

使用案例

升级当前 maven 为最新版本

  1. 升级 Homebrew 自己使用update关键字

    1
    $ brew update
  2. 升级第三方包,使用upgrade关键字

    1
    $ brew upgrade maven

    输出日志如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    Updating Homebrew...
    ==> Auto-updated Homebrew!
    Updated 1 tap (homebrew/core).
    No changes to formulae.

    ==> Upgrading 1 outdated package, with result:
    maven 3.3.9
    ==> Upgrading maven
    ==> Using the sandbox
    ==> Downloading https://www.apache.org/dyn/closer.cgi?path=maven/maven-3/3.3.9/binaries/apache-maven-3.3.9-bin.tar.gz
    ==> Best Mirror http://www-us.apache.org/dist/maven/maven-3/3.3.9/binaries/apache-maven-3.3.9-bin.tar.gz
    ######################################################################## 100.0%
    🍺 /usr/local/Cellar/maven/3.3.9: 95 files, 9.6M, built in 30 seconds

为系统环境创建软链接

通常,通过 homebrew 安装的应用并不能直接当做系统命令进行使用,我们往往需要在系统环境 /usr/bin 中为其创建软链接才行;

正好在研究 openssl,那么这里就以 openssl 为例来讲解

https://medium.com/@katopz/how-to-upgrade-openssl-8d005554401

ln 命令

1
2
$ sudo ln -s /usr/local/Cellar/openssl/1.0.2a-1/bin/openssl /usr/bin/openssl
ln: /usr/bin/openssl: Operation not permitted

报错,Operation not permitted

切换到 root

1
$ sudo -i

1
2
$ sudo ln -s /usr/local/Cellar/openssl/1.0.2a-1/bin/openssl /usr/bin/openssl
ln: /usr/bin/openssl: Operation not permitted

同样报错,Operation not permitted

再次执行

一般而言,通过brew link --force xxx就可以将通过 brew 所安装的软件包直接连接到/usr/bin/目录中;但是这里记录下openssl的情况,

1
2
3
4
5
6
$ brew link --force openssl
Warning: Refusing to link: openssl
Linking keg-only openssl means you may end up linking against the insecure,
deprecated system OpenSSL while using the headers from Homebrew\'s openssl.
Instead, pass the full include/library paths to your compiler e.g.:
-I/usr/local/opt/openssl/include -L/usr/local/opt/openssl/lib

可见,这里提出了一个 warning,如果更改系统的 openssl 的指向将导致系统安全风险,所以,这里不推荐这么做;google 了一下原因,

http://stackoverflow.com/questions/38670295/homebrew-refusing-to-link-openssl
brew 团队:https://github.com/Homebrew/brew/pull/597

brew 开发团队的解释,重要的部分摘要如下,

Our goals:

  • MacOS users can use .NET Core
  • Security updates to OpenSSL can be delivered to users independent of our release schedule
  • While documentation may assume a single dependency source (e.g. homebrew), have no hard dependence on that dependency source
  • Have as simple of getting started instructions as possible given preceding goals

可以通过 .NET Core 第三方独立的 openssl 环境来进行安全升级,

于是,按照 http://stackoverflow.com/questions/38670295/homebrew-refusing-to-link-openssl 中的步骤进行修改,

如果是安装的 1.0.0 NET Core,执行

1
$ sudo install_name_tool -add_rpath /usr/local/opt/openssl/lib /usr/local/share/dotnet/shared/Microsoft.NETCore.App/1.0.0/System.Security.Cryptography.Native.dylib

如果安装的是 1.0.1 NET Core,执行

1
$ sudo install_name_tool -add_rpath /usr/local/opt/openssl/lib /usr/local/share/dotnet/shared/Microsoft.NETCore.App/1.0.1/System.Security.Cryptography.Native.dylib

不过我安装的是 1.0.5,所以执行的是

1
$ sudo install_name_tool -add_rpath /usr/local/opt/openssl/lib /usr/local/share/dotnet/shared/Microsoft.NETCore.App/1.0.5/System.Security.Cryptography.Native.dylib

总之,都试过了,总之上述的解决方案对最新版本的 OSX 均不支持,看看 brew 开发团队的一句话彻底将我的希望浇灭了;

@drem-darios We don’t support brew link –force openssl any more for the reasons described when you run the command.

我们不再支持通过brew link --force openssl的方式,原因是,openssl是与核心安全相关的,决不允许擅自更改,否则会造成全局的危险… 所以,没辙了,最后只能通过

1
$ /usr/local/Cellar/openssl/1.0.2k/bin/openssl version

来操作啦…

Reference

非常好的 user guide: http://friendly-101.readthedocs.io/en/latest/homebrew.html
官方 doc 以及 troubleshooting https://github.com/Homebrew/brew/blob/master/docs/Troubleshooting.md