Adventures in Custom Compiling PHP 7.4

Famous last words: uh … the PHP 7.4 migration guide did mention there were some backwards compatible changes, right?  I’ve already written about the Strange Case of ArrayObject.  Now it’s time to have a look at a very obscure reference in the migration guide referring to changes in custom PHP installation: the migration to pkg-config.  Before I get into what changed and its significance, we need to step back for a moment and review the compile process.

 

PHP Compile Process Revisited

Many of us are used to either having PHP already available on our web servers, or having it provided for us through an operating system package manager (e.g. for Redhat/Fedora/CentOS includes yum).  In addition, many of us have used meta-packages such as XAMPP, providing not only PHP, but Apache, MariaDB and Perl as well.  So the natural question arises: why bother compiling PHP from source?

The answer in most cases can be boiled down to two main trends.  One situation is where you need to produce a stripped-down version of PHP only containing exactly what’s needed for a particular application to produce the lightest possible footprint and best performance.  The second is where a DevOp needs to test drive an upcoming version of PHP that has not yet been released.

For those who have not yet compiled their own version of PHP from source, after downloading the source code from https://www.php.net/downloads.php, the procedure is as follows:

  1. Extract the source code into /some/directory
  2. Open a terminal window / command prompt and change to /some/directory
  3. Run configure adding PHP-related option flags as needed
  4. Run make
  5. Run make test
  6. Run make install (as a super user)

Now, without a doubt, you’re wondering what in heck is configure and what is make?   configure accepts a series of command line options and produces a makefile.  The makefile is a script that, as mentioned in the GNU documentation, “tells make what to do”.  An analogy might be to the relationship between a composer.json file and composermake is a tool that assembles strings of paths and directives, and calls the appropriate compiler.  It can also copy and update configuration files as needed.  make is not tied to C language, but is commonly used to ease the process of compiling C code.

 

So … Exactly What Changed in PHP 7.4?

The focus of this article is on changes PHP 7.4 makes to configuration option flags during the custom compile process.  Many options have switched from --with-xxx to --enable-xxx … and vice versa.  Also, in a number of cases, configure options that included a directory reference have been simplified.  Thus, where you would formerly specify an option as --with-xxx-dir=/yyy you now would use this syntax: --with-xxx.

It can get a little crazy.  Here’s one example:  prior to PHP 7.4, when custom compiling PHP, you might specify the following configure option if your application needed XML as well as the Zip extensions:

$ configure --enable-libxml --with-libxml-dir=/usr --with-libzip

When compiling PHP 7.4, however, the option flags are flipped around, and you end up with this:

$ configure --with-libxml --enable-zip

Note that with and enable have been flipped.  You’ll also note that the former --with-libxml-dir=/usr option has been removed and rolled into the simplified form --with-libxml.

I think, at this point, we can all agree that the second example is much more streamlined and concise, despite the crazy flipping between with and enable.  However … you are probably wondering, as was I, how is this possible? Hows does the compiler know where to find the associated OS libraries?  In order to properly solve this mystery, I introduce a new player: pkg-config.

 

What Is pkg-config and What Does It Have to Do With PHP?

Okay … first things first.  What the heck is pkg-config? pkg-config is a tool used to help manage applications or library compile-time options.  As with make, it’s not specifically tied to C language, but is often used when compiling C source code. pkg-config manages a list of operating system packages and the location of files associated with each package.

As for the followup question, what does this have to do with PHP, think back for a second to the example shown just above.  Why should we, as PHP developers, have to instruct the compiler as to the location of the directory structure containing files for this or that operating system package … when such a tool already exists?  Click!  Lightbulb goes on!  This is where pkg-config comes into play.

Let’s now have a look at a list of configure options no longer needing a directory path as an argument.

 

Configure Options No Longer Needing a Directory Path

