How to create a C or C++ plugin that will work on MacOS and workarounds

This guide is an evolving description on how to build, create and publish plugins on MacOS. This should be paired with general guidance on building GIMP plugins.

Environment

To build your plug-in with the dev package, first set some environment variables

Set up to support all OSs that GIMP supports

Currently SDK version for x86_64 is same as arm64. This could change. Check https://gitlab.gnome.org/Infrastructure/gimp-macos-build/-/blob/master/scripts/macports0_install.sh?ref_type=heads for updates.

SDK_VERSION=11.3
SDK_MAJOR_VERSION=$(echo $SDK_VERSION | cut -d. -f1)
cd /Library/Developer/CommandLineTools/SDKs
if [ ! -d "MacOSX${SDK_VERSION}.sdk" ]; then
  sudo curl -L "https://github.com/phracker/MacOSX-SDKs/releases/download/11.3/MacOSX${SDK_VERSION}.sdk.tar.xz" | sudo tar -xzf -
fi
if [ -L "MacOSX${SDK_MAJOR_VERSION}.sdk" ]; then
  sudo rm "MacOSX${SDK_MAJOR_VERSION}.sdk"
fi
sudo ln -s "MacOSX${SDK_VERSION}.sdk" "MacOSX${SDK_MAJOR_VERSION}.sdk"

export SDKROOT=/Library/Developer/CommandLineTools/SDKs/MacOSX${SDK_VERSION}.sdk
SDK=" -isysroot $SDKROOT" # <- use this if you want to use a specific SDK
export GIMPDIR=/Applications/GIMP.app
export OTHER_PREFIX=/path to top folder of another optional prefix # <- your plug-in might need other includes or libraries which might be in another build prefix
export MACOSX_DEPLOYMENT_TARGET=11.0 # <- this sets the same deployment target The GIMP uses
export PATH=$GIMPDIR/Contents/Macos/:$OTHER_PREFIX/bin:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin # <- Be careful about not pulling in homebrew libraries
export CC="/usr/bin/clang"
export CXX="/usr/bin/clang++"
export CPPFLAGS='-I'$GIMPDIR'/Contents/Resources/include -I'$OTHER_PREFIX'/include'$SDK
export CFLAGS='-I'$GIMPDIR'/Contents/Resources/include -I'$OTHER_PREFIX'/include'$SDK
export CXXFLAGS='-I'$GIMPDIR'/Contents/Resources/include -I'$OTHER_PREFIX'/include'$SDK
export LDFLAGS='-L'$GIMPDIR'/Contents/Resources/lib -I'$OTHER_PREFIX'/lib -headerpad_max_install_names'$SDK
export PKG_CONFIG_PATH=$GIMPDIR/Contents/Resources/lib/pkgconfig:$OTHER_PREFIX/lib/pkgconfig
export LIBTOOLFLAGS="--silent"

For x86_64 cross compile use these:

export GIMPDIR=/Applications/GIMP.app # <- change to where the x86_64 app bundle is stored
export CC="/usr/bin/clang -arch x86_64"
export CXX="/usr/bin/clang++ -arch x86_64"

Notes on some environment variables:

PATH <- IMPORTANT: somewhere in the given folders you must have pkg-config installed. Most everything will fail without pkg-config.

SDK <- use this if you want to use a specific SDK, otherwise just don’t set it, the follow up variables work fine when it isn’t set or empty.

OTHER_PREFIX <- your plug-in might need other includes or libraries which might be in another build prefix

MACOSX_DEPLOYMENT_TARGET <- this sets the same deployment target The GIMP uses, I advise to use the same, unpredictable issues might arise if you don’t match with the The GIMP and its libraries.

headerpad_max_install_names <- pads the header by enough bytes so that the dylib ID and loaded dylib paths can all be extended to MAXPATHLEN.

Autotools

Run whatever you need to run to get your build ready to be build.

In my case:

cd /path_to/our_code
autoreconf -i
./configure --enable-gimp-plugin=yes
make

meson, cmake:**

TBD: would need to test with a simple plug-in that utilizes these

rpath

Using this dev package will make your plug-in look for its dylib dependencies in @rpath/lib/, so you need to add an rpath to your plug-in. Run:

install_name_tool -add_rpath /Applications/GIMP.app/Contents/Resources/ /path_to/plug-in

As this is the default GIMP application name, adding it makes most sense, but you can use another path, and most importantly, you can add more than one rpath (just run the above command with another path).

Bundling x86_64 and arm64 build

If you want to make your plug-in available for both x86_64 and arm64 macOS you can build them individually and then “glue” the two binaries together. After building the two binaries run:

lipo -create -arch x86_64 /path_to/x86_64/plug-in -arch arm64 /path_to/arm64/plug-in -output /path_to/where_you_want_it_stored/plug-in

Codesigning

