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 usedbrew 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 thesphinx-build
executable unfindable during the installation.3 Homebrew does have an affordance, however, for turning off the$PATH
restrictions. You can addenv :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.
-
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.↩
-
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 ispip install --user
.↩ -
It would still be findable if Sphinx had been installed as a system-level module. Notice a theme?↩