In PHP 7.4 a number of extensions, mainly those reliant upon operating system libraries, have been migrated over to use pkg-config in locating their associated files.  What this means, for the most part, is that any configure directives involving these extensions no longer require you to provide a directory path.  Here are the extensions affected with options that no longer accept a directory path as an argument:

Extension New Option Extension New Option
CURL –with-curl Enchant –with-enchant
IMAP –with-kerberos-systemd LDAP –with-ldap-sasl
ODBC –with-iodbc OpenSSL –with-ldap-sasl
PDO_SQLite –with-pdo-sqlite Readline –with-libedit
Sodium –with-sodium SQLite3 –with-sqlite3
XSL –with-xsl FPM –with-fpm-systemd

This list is not complete, however, as there are a couple of other extensions affected by the change.

 

Other Configure Option Changes Need to Worry About

The list mentioned above clearly shows that your life as a custom PHP producer has just been made way easier: all you need to do is to make sure pkg-config knows where everything is located (more on that later!).  So … life is good, and can you now go get that beer?  Uh … not quite yet.  There are a couple of other configure related changes that are going to make your life slightly more complicated.  Sorry!  OK, taking a deep breath, let’s dive in.

The intl extension is needed if you maintain code for internationalized websites.  It’s also used by many frameworks, including Symfony, Magento, CakePHP and Laminas, to name a few.  The previous configure option flag, –with-icu-dir, is now removed.  However, if you enable it using the new configure option –enable-intl then libicu is required.  Not only that, but the operating system ICU (International Components for Unicode) version must be 50.1 or greater. 

Working with the libxml extension is a bit trickier.  As I alluded in the simple configure example shown above, the –with-libxml-dir and -enable-libxml options have been removed, both rolled into a single option –with-libxml.  Also, no directory path is required as the information is retrieved from pkg-config.

The extension with the most complex changes is the GD extension.

 

What About the GD Extension?

First of all, to be sure everybody is on the same page, the GD extension is arguably the most widely used  PHP extension for handing graphics.  GD originally stood for GIF Draw, but early on GIF support was actually withdrawn due to legal issues relating to a Unisys patent.  The patent expired in 2004, and GIF processing returned to the extension.  Historically the extension also provides support for JPG, PNG and SWF images.  Accordingly, there are several OS libraries needed to make it all work.  Prior to PHP 7.4, you needed to supply directory paths for all of these underlying libraries.  This, to use the vernacular, was a pain in the butt (“pain in the bottom” for the English-speaking readers).  Accordingly, the following changes are now in effect:

Old Option New Option
–with-gd –enable-gd
–with-jpeg-dir –with-jpeg
–with-webp-dir –with-webp
–with-xpm-dir –with-xpm

To further simplify matters these two options have been removed: –with-png-dir and –with-zlib-dir. However, you now need to ensure that the OS has both the libpng and zlib libraries.  You also have the option to leverage a custom libgd using the –with-external-gd configure option, which does require a directory path.  For an overview of configure option changes, have a look at the php.net documentation reference: https://www.php.net/manual/en/migration74.other-changes.php#migration74.other-changes.pkg-config.

So that about wraps up the backwards incompatible changes when compiling PHP 7.4.  Let’s now have a look at an example of a custom compile using the Linux for PHP Docker image.  (Aha … here comes the shameless plug, right?  You knew there had to be a catch!)

 

Using Linux for PHP to Compile a Custom PHP Version

(Shameless plug:) For those of you who have not yet encountered Linux for PHP (LfPHP for short), it’s a Docker image you can use for PHP testing and development that includes not only a custom-compiled version of PHP, but also a complete LAMPP stack, including (among many other things) Apache 2.4, MariaDB, OpenSSL, OpenLDAP, PostgreSQL, Perl, Python and Ruby.  What many folks don’t know (assuming you knew this much already!) is that a full set of tools are available allowing you to download and compile your own version of PHP, including wget, git, gcc, make, configure and, of course our new friend pkg-config.