To codesign run the below command. Replace "Developer ID Application" with your developer ID (you need a paid developer account). You will need to codesign with hardened runtime which will also require you to point to an entitlements.plist file. This is the file that GIMP uses, which may or may not be right for a plugin: Hardening entitlements. Codesigning is only needed for distributing your plug-in.

codesign -s "Developer ID Application" \
    --timestamp \
    --options runtime \
    --entitlements /path_to/entitlements.plist \
    /path_to/plug-in

If you do not properly codesign your plugin, it cannot be notarized. And if it is not notarized, it will need to be unquarantined on the end user’s macOS. In that case the end user will need to run:

sudo xattr -rd com.apple.quarantine /path_to/plug-in

Note: This is a security risk and users without admin rights will not be able to do this anyway, so this should not the a recommended approach.

Creating a PKG installer:

The seemingly best way to install your plugin is using a PKG package. Unfortunately your plugin needs to be installed in the user’s home folder structure (~/Library/Application\ Support/GIMP/3.0/plug-ins/plug-in_name) and this presents two problems (that can be solved):

  1. You cannot pass ~/ (or $HOME/, or /users/$USER/) to pkgbuild’s --install-location arg as it only accepts absolute paths. If you try anyway this will result in several problems but will never result in the plug-in being in the right folder on your end user’s macOS. This can be resolved by using a script.
  2. By default the install will use Admin rights and the plug-in will end up being owned by root. This can be resolved by using a distributions.xml in a productbuild step.

Steps:

  • create a folder structure in folder called package, with your plugin in a subfolder named plugin
  • in another subfolder called script place a file called postinstall with the following content
#!/bin/zsh
mkdir -p $HOME/Library/Application\ Support/GIMP/3.0/plug-ins/plug-in_name
cp -pf /tmp/gimp_plugin/* $HOME/Library/Application\ Support/GIMP/3.0/plug-ins/plug-in_name

This is a very basic script and could be made more elaborate. Change plug-in_name appropriately.

  • change the file script/postinstall to be executable (chmod +x ./script/postinstall)
  • create a file called distributions.xml in the folder package with the content:
<?xml version="1.0" encoding="utf8"?>
<installer-gui-script minSpecVersion="2">
 <title>MY AWESOME Plug-in for The GIMP 3.0</title>>
 <options customize="never"/>
 <domains enable_anywhere="false" enable_currentUserHome="true" enable_localSystem="false"/>
 <choices-outline>
  <line choice="plug-in"/>
 </choices-outline>
 <choice id="plug-in" visible="false" customLocation="/tmp/gimp_plugin">
  <pkg-ref id="domainending.domain.plugin_name.pkg" version="0" onConclusion="none">plugin.pkg</pkg-ref>
 </choice>
</installer-gui-script>
  • in terminal cd to the package folder
  • run pkgbuild --nopayload --identifier domainending.domain.plugin_name.pkg --scripts ./script --root ./plugin/ plugin.pkg
  • run
productbuild --distribution ./distribution.xml \
 --package-path plugin.pkg \
 ./plugin_name.pkg
  • you can now delete the plugin.pkg, it was only a temporary file.

If you want to codesign the pkg, run the productbuild command with these args instead:

productbuild --distribution ./distribution.xml \
 --sign "Developer ID Installer: YOUR NAME" --timestamp \
 --package-path plugin.pkg \
 ./plugin_name.pkg

Please take note that you need the Installer certificate for the pkg, not the Application one.

Short breakdown of what this all does:

Pkgbuild packages your plugin and sets up the script. Productbuild uses this package, sets it to run as the installing user (enable_currentUserHome="true") and to install the plugin to a temporary folder /tmp/gimp_plugin.

When you run the installer, the plugin gets installed to /tmp/gimp_plugin and the script copies it over to $HOME/Library/Application\ Support/GIMP/3.0/plug-ins/plug-in_name.

Notarization

To notarize your plug-in with Apple (seemingly more security but same as codesigning it is only necessary for distributing your plug-in), package your codesigned plug-in either in a zip file, a disk image (DMG) or apackage (PKG) and run:

xcrun notarytool submit --wait --keychain-profile 'notarize' /path_to/plugin_name.pkg

Once this succeeds staple the notarization to the package

xcrun stapler staple /path_to/plugin_name.pkg

Note: This will require a certain amount of setup (maybe it’s already done with the certificates you used for codesigning) but follow Apple’s instructions. In our experience, this is also quite fiddley.

Note: It is possible, when distributing, that even after all of this, the file will not be accepted on the user’s MacOS. In our experience this is typically because one of the executables or libraries is linking to a library outside the package. However, if this was the case for plugins, then they would never work because they have to link to the GIMP libraries.

GIMP code signing

Here is links to information on the code that does code signing for GIMP. Look at

An Apple Developer account and the right credentials are also needed.