Strongly Emergent

What comes from combining humans, computers, and narrative

Debugging Exercise: Homebrew, Notmuch, and the Missing Manpages

Recently I’ve been tinkering with the email setup on my MBP. When I installed notmuch, I encountered a bug. Notmuch is a project that sports a majestic Unix beard, so naturally among the forms of documentation they provide are manpages. A quick brew install notmuch gave me a working notmuch, but no manpages. Figuring out why the manpages didn’t install was mildly tricky, so I’m writing it down here in case anyone else (possibly Future Me) has the same problem.1

When you’re looking at a problem with command-line tools, switching them to verbose mode is always a good place to start. Homebrew normally suppresses the output of installer programs, but its --verbose flag makes that output visible. The average Makefile can produce a lot of output, though, so I used grep to see if there was any low-hanging fruit. There was:

brew install --verbose notmuch | egrep -i 'man.?page'
# => Checking if sphinx is available and supports nroff output... No (so will not install man pages).

I was puzzled: Sphinx is a widely-used tool, my system does have it installed, and it does support nroff output.

This is the point where the problem went from “am I doing the right thing?” to “why did the right thing fail to happen?” When problems come up, be sure to look at the possibility that the failure is your fault. We’ve all made errors, and humility is an important life skill. The Sphinx error told me there was probably a bug in the code involved, rather than in my understanding of them. All of the code involved is freely available (thank you RMS) so I downloaded it and took a look:

git clone git://notmuchmail.org/git/notmuch
cd notmuch
git grep -l 'supports nroff output'
# => configure

Looking for the error message led me to the configure script. It’s part of a fairly complex Makefile infrastructure, but the two-part test it uses to search for Sphinx is easy to reproduce: if command -v sphinx-build > /dev/null && ${python} -m sphinx.writers.manpage > /dev/null 2>&1 ; The first half reproduces with a quick copy and paste: command -v foo is similar to which foo, but in addition to asking whether there’s an executable file foo in $PATH (as which does), it also looks at builtins, shell functions, and aliases. To reproduce the second half, I need the value of ${python}, which an earlier part of the script defines by looking for a Python interpreter under various names. Usually the value will be just python, so I used that.

command -v sphinx-build > /dev/null
echo "$?"
# => 0
python -m sphinx.writers.manpage > /dev/null 2>&1
echo "$?"
# => 0

Running the configure script’s test confirms that yes, I have a Sphinx install that’s capable of generating manpages. The next question is, why is that Sphinx install not visible when the configure script is running during installation? Answering that question is what the site module is best at. It’s imported by default when you run Python, and it’s responsible for “adding all the standard site-specific directories to the module search path,” which in turn is a critical part of what makes the import statement work.

I used find $(brew --cache) -iname 'notmuch*' and brew formula notmuch to find the install source and the install script, then started editing. First, I commented out the sha256 "deadbeef0000" lines in the install script. Homebrew checks the SHA256 hash of sources during a normal install, which is a good and correct security feature that needs to be turned off for this. Then I edited the notmuch configure script in the install source, added a ${python} -m site invocation, saved it, and ran the installation again.

