Hi,
Out of curiousity, I also wanted to learn about / remind myself about how to do this, so I attempted to complete your task by following the SwiftPM Docs on 'Adding a System Library Dependency'.
Overview
You can link against system libraries, using them as a dependency in your code, using the package manager. To do so, add a target of type systemLibrary, and a module.modulemap for each system library you’re using.
Seems simple enough: make a new target / module in the package manifest and then create a module.modulemap (whatever that may be) for the C lib.
Header and Linker search paths
Before we deal with the two tasks so far mentioned in the docs, it seems there's some more things to deal with...
For Unix-like systems, Swift Package Manager can use pkgConfig to provide the compiler with the paths for including library headers and linking to binaries. If your system doesn’t provide pkgConfig, or the library doesn’t include package config files, you can provide the options to the Swift compiler directly.
OK - macOS is a Unix-like system, perhaps pkgConfig will provide the paths to the headers and binaries.
zsh: command not found: pkgConfig
It turns out that pkgConfig isn't installed by default on macOS.
(We could, perhaps, install pkgConfig via Brew but that would make our package not very portable / fragile.)
$ pkg-config --cflags libetpan
Package libetpan was not found in the pkg-config search path.
Perhaps you should add the directory containing 'libetpan.pc' to the PKG_CONFIG_PATH environment variable Package 'libetpan' not found
So: installing pkgConfig doesn't help anyway as the libetpan package doesn't appear to provide the header locations automatically.
Perhaps we must provide the options to the compiler directly...
Declaring the system library
Ah, one of the two main tasks...
This seems pretty straightforward to anyone who is already familar with SwitPM - it's just adding a new target to our package: a .systemLibrary target:
.systemLibrary(
// Our Swift module name
name: "clibetpan",
// C Library name
pkgConfig: "libetpan",
// Name of the package in package delivery systems.
providers: [
.brew(["libetpan"]),
.aptItem(["libetpan-dev"])
]
),
Authoring a module map
The other main task.
The module.modulemap file declares the C library headers, and what parts of them, to expose as one or more clang modules that can be imported in Swift code. Each defines:
- A name for the module to be exposed
- One or more header files to reference
- A reference to the name of the C library
- One or more export lines that identify what to expose to Swift
It would appear that we need to create the module.modulemap file in the clibetpan directory with the following contents:
module clibetpan [system] {
header "clibetpan.h"
link "clibetpan"
export *
}
OK - let's see how we have got on...
Let's try and call a C function from Swift
EtpanDemo.swift
import ArgumentParser
import clibetpan
@main
struct EtPanDemo: ParsableCommand {
mutating func run() throws {
print("Hello, world! Trying to use libetpan")
// Libetpan function call
mailimf_mailbox_list_new_empty()
print("libetpan version: \(libetpan_get_version_major()).\(libetpan_get_version_minor()) ")
}
}
Try and build the package.
$ swift build
Building for debugging...
error: emit-module command failed with exit code 1 (use -v to see invocation)
/Users/diggory/Code/Swift/EtPanDemo/Sources/clibetpan/module.modulemap:2:10: error: header 'libetpan.h' not found
1 | module clibetpan [system] {
2 | header "libetpan.h"
| `- error: header 'libetpan.h' not found
3 | link "etpan"
4 | export *
OK, there's a problem with the modulemap file. It cannot find the headers.
The docs say:
Try to reference headers that reside in the same directory or as a local path to provide the greatest flexibility. You can use an absolute path, although that makes the declaration more brittle, as different systems install system libraries in a variety of paths.
The headers aren't in our package - they are in the Homebrew storage, that's why they aren't found.
I thought that there may be three options for solving this:
-
As mentioned earlier in the docs, we could pass the paths to the headers and binaries as options to the compiler manually. This seems not very portable/fragile and also fiddly. Wouldn't it be better to incorporate this information into the package itself?
-
Adjust the modulemap file so that it can find the headers itself and make a module that Swift can import.
-
Copy the headers from Homebrew into our package so that the headers can be found locally. This seems brittle as well, as any change in the library headers will need to be manually updated in our own package.
Option 3 - Copy headers
I tried copying the header files from Homebrew into our clibetpan module's folder so that the module could be built - however this lead to another issue: the main header libetpan.h refers to a lot of other headers using the form #include <libetpan/subheader.h>. This (the angle brackets) appears to mean "look in the system or library locations" for the headers. Homebrew's packages headers are not in the standard locations so this doesn't work. (If they were then the compilation would have worked earlier...)
Didn't work.
Option 2 - Adjust modulemap file to inform it where the headers are
The Swift docs mentioned above link to the LLVM/Clang docs that explain ModuleMap language.
It appears that you can define a directory as an 'umbrella directory' where it can find multiple headers. I thought that this might work - but alas it didn't (for the same reason as Option 3 above). Also this would mean hard-coding absolute paths into the modulemap file, which might be fine for macOS only, but not ideal.
Didn't work.
Option 1 - Manually pass the paths to the swift build command
The docs explicitly stated this, so I suppose I should have been paying attention!
To manually provide search paths for headers, use the -Xcc -I/path/to/include/ as additional parameters to swift build. To match the above example from pkgConfig, the additional command line options would be: -Xcc -I/opt/homebrew/Cellar/libgit2/1.9.0/include
and
To manually provide search paths for linking to binaries, use the -Xlinker -L/path/to/include/ as additional parameters to swift build. To match the above example from pkgConfig, the additional command line options would be: -Xlinker -L/opt/homebrew/Cellar/libgit2/1.9.0/lib.
By updating our executable target to:
.executableTarget(
name: "EtPanDemo",
dependencies: [
.product(name: "ArgumentParser", package: "swift-argument-parser"),
.byName(name: "clibetpan"),
],
swiftSettings: [.unsafeFlags(["-I/opt/homebrew/include", "-L/opt/homebrew/lib"])]
),
The build still fails, but now the headers are visible to the package (e.g. you can command-click on mailimf_mailbox_list_new_empty() in Xcode and it takes you to the header).
N.B. Changing the settings in the package manifest didn't seem to take effect in Xcode until the package was closed and opened again...
$ swift build
warning: you may be able to install libetpan using your system-packager:
brew install libetpan
warning: you may be able to install libetpan using your system-packager:
brew install libetpan
[1/1] Planning build
Building for debugging...
error: link command failed with exit code 1 (use -v to see invocation)
ld: warning: Could not find or use auto-linked library 'etpan': library 'etpan' not found
Undefined symbols for architecture arm64:
"_mailimf_mailbox_list_new_empty", referenced from:
EtPanDemo.EtPanDemo.run() throws -> () in EtPanDemo.swift.o
ld: symbol(s) not found for architecture arm64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
[3/5] Linking EtPanDemo
I'm afraid that's as far as I've got - I'm not sure why the header setting works, but the binaries path / linker setting doesn't...
I should imagine that someone who knows more about C and SwiftPM will be able to help... or hopefully that's put you further down the road to working it out...
[edit] Ah... I see my mistake, the linker setting is separate from the other setting. So, the section of the manifest should be:
.executableTarget(
name: "EtPanDemo",
dependencies: [
.product(name: "ArgumentParser", package: "swift-argument-parser"),
.byName(name: "clibetpan"),
],
swiftSettings: [.unsafeFlags(["-I/opt/homebrew/include"])],
linkerSettings: [.unsafeFlags(["-Xlinker", "-L/opt/homebrew/lib"])]
),
Although that now fails with a different linker error:
Building for debugging...
error: link command failed with exit code 1 (use -v to see invocation)
Undefined symbols for architecture arm64:
"_iconv", referenced from:
_mail_iconv in libetpan.a[12](charconv.o)
_mail_iconv in libetpan.a[12](charconv.o)
"_iconv_close", referenced from:
_charconv in libetpan.a[12](charconv.o)
_charconv in libetpan.a[12](charconv.o)
_charconv_buffer in libetpan.a[12](charconv.o)
"_iconv_open", referenced from:
_charconv in libetpan.a[12](charconv.o)
_charconv_buffer in libetpan.a[12](charconv.o)
ld: symbol(s) not found for architecture arm64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
[5/7] Linking EtPanDemo