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!
https://github.com/carlynorama/SwiftLIBPNG
References
- How Swift imports C APIs
- How to use C libraries in Swift?
- How to wrap a C library in Swift, but also its more up to date repo
- Making a C library available in Swift using the Swift Package Manager
- https://clang.llvm.org/docs/Modules.html
- Guide to including a C library into an iOS project written in Swift
- PackageManager BuildSettings Proposal (implemented)
- PackageManager LinkerSettings for when it really is a System library.
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
mkdir $YOUR_LIBRARY_NAME
cd $YOUR_LIBRARY_NAME
# 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
cd $SYSLIB_NAME
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?
cd $PROJECT_DIR
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
->Sources
->png
--- module.modulemap
--- umbrella.h
->SwiftLIBPNG
--- 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.
.library(
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"])]),
.target(
name: "SwiftLIBPNG",
dependencies: ["png"]),
.testTarget(
name: "SwiftLIBPNGTests",
dependencies: ["SwiftLIBPNG"]),
]
)