How to use SE-0238 in Swift 5 with system libraries?

I've wrapped an ImageMagick script in a Swift package: GitHub - danramteke/MagickBird: Swifty wrapper around ImageMagick. Requires ImageMagick.

Currently, I run my program (after brew install imagemagick), like this: swift run -Xswiftc -I/usr/local/Cellar/imagemagick/7.0.8-42/include/ImageMagick-7 -Xlinker -L/usr/local/Cellar/imagemagick/7.0.8-42/lib -Xcc -DMAGICKCORE_HDRI_ENABLE=0 -Xcc -DMAGICKCORE_QUANTUM_DEPTH=16

With the release of SE-0238 in Swift 5, I was hoping to be able to reduce my command to something like swift run.

However, after looking through the source code quite a bit, and reading and re-reading the SE-0238, I cannot figure out how to pass the paths to the homebrew installation of ImageMagick. The linkerSettings options only takes the names of the libraries. How do I specify the search paths?

Thanks for your help!

Oops - my link to SE-0238 was in correct. The correct link is SE-0238: Package Manager Target Specific Build Settings

I experimented a bit with translating the swift run invocation from your readme into corresponding build settings in the package manifest. This is the result.

Here's the swift run command (note that I replaced the ImageMagick version number with what's installed on my system):

swift run \ 
  -Xswiftc -I/usr/local/Cellar/imagemagick/7.0.8-43/include/ImageMagick-7 \ 
  -Xlinker -L/usr/local/Cellar/imagemagick/7.0.8-43/lib \ 
  -Xlinker -lMagickWand-7.Q16HDRI \
  -Xlinker -lMagickCore-7.Q16HDRI \
  -Xcc -DMAGICKCORE_HDRI_ENABLE=0 \ 
  -Xcc -DMAGICKCORE_QUANTUM_DEPTH=16

And here's the Package.swift I came up with:

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

import PackageDescription

let package = Package(
    name: "MagickBird",
    products: [
        .library(name: "MagickBird", targets: ["MagickBird"])
    ],
    dependencies: [
    ],
    targets: [
        .target(
            name: "MagickBirdSample", dependencies: ["MagickBird"]),
        .target(
            name: "MagickBird", 
            dependencies: ["MagickWand"],
            cxxSettings: [
                .define("MAGICKCORE_HDRI_ENABLE", to: "0"),
                .define("MAGICKCORE_QUANTUM_DEPTH", to: "16"),
            ],
            swiftSettings: [
                .unsafeFlags([
                    "-I/usr/local/Cellar/imagemagick/7.0.8-43/include/ImageMagick-7",
                ]),
            ],
            linkerSettings: [
                .unsafeFlags([
                    "-L/usr/local/Cellar/imagemagick/7.0.8-43/lib",
                    "-lMagickWand-7.Q16HDRI",
                    "-lMagickCore-7.Q16HDRI",
                ]),
            ]),
        .systemLibrary(name: "MagickWand"),
    ]
)

Unfortunately, I get an error when typing swift run now:

$ swift run
<module-includes>:1:9: note: in file included from <module-includes>:1:
#import "/usr/local/opt/imagemagick/include/ImageMagick-7/MagickCore/MagickCore.h"
        ^
/usr/local/opt/imagemagick/include/ImageMagick-7/MagickCore/MagickCore.h:29:12: error: 'MagickCore/magick-config.h' file not found
#  include "MagickCore/magick-config.h"
           ^
<unknown>:0: error: could not build Objective-C module 'MagickWand'

But maybe this helps you get on the right track?

Note that unsafe flags are not viable for a package vending a library. From SE‐238:

Products that contain a target which uses an unsafe flag will be ineligible to act as a dependency for other packages.

If you can reduce your absolute paths down to simple library names like this example in the llbuild repository, then you can translate them into the package manifest like this. But if you cannot first remove the absolute paths from your script, then I do not think you can move them to the manifest either.

ImageMagick appears to be written in C. Since it’s licence appears to permit it, as long as you include the licence and proper attribution, you could probably copy the source of a particular release into a subdirectory of your package and set it up as a C target. That is one possibility for ridding yourself of the need for absolute paths.

@SDGGiesbrecht ooo - this example was super helpful. I embedded the MagickWand and MagickCore headers into my target's include dir, and now I'm down to just one command line param: swift build -Xlinker -L/usr/local/Cellar/imagemagick/7.0.8-42/lib Nice!

I can futher simplify it to swift build -Xlinker $(pkg-config --libs-only-L MagickWand) See this branch for updates: GitHub - danramteke/MagickBird at embedded-headers

However it looks like the cSettings package setting isn't working? I get the following warning when I build. "you should set MAGICKCORE_QUANTUM_DEPTH to sensible default set it to configure time default" The warning is only suppressed when I add -Xcc -DMAGICKCORE_QUANTUM_DEPTH=16 to the end of my swift build invocation.

When I add --verbose, I can see that the cSettings target setting only appears once (right after -fmodules-cache-path) however flags added using -Xcc appear twice (right after -fmodules-cache-path and again at the very end of the invocation after -color-diagnostics)

@ole thanks, as @SDGGiesbrecht mentioned, I would like to avoid unsafe flags.

I have not looked at it closely, but is this what you are running into? (from SE‐238 again)

Define (C/CXX)

[...]

Note: It is not recommended to use this setting for public headers as the target-specific settings are not imparted.

The linker flags are mostly intended for libraries that are a stable part of the operating system, whereas the C defines are mostly intended for C targets built from source within the package.

Since ImageMagick is not part of the OS, it may be more reliable to embed not just its headers, but basically the entire ImageMagick repository inside your package by copying the source from a stable release. As I said above, with proper attribution, it’s licence allows it. Here is an example which uses that same strategy to embed CMark. (While it is tempting to use a Git submodule instead of copying the files into the project, that runs into trouble once someone else is trying to use your package as a dependency.)

1 Like