Skip to content

Commit

Permalink
Add an arm64 macOS installer (#1201)
Browse files Browse the repository at this point in the history
* Add a separate build for an arm64 macOS installer. This has some minor differences from the x64 build

* Set DYLD_LIBRARY_PATH to fix library loading issues when running tests

* Add an argument for the runtime identifier to use for dotnet publish

* Update the bundle_gtk script to work with the arm64 homebrew install prefix

* Update readme and changelog

Fixes: #1198
  • Loading branch information
cameronwhite authored Jan 2, 2025
1 parent 6b4fbe1 commit ec1eccc
Show file tree
Hide file tree
Showing 5 changed files with 106 additions and 38 deletions.
52 changes: 49 additions & 3 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ jobs:
path: pinta-3.0.zip
if-no-files-found: error

build-macos:
build-macos-x86_64:
runs-on: macos-13

steps:
Expand Down Expand Up @@ -100,13 +100,59 @@ jobs:
MAC_DEV_PASSWORD: ${{ secrets.MAC_DEV_PASSWORD }}
run: |
cd installer/macos
./build_installer.sh
./build_installer.sh osx-x64
- name: Upload Installer
if: github.event_name != 'pull_request'
uses: actions/upload-artifact@v4
with:
name: "Pinta.dmg"
name: "Pinta-x86_64.dmg"
path: installer/macos/Pinta.dmg
if-no-files-found: error

build-macos-arm64:
# Note the macos-14 runner is arm64, while the macos-13 runner is Intel
runs-on: macos-14

steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{env.DOTNET_VERSION}}
- name: Install Dependencies
env:
# Work around webp-pixbuf-loader issue: https://github.com/Homebrew/homebrew-core/issues/139497
HOMEBREW_NO_INSTALL_FROM_API: 1
run: brew install libadwaita adwaita-icon-theme gettext webp-pixbuf-loader
- name: Build
run: dotnet build Pinta.sln -c Release
- name: Test
env:
# Add libraries from homebrew to the search path so they can be loaded by gir.core
DYLD_LIBRARY_PATH: "/opt/homebrew/lib"
run: dotnet test Pinta.sln -c Release

- name: Add Cert to Keychain
if: github.event_name != 'pull_request'
uses: apple-actions/import-codesign-certs@v3
with:
p12-file-base64: ${{ secrets.MAC_CERTS_BASE64 }}
p12-password: ${{ secrets.MAC_CERTS_PASSWORD }}

