Installing PHP and other languages with ASDF on MacOS Catalina

So you are sitting there with your cup of tea, writing some amazing code, and then drop in to the terminal to run good old trusty php mysecretproject.php and POW some random library is now unlinked with the version you had installed. Homebrew has struck again. 

You sit with your hands on your head, remembering that NodeJS had wanted to update that library for “reasons”. 

Now you get the joy of building your new dev stack yet again. 

Or 

The project lands on your desk, and you run php veryoldproject.php and it just errors, turns out it just won’t run on that version of PHP installed on your system.

I have recently had both issues land, and figured it was time I did something about it. I pondered two different approaches, both had pros and cons

  • Docker Instance 
  • ASDF-VM (version manager not virtual machine)

The docker approach would be to have a small Docker instance that runs PHP within it, and then applications would use this instance of Docker. Need a different version, no problem it’s just a case of a separate instance pegged at the required version. It being a container, it’s completely isolated and we shouldn’t run into issues with Homebrew. The downside is that on MacOS Docker can at times be a bit slow and does come with an overhead. This shouldn’t be an issue. The second downside is that it’s just extra faff for those quick little projects; spending time on tooling would reduce this but it’s not a fire and forget solution.

ASDF might not be something you have heard of. It’s a program version manager similar in concept to, say, NVM for Node, only except it being for Node it’s for almost every language you can think of!

So you can manage everything from PHP to Python and indeed Node, using ASDF. It has a simple set of commands, for example to get the latest version of Python running you would do something like:

asdf plugin-add python asdf install python latest asdf global python latest

That’s it. The latest version of Python would be made available to you globally, but if you wanted to install another version, say 2.7 for an older project, you would install it with:

asdf install python 2.7

Then you can use that, because we haven’t set it globally it’s still defaulting to the latest version. In a project we can, within the working directory, run:

asdf local python 2.7

This will then write a .tool-versions file into your working directory and pin the version used within it. 

ASDF on the face of it certainly fixes issue 2 with the ability to run multiple versions, but should also fix issue 1 as each version is separately compiled. It also brings a much more simplified versioning and keeps things local with virtually no overhead meaning everything should be fast and just work.

So I opted for ASDF over Docker. I mean, what could be wrong?

Installing PHP with ASDF

I’m going to go through the entire process, the only assumed thing you have is that you are on MacOS Catalina (I suspect it’s the same for everything) and you have Homebrew installed (If you don’t, this will be a fun experience).

So step 1 install ASDF

If you haven’t already got it installed:

brew install asdf