LfPHP images are based upon asclinux, a custom version of Linux developed by Andrew Caya, specifically designed for Docker and cloud environments.  (Full disclosure: Andrew is the CEO of PHP-CL, of which I am a partner.)  What I like about this version of Linux is that it’s lean and mean: stripped down, minus the usual overhead and baggage associated with X Windows desktops.  This means, of course, that you can only interact via the command line.  However as it’s main purpose is to serve up web-based applications in a cloud environment, you can connect to it using your browser.  Also, as it’s a Docker container, you can mount a volume that maps to a directory on your host computer, allowing you to modify source code without having to mess with the command line inside the container.  If you are interested in asclinux roots, have a look at the Linux from Scratch project.

The current set of images are available here: https://hub.docker.com/u/asclinux.  As you scroll down the page you’ll notice many different versions of PHP are available (even including PHP 8.0!).  For the purposes this illustration we will compile a custom version of PHP 7.4.6.

 

LfPHP Auto Compile

There are two ways to compile a custom version of PHP.  One technique is to use lfphp-compile, a utility included with LfPHP.  All you need to do is to include this as an add-on to docker run and you’ve got a new version of PHP … like magic!  Here is the command line string you could execute to trigger the auto-compile option:

docker run -dit -p 8181:80 asclinux/linuxforphp-8.2-ultimate:src /bin/bash -c "lfphp-compile 7.4.6 nts"

You then simply wait for a few minutes for the compile to finish, and voila, instant any-version of PHP.  If you’re impatient, just find the running container ID, and use docker exec -it CONTAINER_ID /bin/bash to see what’s going on.  Here’s a screen shot of the compile in process (using top):

But I will introduce a slight hiccup: let’s say that we also want to include the latest version of libzip.  Further, let’s say we want to specify our own compile flags.  Using the auto-compile option won’t work.  Fortunately LfPHP also provides a manual compile option.

LfPHP Manual Compile

To perform a manual compile against the base LfPHP image, all you really need to do is to run the same docker command as above, but leave off the trailing lfphp-compile command.  The syntax looks like this:

docker run -it -p 8181:80 asclinux/linuxforphp-8.2-ultimate:src /bin/bash

The next step is to download the source code.  Most DevOps prefer to go to https://php.net/downloads.php to find the source package to install.  If you’re feeling extremely adventurous, on the other hand, go directly to the PHP source on github: https://github.com/php/php-src.  From there, click on the button labeled Branch and choose the branch you want to download.  You can then either clone this branch, or download the source code in the form of a ZIP file.  For the purposes of this article we’ll grab PHP 7.4.6 from github as shown here:

cd /root
wget https://github.com/php/php-src/archive/PHP-7.4.6.zip
unzip PHP-7.4.6.zip

As the source code was drawn directly from github, there’s one additional step needed: running buildconf.  This step is not required if you download from https://php.net/downloads.php.

cd /root/php-src-PHP-7.4.6/
buildconf --force

Here is the output:

Another difference between using source from github.com rather than php.net/downloads is that the directory structure after unzipping from github has a prefix php-src-.

At this point, we are ready to run through the normal compile process … but there’s one problem:  I want to use the absolute latest version of libzip.

Adding Libraries into the Custom Compile

Installing an additional operating system library is just a matter of downloading the source and compiling.  To use libzip as an example, we can proceed as follows:

cd /root
wget https://libzip.org/download/libzip-1.6.1.tar.gz
tar xvfz libzip-1.6.1.tar.gz

We can then move into the extracted directory and perform the usual make, make install, etc. on the new library:

cd libzip-1.6.1
mkdir build
cd build
cmake ..
make
make install

OK, so far so good!  But when confirming the installation using /sbin/ldconfig -p |grep libzip, it appears there are two versions installed: the one that came with LfPHP, and the new one just installed.  To make matters worse, running pkg-config --list-all |grep libzip returns a blank.  So, to sum up the situation, we have two versions of libzip installed, and neither one is recognized by pkg-config!  This is going to pose a big problem when we go to compile PHP and include the ZIP extension.