- name: Build Installer
if: github.event_name != 'pull_request'
env:
MAC_DEV_PASSWORD: ${{ secrets.MAC_DEV_PASSWORD }}
run: |
cd installer/macos
./build_installer.sh osx-arm64
- name: Upload Installer
if: github.event_name != 'pull_request'
uses: actions/upload-artifact@v4
with:
name: "Pinta-arm64.dmg"
path: installer/macos/Pinta.dmg
if-no-files-found: error

Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Thanks to the following contributors who worked on this release:
### Added
- Ported to GTK4 and libadwaita
- Upgraded the minimum required .NET version to 8.0
- Added an arm64 installer for macOS (Apple silicon)
- Restored support for add-ins, which had been disabled in Pinta 2.0 due to technical limitations
- Added a preference (in the `View` menu) for switching between a dark or light color scheme
- Added an improved color picker dialog (#570, #761, #1025)
Expand Down
13 changes: 11 additions & 2 deletions installer/macos/build_installer.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
#!/bin/sh
set -e

# Parse command line arguments
runtimeid=$1

if [ "$runtimeid" != "osx-x64" ] && [ "$runtimeid" != "osx-arm64" ]; then
echo "Invalid runtime identifier (should be osx-x64 or osx-arm64)"
echo "Usage: ./build_installer.sh runtimeid"
exit 1
fi

MAC_APP_DIR="$PWD/package/Pinta.app"
MAC_APP_BIN_DIR="${MAC_APP_DIR}/Contents/MacOS/"
MAC_APP_RESOURCE_DIR="${MAC_APP_DIR}/Contents/Resources/"
Expand All @@ -15,7 +24,7 @@ run_codesign()

mkdir -p ${MAC_APP_BIN_DIR} ${MAC_APP_RESOURCE_DIR} ${MAC_APP_SHARE_DIR}

dotnet publish ../../Pinta.sln -p:PublishDir=${MAC_APP_BIN_DIR} -p:BuildTranslations=true -c Release -r osx-x64 --self-contained true
dotnet publish ../../Pinta.sln -p:PublishDir=${MAC_APP_BIN_DIR} -p:BuildTranslations=true -c Release -r $runtimeid --self-contained true

# Remove stuff we don't need.
rm ${MAC_APP_BIN_DIR}/*.pdb
Expand All @@ -31,7 +40,7 @@ cp pinta.icns ${MAC_APP_DIR}/Contents/Resources

# Install the GTK dependencies.
echo "Bundling GTK..."
./bundle_gtk.py --resource_dir ${MAC_APP_RESOURCE_DIR}
./bundle_gtk.py --runtime $runtimeid --resource_dir ${MAC_APP_RESOURCE_DIR}
# Add the GTK lib dir to the library search path (for dlopen()), as an alternative to $DYLD_LIBRARY_PATH.
install_name_tool -add_rpath "@executable_path/../Resources/lib" ${MAC_APP_BIN_DIR}/Pinta

Expand Down
75 changes: 44 additions & 31 deletions installer/macos/bundle_gtk.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,18 @@
import subprocess
from stat import S_IREAD, S_IRGRP, S_IROTH, S_IWUSR

PREFIX = "/usr/local"

# Grab all dependencies of libadwaita / libgtk, plus pixbuf loader plugins.
GTK_LIB = "/usr/local/lib/libadwaita-1.0.dylib"
RSVG_LIB = "/usr/local/lib/librsvg-2.2.dylib"
TIFF_LIB = "/usr/local/lib/libtiff.6.dylib"
WEBP_DEMUX_LIB = "/usr/local/lib/libwebpdemux.2.dylib"
WEBP_MUX_LIB = "/usr/local/lib/libwebpmux.3.dylib"
GTK_LIB = "lib/libadwaita-1.0.dylib"
RSVG_LIB = "lib/librsvg-2.2.dylib"
TIFF_LIB = "lib/libtiff.6.dylib"
WEBP_DEMUX_LIB = "lib/libwebpdemux.2.dylib"
WEBP_MUX_LIB = "lib/libwebpmux.3.dylib"
ROOT_LIBS = [GTK_LIB, RSVG_LIB, TIFF_LIB, WEBP_DEMUX_LIB, WEBP_MUX_LIB]

ADWAITA_THEME = "/usr/local/share/icons/Adwaita/index.theme"
ADWAITA_THEME = "share/icons/Adwaita/index.theme"
PIXBUF_LOADERS = "lib/gdk-pixbuf-2.0/2.10.0"
GLIB_SCHEMAS = "share/glib-2.0/schemas"

# Match against non-system libraries
OTOOL_LIB_REGEX = re.compile(r"(/usr/local/.*\.dylib)")
# Match against relative paths (webp and related libraries)
OTOOL_REL_LIB_REGEX = re.compile(r"@rpath/(lib.*\.dylib)")


def run_install_name_tool(lib, deps, lib_install_dir):
# Make writable by user.
Expand All @@ -40,49 +33,49 @@ def run_install_name_tool(lib, deps, lib_install_dir):
dep_lib = "@executable_path/../Resources/lib/" + dep_lib_name
cmd = ['install_name_tool',
'-change', dep_path, dep_lib,
'-change', f"@rpath/{dep_path_basename}", dep_lib, # For libraries like webp
'-change', f"@rpath/{dep_path_basename}", dep_lib, # For libraries like webp
lib]
subprocess.check_output(cmd)


def collect_libs(src_lib, lib_deps):
def collect_libs(src_lib, lib_deps, otool_lib_regex, otool_rel_lib_regex):
"""
Use otool -L to collect the library dependencies.
"""
cmd = ['otool', '-L', src_lib]
output = subprocess.check_output(cmd).decode('utf-8')
referenced_paths = re.findall(OTOOL_LIB_REGEX, output)
referenced_paths = re.findall(otool_lib_regex, output)

folder = os.path.dirname(src_lib)
referenced_paths.extend([os.path.join(folder, lib)
for lib in re.findall(OTOOL_REL_LIB_REGEX, output)])
for lib in re.findall(otool_rel_lib_regex, output)])

real_lib_paths = set([os.path.realpath(lib) for lib in referenced_paths])

lib_deps[src_lib] = referenced_paths

for lib in real_lib_paths:
if lib not in lib_deps:
collect_libs(lib, lib_deps)
collect_libs(lib, lib_deps, otool_lib_regex, otool_rel_lib_regex)


def copy_resources(res_path):
def copy_resources(src_prefix, res_path):
"""
Copy a folder from ${PREFIX}/${res_path} to Contents/Resources/${res_path}.
"""
dest_folder = os.path.join(args.resource_dir, res_path)
shutil.copytree(os.path.join(PREFIX, res_path),
shutil.copytree(os.path.join(src_prefix, res_path),
dest_folder,
dirs_exist_ok=True)


def copy_plugins(res_path, lib_install_dir):
def copy_plugins(src_prefix, res_path, lib_install_dir, otool_lib_regex, otool_rel_lib_regex):
"""
Copy a folder of plugins from ${PREFIX}/${res_path} to
Contents/Resources/${res_path} and update the library references.
"""

copy_resources(res_path)
copy_resources(src_prefix, res_path)

# Update paths to the main GTK libs.
lib_install_dir = os.path.join(args.resource_dir, 'lib')
Expand All @@ -94,17 +87,18 @@ def copy_plugins(res_path, lib_install_dir):

lib_path = os.path.join(root, lib)
lib_deps = {}
collect_libs(lib_path, lib_deps)
collect_libs(lib_path, lib_deps, otool_lib_regex,
otool_rel_lib_regex)
run_install_name_tool(lib_path, lib_deps[lib_path],
lib_install_dir)


def install_plugin_cache(cache_path, resource_dir):
def install_plugin_cache(src_prefix, cache_path, resource_dir):
"""
Copy a file such as immodules.cache, and update the library paths to be
paths inside the .app bundle.
"""
src_cache = os.path.join(PREFIX, cache_path)
src_cache = os.path.join(src_prefix, cache_path)
dest_cache = os.path.join(resource_dir, cache_path)

with open(src_cache, 'r') as src_f:
Expand All @@ -117,15 +111,31 @@ def install_plugin_cache(cache_path, resource_dir):


parser = argparse.ArgumentParser(description='Bundle the GTK libraries.')
parser.add_argument('--runtime', type=str, required=True,
help='The dotnet runtime id, e.g. osx-x64 or osx-arm64')
parser.add_argument('--resource_dir',
type=pathlib.Path,
required=True,
help='Directory to copy extra resources to.')
args = parser.parse_args()

src_prefix = ""
if args.runtime == "osx-x64":
src_prefix = "/usr/local"
elif args.runtime == "osx-arm64":
src_prefix = "/opt/homebrew"
else:
raise RuntimeError("Invalid runtime id")

# Match against non-system libraries
otool_lib_regex = re.compile(fr"({src_prefix}/.*\.dylib)")
# Match against relative paths (webp and related libraries)
otool_rel_lib_regex = re.compile(r"@rpath/(lib.*\.dylib)")

lib_deps = {}
for root_lib in ROOT_LIBS:
collect_libs(os.path.realpath(root_lib), lib_deps)
lib_path = os.path.realpath(os.path.join(src_prefix, root_lib))
collect_libs(lib_path, lib_deps, otool_lib_regex, otool_rel_lib_regex)

lib_install_dir = os.path.join(args.resource_dir, 'lib')
os.makedirs(lib_install_dir)
Expand All @@ -139,18 +149,21 @@ def install_plugin_cache(cache_path, resource_dir):
os.path.join(lib_install_dir, "libgdk_pixbuf-2.0.dylib"))

# Copy translations and icons.
gtk_root = os.path.join(os.path.dirname(os.path.realpath(GTK_LIB)), "..")
gtk_root = os.path.join(os.path.dirname(
os.path.realpath(os.path.join(src_prefix, GTK_LIB))), "..")
shutil.copytree(os.path.join(gtk_root, 'share/locale'),
os.path.join(args.resource_dir, 'share/locale'),
dirs_exist_ok=True)
# TODO - could probably trim the number of installed icons.
adwaita_icons = os.path.join(os.path.dirname(os.path.realpath(ADWAITA_THEME)), "..")
adwaita_icons = os.path.join(os.path.dirname(
os.path.realpath(os.path.join(src_prefix, ADWAITA_THEME))), "..")
shutil.copytree(adwaita_icons,
os.path.join(args.resource_dir, 'share/icons'),
dirs_exist_ok=True)

copy_plugins(PIXBUF_LOADERS, lib_install_dir)
install_plugin_cache(os.path.join(PIXBUF_LOADERS, "loaders.cache"),
copy_plugins(src_prefix, PIXBUF_LOADERS, lib_install_dir,
otool_lib_regex, otool_rel_lib_regex)
install_plugin_cache(src_prefix, os.path.join(PIXBUF_LOADERS, "loaders.cache"),
args.resource_dir)

copy_resources(GLIB_SCHEMAS)
copy_resources(src_prefix, GLIB_SCHEMAS)
3 changes: 1 addition & 2 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,12 @@ For building on the command line:

- Install .NET 8 and GTK4
- `brew install dotnet-sdk libadwaita adwaita-icon-theme gettext webp-pixbuf-loader`
- For Apple Silicon, set `DYLD_LIBRARY_PATH=/opt/homebrew/lib` in the environment so that Pinta can load the GTK libraries
- Build:
- `dotnet build`
- Run:
- `dotnet run --project Pinta`

Alternatively, Pinta can be built by opening `Pinta.sln` in [Visual Studio for Mac](https://visualstudio.microsoft.com/vs/mac/).

## Building on Linux

- Install [.NET 8](https://dotnet.microsoft.com/) following the instructions for your Linux distribution.
Expand Down

0 comments on commit ec1eccc

Please sign in to comment.