A Workflow for Iterating On Libraries and Applications Together

Debugging a program or writing a feature often gets harder when 3rd party libraries or toolkits are involved. The function calls within the library might not show up in your debugger, and if you make changes to the library source code it might not be obvious how to get your application to pick them up. Here I’ll describe some common solutions to these problems, and lay out the workflow I use.

Debugging symbols

The quickest and easiest trick is often installing debugging symbols. On Ubuntu, this takes a little setup but is very much worth it.

With debugging symbols installed more information about calls into the library will be shown in a debugger, but you’re still using the same binary libraries. You can’t make changes to them and the available debugging information might be lower quality than you’d get with an unoptimized build. For example, functions might not show up if they’ve been inlined and some variables might be optimized out.

Building Libraries From Source

If you build from source it’s easy to build with debugging symbols included (refer to the documentation of the build system the library uses for details). You can also patch the libraries, and even use printf debugging in them. The hard part is making the program build against and run with your custom build of the library.

Installing to System Directories

Once you’ve built a library it’s generally possible to install it to system directories. sudo make install or sudo ninja -C build install will install it to /usr/local/lib or a similar directory. I advise against doing this most of the time. It can be difficult to cleanly remove libraries installed this way, they’ll get used instead of the system version of the same library when you don’t want them to and they can conflict with packages installed by the package manager in all sorts of nasty ways.

Install to a Subdirectory

There’s a few different directory layouts that work.

  • A single install directory within your home directory
  • One install directory for multiple projects
  • An install directory for each library

I generally go with the last one and keep it within the project’s directory, next to the build directory. To do this with cmake run the cmake command with -DCMAKE_INSTALL_PREFIX=$PWD/install (-DCMAKE_INSTALL_RPATH="$INSTALL/lib" might also be needed if the library consists of several .so files that need to find each other) and for meson it’s --prefix $PWD/install. You can now make install or ninja -C build install (no need for sudo) without touching your system directories.

Build and Run Against Custom Library Build

The easiest way to make a program use a library installed to a specific directory is by setting PKG_CONFIG_PATH=/path/to/install/lib/pkgconfig/ and running your build tool. Note that you’ll probably have to delete your build directory and run cmake or meson again with PKG_CONFIG_PATH set to pick up the change (make and ninja generally don’t care about PKG_CONFIG_PATH).

The particular value of PKG_CONFIG_PATH can vary. It should be a directory in the install directory called pkgconfig that contains .pc file(s). You can use find path/to/install -name '*.pc' if you’re not sure.

Other Tricks

If things aren’t working, there’s other things you can try. Setting LD_LIBRARY_PATH to the path of the directory that contains the .so files you want to use is often helpful. Note that unlike PKG_CONFIG_PATH this needs to be set when the program is run. Also setting LD_PRELOAD to the path of the .so file (not its directory) can work.

Simplifying With Scripts

Rather than remembering all these details, I tend to dump the stuff that needs to be done for a particular library into scripts. I’ll have a build.sh script that builds and installs (to a subdirectory), and a use.sh script that can be run from any location and sets the environment variables needed to use the library in another project. (the use.sh script isn’t executable, as a reminder that it needs to be sourceed instead of run)

Example

Here’s an example of the scripts I mentioned, I usually tweak them a little for each library. Both are placed in the project directory of the library.

build.sh:

#!/usr/bin/env bash

# Unofficial BASH safe mode (not super important here, but becomes valuable if the script grows in complexity)
# -e: exit if any command fails
# -u: exit if there are references to uninitialized variables
# -o pipefail: exit if any command in a pipeline fails
set -euo pipefail

# Move into the directory where this script is (if we’re not already there)
cd "$( dirname "${BASH_SOURCE[0]}" )"

# Set up the build directory if it doesn’t exist
if test ! -d build; then
  meson build --prefix ”$PWD/install”
fi

# Build and install
ninja -C build install


echo “Done”

use.sh:

# Not executable because this needs to be sourced

# Get the directory where this file is
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"

# This will be something like “x86_64-linux-gnu”
TRIPLET=$(gcc -dumpmachine)

# Set PKG_CONFIG_PATH so other projects can find this library
export PKG_CONFIG_PATH="$DIR/install/lib/$TRIPLET/pkgconfig:$PKG_CONFIG_PATH"

echo "Using LibFoo from $PWD/install"

To use build.sh you simply mark it as executable (chmod +x build.sh) and run it (./build.sh). To use use.sh you source it before running the build system of your application. Rather than sourcing as a standalone command (which pollutes the environment of the shell you’re in), I prefer to do it in a subshell along with the invocation of the build tool. This is done with parentheses:

~/code/bar/build$ (source ~/code/foo/use.sh && cmake ..)
~/code/bar/build$ make
~/code/bar/build$ ./my_binary

Conclusion

I encourage you to experiment with this kind of thing if you haven’t already. What’s described here is just one of many possible techniques. I found it through trial-and-error and It works for me, but something slightly or entirely different might fit your needs best.

1 Like