Getting pkg-config to Recognize a Library

A closer examination of the output from the libzip installation process reveals that a pkg-config *.pc file was created for the new installation.  Further, you would see that it’s placed in the /usr/local/lib/pkgconfig folder.  Here is what it looks like:

In order to get pkg-config to recognize packages outside of its normal purview, you need to add this directory to the list represented by the environment variable PKG_CONFIG_PATH.  As a test, here is how that might work:

We are now ready to rock N roll!  Without further ado, let’s compile PHP.

Manually Compiling PHP

As mentioned above, the first step is to run configure.  This requires a string of options that varies depending on what you want in your custom version.  For the purposes of this article, here is the option string used.  Please note that the trailing backslash (“\“) is used to indicate that the command should be all on a single line.

./configure  \
    --prefix=/usr --sysconfdir=/etc --localstatedir=/var --datadir=/usr/share/php \
    --mandir=/usr/share/man --enable-fpm --with-fpm-user=apache --with-fpm-group=apache \
    --with-config-file-path=/etc --with-zlib --enable-bcmath --with-bz2 --enable-calendar \
    --enable-dba=shared --with-gdbm --with-gmp --enable-ftp --with-gettext=/usr --enable-mbstring \
    --enable-pcntl --with-pspell --with-readline --with-snmp --with-mysql-sock=/run/mysqld/mysqld.sock \
    --with-curl --with-openssl --with-openssl-dir=/usr --with-mhash --enable-intl --with-libdir=/lib64 \
    --enable-sockets --with-libxml --enable-soap --enable-gd --with-jpeg --with-freetype --enable-exif \
    --with-xsl --with-xmlrpc --with-pgsql --with-pdo-mysql=/usr --with-pdo-pgsql --with-mysqli \
    --with-pdo-dblib --with-ldap --with-ldap-sasl --enable-shmop --enable-sysvsem --enable-sysvshm \
    --enable-sysvmsg --with-tidy --with-expat --with-enchant --with-imap=/usr/local/imap-2007f \
    --with-imap-ssl=/usr/include/openssl --with-kerberos=/usr/include/krb5 --with-sodium=/usr \
    --with-zip --enable-opcache --with-pear --with-ffi

When configure finishes creating the makefile, you should see something like this:

You are now ready to proceed with the next set of commands.  Bear in mind that each additional configure option causes another library to be compiled, thus lengthening the time it takes to run make and make test:.  You only need to run make clean if you’ve already run make once unsuccessfully.  make test is also optional, however, for the greater good of the PHP community please run this and send any reports back to the PHP core team.

make clean
make
make test
make install

Here is the output from make install:

And, of course, the final test, see if PHP is installed and is the correct version:

And, as the saying goes, that’s all folks.

Conclusion

There are certain situations where a custom-compiled version of PHP is desired.  One such is where you want to to strip it down to its bare minimum requirements in order to achieve the greatest possible speed.  In other situations, developers, testing groups, and people involved with technical documentation may need access to a version of PHP that has not yet been officially released.

One key takeaway you get from this article is that the configuration options have changed in PHP 7.4, with a greater reliance on the operating system utility pkg-config.  In particular, configure options pertaining to the GD extension have radically changed.

One tool ideal for testing a custom PHP installation is Linux for PHP.  As you read in this article, there is a src tagged image that allows you to do both an automated custom PHP installation as well as going through the manual process.  An excellent overview of the manual compile process can be found in the overview here: https://hub.docker.com/r/asclinux/linuxforphp-8.2-ultimate.

Another key takeaway from this article is that when you need to get a custom library to be recognized during the compile process, you can add the path to the library’s pkg-config *.pc file by appending a value to the PKG_CONFIG_PATH environment variable.

Finally, the notes for this article, along with a shell script and Dockerfile, can be found here: https://github.com/phpcl/lfphp_custom_php_source.  Thanks for reading and happy compiling!