You can follow the install instructions here [https://asdf-vm.com/#/core-manage-asdf] especially if you are not using ZSH/Homebrew. 

Once installed you need to add ASDF startup script to your bashrc/zshrc for me that meant:

echo -e "\n. $(brew --prefix asdf)/asdf.sh" >> ~/.zshrc

Reload zsh

. ~/.zshrc

And then you should have ASDF available and you can add PHP

asdf plugin-add php

Ok, let’s install something!
Tip – as of Nov 2020 if you install php latest rather than getting the latest stable PHP7.4 branch you get the latest stable PHP8 beta build. So you probably want to peg to 7.4

asdf install php 7.4

And…

It falls down

Ok, there is a reasonable chance you are now in the fun world of dependencies. You see, to compile PHP a lot of things have to be there first, which, depending on your setup, are going to be missing. When we install things using Homebrew or another package manager they do nice things like installing dependencies. 

If you have been using Homebrew previously with PHP, good news you probably have most of these dependencies, but on a vanilla install you won’t, so I’m going to split this into 2 sections, what’s needed to build PHP and what it depends on.

brew install bison gawk re2c pkg-config

This will get you to a point where you can install some of the dependencies. For me, on a vanilla install, this was:

Brew install openssl gnupg gmp unixodbc libpng icu4c onigurma libpq libiconv libzip

Ok, so you go to run: 

asdf install php 7.4

And it failed, complaining that pkg-config doesn’t know the path to many of these things you just installed. For example OpenSSL was installed as “keg only” this means it’s running alongside MacOS existing openssl implementation. 

Right so how do we fix this?

First off you can find a list of things pkg-config does know about by using:

pkg-config --list-all

You can get a list of things that it should know about from:

find /usr/local/Cellar -name "pkgconfig" -print

There is probably a smarter way to do this, but for the sake of simplicity, I just went through by hand and added the few keg only packages into my .zshrc file.

So I edited .zshrc to include:

export PKG_CONFIG_PATH="/usr/local/opt/openssl/lib/pkgconfig" export PKG_CONFIG_PATH="$PKG_CONFIG_PATH:/usr/local/opt/libtiff/lib/pkgconfig" export PKG_CONFIG_PATH="$PKG_CONFIG_PATH:/usr/local/opt/gmp/lib/pkgconfig" export PKG_CONFIG_PATH="$PKG_CONFIG_PATH:/usr/local/opt/libpng/lib/pkgconfig" export PKG_CONFIG_PATH="$PKG_CONFIG_PATH:/usr/local/opt/ncurses/lib/pkgconfig" export PKG_CONFIG_PATH="$PKG_CONFIG_PATH:/usr/local/opt/mpfr/lib/pkgconfig" export PKG_CONFIG_PATH="$PKG_CONFIG_PATH:/usr/local/opt/libyaml/lib/pkgconfig" export PKG_CONFIG_PATH="$PKG_CONFIG_PATH:/usr/local/opt/icu4c/lib/pkgconfig" export PKG_CONFIG_PATH="$PKG_CONFIG_PATH:/usr/local/opt/readline4/lib/pkgconfig" export PKG_CONFIG_PATH="$PKG_CONFIG_PATH:/usr/local/opt/webp/lib/pkgconfig" export PKG_CONFIG_PATH="$PKG_CONFIG_PATH:/usr/local/opt/unixodbc/lib/pkgconfig" export PKG_CONFIG_PATH="$PKG_CONFIG_PATH:/usr/local/opt/jpeg/lib/pkgconfig" export PKG_CONFIG_PATH="$PKG_CONFIG_PATH:/usr/local/opt/libpq/lib/pkgconfig"

Notice I’m using the path /usr/local/opt/, that is so I am not pegging any library to an explicit version; the paths are all symlinked by Homebrew to the latest versions. This is convenient but will mean I might end up in a similar issue as I had with Homebrew. I’m fairly confident that as ASDF doesn’t manage dependencies this will not be an issue and in testing I have managed to get multiple versions of NodeJS and different PHP versions all working at the same time without issue, something I had never previously achieved without it falling over using just Homebrew.

Anyway back to installing, with pkg_config now knowing where everything is you should be able to do:

asdf install php 7.4

And it will compile, yay.

What you should be left with is PHP and Composer installed and ready to go.

If you were to right now type php –version you would probably be disappointed to find it’s still the OS default. That’s because we need to do:

asdf global php 7.4

Ok now we are done and we should have PHP 7.4 setup and installed.

If we want to run a different version of PHP then it’s just:

asdf install php 7.2

Then within the project you want to use it’s simply:

asdf local php 7.2

Bonus –  getting NodeJS working with ASDF

One of the reasons for me moving to ASDF is I don’t work just with PHP, indeed I have multiple languages setup. I have noticed NodeJS, or I should say NVM, to be particularly cumbersome when it comes to managing versions so having ASDF setup I was particularly looking forward to being able to peg a specific version of Node and never have to worry about it again.

Why I’m mentioning it, the install progress has an extra step, it is documented but I read the documentation several times and ignored it. So to get NodeJS running you need to do:

asdf plugin-add nodejs

Then run:

bash -c '${ASDF_DATA_DIR:=$HOME/.asdf}/plugins/nodejs/bin/import-release-team-keyring'

This will install the GPG keys needed to then allow you to run:

asdf install nodejs latest

Caught me out, hopefully won’t catch you out.

So is ASDF-VM worth it?

Has it achieved the goals? Certainly it’s simplified the local development experience once up and running. 

It’s fast, everything is native and no containers

It’s simple to manage everything, and have multiple environments in multiple languages for a project. For example, utilising the .tools-version file you can lock a project to a specific PHP and nodeJS version.

The fact it is PURELY a version manager and not a package manager means you have to do the dependency and package management yourself, which, when setting up, is a right pain. I have hopefully simplified your pain a bit by combining the packages to one liners, but I did so by running the same “install php” command over and over and over finding each package then going and installing it, then repeating.

However, this is a process you should only have to do once. After that it shouldn’t need repeating.

Overall I’m fairly pleased, if your current solution works, don’t move, it’s not going to bring enough benefits. However if you are starting on a new machine or you want to work with lots of different languages pegged at different versions without reaching for containers this is a solution to look at.