SwiftPM in Xcode can't find include directories for a C library on Homebrew

Using Xcode 26.0.1 on an 2020 M1 MacBook Air. Trying out linking to a C library imported to my computer via Homebrew. When I compile my package, it immediately fails because it can’t find the C library’s header files.

Here’s the structure of my package:

me % tree
.
├── Package.swift
├── Sources
│   ├── CLibetpan
│   │   ├── empty.c
│   │   ├── module.modulemap
│   │   └── shim.h
│   └── SwiftEtpan
│       └── SwiftEtpan.swift
└── Tests
    └── SwiftEtpanTests
        └── SwiftEtpanTests.swift

Here’s the Package file, trying to combine the C-level library wrapper with the Swift-level one.

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

import PackageDescription

let package = Package(
  name: "SwiftEtpan",
  platforms: [
    .macOS(.v26)
  ],
  products: [
    // Publish the main target.
    .library(
      name: "SwiftEtpan",
      targets: ["SwiftEtpan"]
    )
  ],
  targets: [
    // Import the C version of "libetpan," assuming it was installed by
    // a pakage configurator.
    .systemLibrary(
      name: "CLibetpan",
      //pkgConfig: "libetpan",
      providers: [.brew(["libetpan"]), .apt(["libetpan-dev"])]
    ),

    // The core target to be published.
    .target(
      name: "SwiftEtpan",
      dependencies: ["CLibetpan"]
    ),

    // Tests for the core target.
    .testTarget(
      name: "SwiftEtpanTests",
      dependencies: ["SwiftEtpan"]
    ),
  ]
)

Then “shim.h”

#ifndef shim_h
#define shim_h

// This shim header is the single place where we include all the
// C headers from libetpan that we want to expose to Swift.
//
// By default, we'll just include the main convenience header.
// If you need more specific parts of the library, you can
// add more #include statements here.
#include <libetpan/libetpan.h>

#endif /* shim_h */

And “module.modulemap”

// This file defines the C module "CLibetpan" that Swift will import.
module CLibetpan [system] {
    // It tells the compiler to find headers in the "shim.h" file.
    header "shim.h"

    // It specifies the C library to link against.
    // The pkg-config in Package.swift handles the *path* to the library,
    // and this tells the linker *which* library to link (e.g., -letpan).
    link "etpan"

    // Exports all symbols found.
    export *
}

Note that all of these files are AI slop, and I don’t grok SPM well enough to recognize any screw-ups, so all this could be PEBKAC.

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:

  1. 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?

  2. Adjust the modulemap file so that it can find the headers itself and make a module that Swift can import.

  3. 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

Ah… ChatGPT is quite useful after all…. It explained the linker error to me. It seems that libetpan uses another library named iconv which we were not linking to.

If we update the linker setting in the package manifest to:

        linkerSettings: [
            .unsafeFlags(["-Xlinker", "-L/opt/homebrew/lib"]),
            .linkedLibrary("iconv")
        ]

Then it compiles.... or at least it does on the command line. Fails in Xcode.... Not sure why.

And now it builds in Xcode. Not sure what changed its mind!

Here's the repo if you'd find it useful.