sys.path = [
    '/private/tmp/notmuch-20171027-17288-bpiisc/notmuch-0.25.1',
    '/usr/local/Cellar/python3/3.6.3/Frameworks/Python.framework/Versions/3.6/lib/python36.zip',
    '/usr/local/Cellar/python3/3.6.3/Frameworks/Python.framework/Versions/3.6/lib/python3.6',
    '/usr/local/Cellar/python3/3.6.3/Frameworks/Python.framework/Versions/3.6/lib/python3.6/lib-dynload',
    '/usr/local/lib/python3.6/site-packages',
    '/usr/local/Cellar/python3/3.6.3/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages',
]
USER_BASE: '/private/tmp/notmuch-20171027-17288-bpiisc/notmuch-0.25.1/.brew_home/Library/Python/3.6' (doesn't exist)
USER_SITE: '/private/tmp/notmuch-20171027-17288-bpiisc/notmuch-0.25.1/.brew_home/Library/Python/3.6/lib/python/site-packages' (doesn't exist)

Success! Comparing this to the same invocation run from my terminal immediately points out a problem, further highlighted by site helpfully adding a little “(doesn’t exist)” note.

sys.path = [
    '~/projects/notmuch',
    '/usr/local/Cellar/python3/3.6.3/Frameworks/Python.framework/Versions/3.6/lib/python36.zip',
    '/usr/local/Cellar/python3/3.6.3/Frameworks/Python.framework/Versions/3.6/lib/python3.6',
    '/usr/local/Cellar/python3/3.6.3/Frameworks/Python.framework/Versions/3.6/lib/python3.6/lib-dynload',
    '~/Library/Python/3.6/lib/python/site-packages',
    '/usr/local/lib/python3.6/site-packages',
    '/usr/local/Cellar/python3/3.6.3/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages',
]
USER_BASE: '~/Library/Python/3.6' (exists)
USER_SITE: '~/Library/Python/3.6/lib/python/site-packages' (exists)
ENABLE_USER_SITE: True

To confirm that this mismatch is causing a problem, I asked the system where my Sphinx install is.

which -a sphinx-build
# => ~/Library/Python/3.6/bin/sphinx-build
pip3 show sphinx | grep -i 'location'
# => Location: ~/Library/Python/3.6/lib/python/site-packages

This is progress: I have a narrow answer to the “why did the right thing fail to happen?” question. The ~/Library/Python/3.6/lib/python/site-packages path for Sphinx tells me that I installed it via pip install --user sphinx.2 The /private/tmp entries in the module search path tell me that during installation, the configure script is sandboxed in a temporary directory and using that directory as $HOME. When invoked from the configure script, Python can only find packages that were installed system-wide, and Sphinx isn’t one of them. I took a quick trip into Homebrew’s source code to look for sandboxing. Because it’s written in Ruby, which makes it very easy to access environment variables like $HOME, it’s very easy to search for idiomatic use of environment variables. A quick cd $(brew --repository) && git grep -E 'ENV\W{2}HOME\W{2}' turned up an old_home = ENV["HOME"] assignment in the stage() function, which does indeed assign a new $HOME during installs.

As gratifying as it is to figure out why something failed, there’s still work to do. There are two main tradeoffs to make after characterizing a problem: specific versus general and workaround versus solution. Among other costs, things closer to the “general” and “solution” poles tend to require more control of the underlying elements and things closer to the “specific” and “workaround” poles tend to not be helpful to other people. With that in mind, here are some ways to address the problem I started with.

  • Roll my own: I already established that in my regular environment, the configure script can find Sphinx just fine, and brew install prints out the invocation it uses. It’s only a few steps more to compile the manpages myself:
    cd ~/projects/notmuch
    PYTHON=$(which python3) ./configure \
        --prefix=/usr/local/Cellar/notmuch/0.25.1 --with-emacs \
        --emacslispdir=/usr/local/Cellar/notmuch/0.25.1/share/emacs/site-lisp/notmuch \
        --emacsetcdir=/usr/local/Cellar/notmuch/0.25.1/share/emacs/site-lisp/notmuch
    make V=1 install-man
    brew unlink notmuch
    brew link notmuch
    This is pretty much all the way out the “specific” and “workaround” axes: it isn’t very reproducible and it doesn’t do anything about the underlying issue.
  • Interactive install: This is a tiny step further towards being reproducible: Homebrew’s sandboxing of installs is turned off if you pass the --interactive flag, so if I used brew install --interactive notmuch, I could run the same installation commands in my normal shell. This still requires doing work by hand, though, so it’s not very appealing.
  • Do it live: I could install Sphinx as a system-scope package, rather than as a user-scope package. This is a solution that doesn’t require doing things by hand and which might be helpful to others. Unfortunately, it requires messing with system-level packages, which is not something I want to do or recommend that others do.
  • Save the environment: In addition to importing site, Python uses the $PYTHONPATH environment variable to find modules. If I added $HOME/Library/Python/3.6/lib/python/site-packages, subsequent Python invocations should be able to find packages installed to that directory. I’d like to avoid setting $PYTHONPATH if I can; it’s prone to causing problems. For example, if you have both Python 2 and Python 3 installed, as many developers do, setting $PYTHONPATH will cause both versions of Python to look at the given path for modules. That’s good when you’re actively trying to develop against both versions of Python, but bad when you’re trying to repair the site-packages path.
  • Eat the $PATH: Another problem with changing $PYTHONPATH is that doing so only makes the Sphinx test halfway pass. As part of its sandboxing, Homebrew also drastically restricts $PATH, leaving the sphinx-build executable unfindable during the installation.3 Homebrew does have an affordance, however, for turning off the $PATH restrictions. You can add env :userpaths to the formula or pass --env=std on the command line. Combining these two approaches gets us to something that approaches being a good workaround:
    export PYTHON=python3.6 && export PYTHONPATH=$($PYTHON -msite --user-site)
    brew install --env=std notmuch
    export PYTHON=‘’ && export PYTHONPATH=‘’
    This isn’t perfect, but it’s got good reproducibility, so it’s what I ended up doing.

At this point I’m not entirely sure whether Homebrew’s behavior here is a bug. I don’t like that it discourages people from installing packages as --user, and it already has the setup_home() function (clumsily) patching the module search path. Needing to perpetrate $PYTHONPATH shenanigans is a bad sign. The superenv approach does make installs much easier and more reproducible, so it’s a very good thing overall, but it could be improved.

What is clearly a bug, though, is an issue in notmuch that I stumbled on while digging through all this. You can set the $PYTHON environment variable to tell the installation where your preferred Python install is. The installer ignores this information when it goes to run Sphinx: it instead takes the first sphinx-build it finds on your $PATH. Similar to the problems with $PYTHONPATH, this can lead to problems when you have both Python 3 Sphinx and Python 2 Sphinx installed. The workaround for this is to use command -v sphinx-build to check which version is first on your $PATH and to use that version. This won’t work indefinitely, but it should work for as long as notmuch can be built with both Python 2 and Python 3.


  1. Per the genre conventions of debugging posts, I’m eliding almost all of the dead ends and unproductive attempts from this and instead writing about how I would have solved the problem if I were staring out the window on a pleasant foggy morning with a tasty cup of coffee beside me and a good night’s sleep behind me.

  2. There have already been plenty of posts about this, so I’ll say this very quickly: you should almost never sudo pip install anything; the right way to install in almost all circumstances is pip install --user.

  3. It would still be findable if Sphinx had been installed as a system-level module. Notice a theme?