Can I get my hands on a homebrew installed library?

Once the pixel data is generated (okay, which it isn’t yet), that data will need to be saved to a file that can be shared on the internet.

The PNG file format spec is pretty straight forward, and there is an official library for it called libpng. ImageMagick and ffmpeg depend on libpng and I typically have those installed.

This week I investigated if I could in fact wrap a system library in a package and then use it from Swift. I got to “Hello version” pretty fast, but then realized my C was pretty rusty!


Checking for Packages

Install checks using homebrew:

# Check if installed at all
which libpng
# Check if installed
brew ls --versions libpng
# Install it if necessary
brew install libpng
# Check who uses it
brew uses --installed libpng
# Check what it needs, including dependencies of dependencies
# which in the case of libpng is nothing.
brew deps --include-build libpng
# just get more info
brew info libpng

Install checks using apt:

# Check if installed at all
which libpng
# Check if installed
apt list --installed libpng-dev
# Install it if necessary
sudo apt-get install libpng-dev
# Check who uses it
apt-cache rdepends libpng-dev
# Check what it needs
# which in the case of libpng is nothing.
sudo apt depends libpng-dev
# just get more info
apt-cache show libpng-dev

Making the Package

Here are the general steps to wrap a system library in a Package with some helper Swift code. Like last week I have an example package that is a WIP.

Learn about the library you want to wrap.

brew install $THING_TO_WRAP  
cd /usr/local/include/
ls -a 
# look for header file of $THING_TO_WRAP so you know its name for later. 

Start the Library

# Note HWS tutorial uses `system-module` which appears to be deprecated. 
swift package init --type library 

Update Package.swift target’s section to include a reference to the installed C system library

(see contents of full file below)

In this example the name is the same as the folder where modulemap lives, which the same as the libraries header file (without the .h)), which is the same as the module map link. This kept it easiest for me.

The pkgConfig parameter is an optional setting, but it does appear to be required for libraries not really in the the System or in the CommandLineTools SDK. The name needed can be found by (on a computer with pkg-config installed) with pkg-config --list-all | grep $THING_TO_WRAP or some fraction of the $THING_TO_WRAP name. I was not able to get this package to compile without pkg-config installed and without this parameter set. My guess is that is because libpng is not a real System Library, nor in the CommandLineTools SDK.

providers is truly optional adding providers will NOT auto install dependencies at this time (2023 Apr).

Neither pkgConfig or providers will auto install dependencies, is my current understanding.

.systemLibrary(name: "png", pkgConfig: "libpng", providers: [.apt(["libpng-dev"]), .brew(["libpng"])]),

Make sure to add the name to the library target’s dependencies as well.

Create a module.modulemap file

If using the deprecated(?) system-module command this was created for you at the top level, but when using the library initializer it has to be done by hand. This file is what really tells the compiler where to find the library. We have choices here, to make a regular header, a shim header, a bridging header, or a umbrella header… I’ve gone with an umbrella header like the SwiftGD repo.

cd Sources
mkdir $SYSLIB_NAME # as referenced in the "name" parameter
touch module.modulemap
touch umbrella.h

module.modulemap contains

module png {
	umbrella header "umbrella.h"
	link "png"

umbrella.h contains the one line #include <png.h>

If XCode is having Trouble finding the library

What does compiling in the command line say?

swift package clean
swift build --verbose

Is package-config installed?

brew install pkg-config

Homebrew path correct?

When installing homebrew did you update the path? Especially important on M1 macs where the install path has changed to /opt/homebrew/ from /usr/local/include/.

Check cat ~/.zprofile it should exist and contain eval "$(/opt/homebrew/bin/brew shellenv)"

If ~/.zprofile does not exist and contain that line run: echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> ~/.zprofile eval $(/opt/homebrew/bin/brew shellenv)

Commands run: export HOMEBREW_PREFIX="/opt/homebrew"; export HOMEBREW_CELLAR="/opt/homebrew/Cellar"; export HOMEBREW_REPOSITORY="/opt/homebrew"; export PATH="/opt/homebrew/bin:/opt/homebrew/sbin${PATH+:$PATH}"; export MANPATH="/opt/homebrew/share/man${MANPATH+:$MANPATH}:"; export INFOPATH="/opt/homebrew/share/info:${INFOPATH:-}";

If this command provides no output if it has already been run.

To check the path: echo $PATH it should now contain /opt/homebrew/bin:/opt/homebrew/sbin:

Directory Structure

--- module.modulemap
--- umbrella.h
--- SwiftLIBPNG.swift

Package file

// swift-tools-version: 5.7
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
    name: "SwiftLIBPNG",
    products: [
        // Products define the executables and libraries a package produces, and make them visible to other packages.
            name: "SwiftLIBPNG",
            targets: ["SwiftLIBPNG"]),
    dependencies: [
        // Dependencies declare other packages that this package depends on.
        // .package(url: /* package url */, from: "1.0.0"),
    targets: [
        .systemLibrary(name: "png", pkgConfig: "libpng", providers: [.apt(["libpng-dev"]), .brew(["libpng"])]),
            name: "SwiftLIBPNG",
            dependencies: ["png"]),
            name: "SwiftLIBPNGTests",
            dependencies: ["SwiftLIBPNG"]),