iPhoto and Aperture running on macOS Catalina.

Technical Deep Dive: How does Retroactive work?

This is the technical backstory of Retroactive.

Tyshawn Cormier

--

Retroactive only receives limited support. You should transition from Retroactive to a wide range of supported apps, many of which are built into macOS or free to download. Learn how to transition from Retroactive to supported apps.

If you need to run Aperture, iPhoto, or iTunes on macOS Big Sur or macOS Catalina, download the Retroactive app.

This article is not a manual, but a technical deep dive on how the first version of Retroactive works. Many improvements, such as compatibility with macOS Big Sur, has been made since this article was written. These improvements are available in the latest version of the Retroactive app.

After saying goodbye to my perpetual copy of Photoshop CS6, I took the plunge and updated my Mac to macOS Catalina this Wednesday. The update went smoothly except for one little surprise.

While I moved my personal library to Photos years ago, I preferred organizing my clients’ photos in Aperture. I’m also still debating between Capture One, Adobe Lightroom Classic CC, and Adobe Lightroom CC, so I stuck with Aperture for a little longer. (Why are there two parallel versions of Adobe Lightroom? Which one should I use?)

iPhoto and Aperture no longer launch on macOS Catalina.

What I missed is with 32-bit support gone, iPhoto and Aperture no longer launch. That’s a little strange. System Information definitely reported both of those apps as 64-bit.

Luckily, St. Clair Software’s excellent Go64 tells us more.

Inspecting 32-bit components in iPhoto and Aperture with St. Clair Software’s Go64 app

What broke Aperture and iPhoto? Can their lifeline be extended just a teensy longer? The answer is yes. Both of them can run on macOS Catalina with a little bit of magic🧚‍♂️.

A Tale of the Brave Pioneers

It’s no news that the brave and the reckless have been able to monkey-patch or shoehorn macOS into running apps under completely unsupported configurations. A rundown of the brave heroes.

2011

2013

2015

2017

2018

2019

Run Aperture on macOS Catalina

Hint: It’s a five-fold process.

Prerequisite: Get Aperture if you have already deleted it.

Open Mac App Store and click on your avatar at the bottom-left corner. Download Aperture and iPhoto from your list of purchased apps.

Step 1. Get around the Info.plist check for Aperture by changing its bundle identifier.

To make things work, we have to start somewhere. A Google search shows Exceptions.plist in CoreTypes.bundle is responsible for what is and what is not allowed to launch in macOS.

/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/Exceptions.plist, opened in BBEdit

Found it! Instead of editing this file to flip the <true/> flag (which would require disabling System Integrity Protection and mounting the system volume as read-write), we can use a safer approach that doesn’t require disabling System Integrity Protection:

Because we know macOS blocks Aperture from launching based on checking for its bundle identifier, the safer approach is to directly edit Aperture’s Info.plist and changing its bundle identifier into something else.

Go to the Applications folder in Finder. Right-click on Aperture.app, and choose “Show Package Contents”. Drill down into the “Contents” folder, and open Info.plist in BBEdit.

Appending a digit ‘3’ at the end of ‘com.apple.Aperture’ and saving the file should suffice.

Appending digit ‘3’ at the end of the original bundle identifier ‘com.apple.Aperture’

Try to launch Aperture. If GateKeeper prevents you from running modified versions of Aperture or iPhoto, temporarily disable GateKeeper in Terminal.

sudo spctl --master-disable

If everything goes well, we should a different error message! “Aperture quit unexpectedly while using the plug-in”.

“Aperture quit unexpectedly while using the plug-in.”

Clicking on “Report…” shows the problem report, which has some useful detail.

Problem report: Aperture crashes because of a code signing error.

That looks like some codesigning error. Solution: just remove the broken code signature. Opening Terminal and run:

sudo codesign --remove-signature /Applications/Aperture.app

Launch Aperture again. We’re good to g… see a different crash?

“Aperture cannot be opened because of a problem.”
“Aperture cannot be opened because of a problem.”

This time, it’s crashing because macOS can’t find /Library/Frameworks/NyxAudioAnalysis.framework. Presumably, this framework has been removed in macOS Catalina.

Step 2. Get a copy of the missing NyxAudioAnalysis.framework from macOS Mojave, then use install_name_tool to change the framework path.

To proceed, we need to get NyxAudioAnalysis.framework from macOS Mojave. Because I upgraded from macOS Mojave to macOS Catalina, I don’t have that framework file anymore.

Luckily, it should be possible to extract NyxAudioAnalysis.framework from the macOS Mojave installer. A trip to the Apple Support website led me to “How to upgrade to macOS Mojave”, which has a helpful link to the macOS Mojave installer in the Mac App Store.

Download the macOS Mojave installer from the Mac App Store.

Downloading the installer took a few minutes. Patience is a virtue.

Control-click on the Install macOS Mojave.app, and choose to Show Package Contents. Locate the largest file in the app bundle, which is InstallESD.dmg, then double click to mount it.

InstallESD.dmg from Install macOS Mojave.app

Drill down the mounted disk image to find the largest package, which is Core.pkg, and open it in the Pacifist app (if you don’t already have Pacifist, install it first).

Core.pkg in mounted InstallESD.dmg

We’re looking for the missing /Library/Frameworks/NyxAudioAnalysis.framework. Hmm, it’s a symbolic link to somewhere else. Where though?

Core.pkg in Pacifist app

The search field comes to the rescue! The actual framework is located at /System/Library/PrivateFrameworks/NyxAudioAnalysis.framework.

A quick search reveals the true location of NyxAudioAnalysis.framework

Extract NyxAudioAnalysis.framework to the Desktop! (It may take a while even though we’re only extracting one framework. Pacifist needs to traverse through all files in the package just to get to this one framework.)

Then, copy NyxAudioAnalysis.framework into /Applications/Aperture.app/Contents/Frameworks.

Just copying the framework over isn’t enough. Aperture will still look for NyxAudioAnalysis.framework at the old location. It’s time to use install_name_tool to tell Aperture NyxAudioAnalysis.framework’s new location.

Open Terminal, and change the library path for NyxAudioAnalysis.framework.

sudo install_name_tool -change "/Library/Frameworks/NyxAudioAnalysis.framework/Versions/A/NyxAudioAnalysis" "@executable_path/../Frameworks/NyxAudioAnalysis.framework/Versions/A/NyxAudioAnalysis" /Applications/Aperture.app/Contents/Frameworks/iLifeSlideshow.framework/Versions/A/iLifeSlideshow
Changing the library path for NyxAudioAnalysis.framework using install_name_tool.

💡Alternatively, if you look through the man page of dyld, you’ll find a helpful environment variable called DYLD_FALLBACK_FRAMEWORK_PATH. Using environment variables to load the library is another valid approach to achieve the same result as using install_name_tool.

Step 3. Launch Aperture, wait till it crashes. Look through crash reports to identify broken methods and missing selectors.

To verify if we actually made progress, let’s launch Aperture again.

How did it go? Aperture has attempted to launch. We know this because the menu bar showed up for a split second before crashing. We’ve gotten further than before, so that’s a good sign.

Aperture crashes again, but we have gotten further than before.

The stack trace is fascinating. It looks like some sort of access violation when initializing a font. We are going to fix this by swizzling Aperture.

0 com.apple.CoreText 0x00007fff3c6a2019 CTFontGetClientObject + 13
1 com.apple.UIFoundation 0x00007fff6b37bcfc __UIFontGetExtraData + 18
2 com.apple.UIFoundation 0x00007fff6b37cd67 -[NSFont initWithTypefaceInfo:key:renderingMode:] + 47
3 com.apple.prokit 0x0000000106955159 +[NSProFont _proSystemFontWithFontName:pointSize:fontAppearance:useSystemHelveticaAdjustments:] + 736
4 com.apple.prokit 0x00000001069b553e -[NSCell(ProAppearanceExtensions) proSetFont:] + 155
5 com.apple.AppKit 0x00007fff37a4bc03 -[NSTextFieldCell setFont:] + 47
6 com.apple.Aperture3 0x0000000105f14cc2 0x105c9e000 + 2583746
7 com.apple.AppKit 0x00007fff37a9dec7 -[NSTextFieldCell init] + 31
8 com.apple.Aperture3 0x0000000105f12901 0x105c9e000 + 2574593
9 com.apple.Aperture3 0x0000000105f22bf4 0x105c9e000 + 2640884
10 com.apple.Aperture3 0x0000000105f1636d 0x105c9e000 + 2589549
11 com.apple.AppKit 0x00007fff379fdb02 -[NSIBObjectData nibInstantiateWithOwner:options:topLevelObjects:] + 1540
12 com.apple.AppKit 0x00007fff37b66b37 -[NSNib _instantiateNibWithExternalNameTable:options:] + 647
13 com.apple.AppKit 0x00007fff37b667bb -[NSNib _instantiateWithOwner:options:topLevelObjects:] + 143
14 com.apple.AppKit 0x00007fff37b65b08 -[NSViewController loadView] + 345
15 com.apple.AppKit 0x00007fff37b61b88 -[NSViewController _loadViewIfRequired] + 72
16 com.apple.AppKit 0x00007fff37b61b05 -[NSViewController view] + 23
17 com.apple.Aperture3 0x0000000105cf30ed 0x105c9e000 + 348397
18 com.apple.AppKit 0x00007fff37b601e0 -[NSWindowController _windowDidLoad] + 624
19 com.apple.AppKit 0x00007fff37ad3718 -[NSWindowController window] + 110

Step 4. Create a new framework in Xcode to swizzle broken methods in Aperture, and fill in selectors that have been removed.

It’s swizzle time! How do we swizzle an existing app? Of course, we use Xcode.

If you don’t have Xcode yet, download Xcode, then create a new Xcode project with a macOS Framework template.

Make sure the language is Objective-C.

Create a new file.

Create an Objective-C file.

The easiest way to swizzle something is just to make a category on NSObject and call method_exchangeImplementations in +load. I’ll name this file “Swizzling”, but you can name it whatever you want.

If we want to swizzle the problematic initializer on NSProFont, it’s time to put on our (NS)Hipster hat and revise our swizzling skills.

Import the Objective-C runtime header, and let the fun begin.

import <objc/runtime.h>

Refer back to the problem report. Here’s the suspect of the crash:

3 com.apple.prokit 0x0000000106955159 +[NSProFont _proSystemFontWithFontName:pointSize:fontAppearance:useSystemHelveticaAdjustments:] + 736

Start with the most obvious idea: what if we just initialize a regular system font instead of whatever special pro font Aperture is trying (and failing) to initialize?

+ (NSFont *)swizzled_proSystemFontWithFontName:(NSString *)name pointSize:(CGFloat)size fontAppearance:(id)appearance useSystemHelveticaAdjustments:(BOOL)adjustments {
return [NSFont systemFontOfSize:size];
}

Then use the dynamic Objective-C runtime to exchange Aperture’s implementation with ours.

+ (void)load {
method_exchangeImplementations(class_getClassMethod(NSClassFromString(@"NSProFont"), NSSelectorFromString(@"_proSystemFontWithFontName:pointSize:fontAppearance:useSystemHelveticaAdjustments:")), class_getClassMethod([self class], @selector(swizzled_proSystemFontWithFontName:pointSize:fontAppearance:useSystemHelveticaAdjustments:)));
}

Some wishful thinking: If initializing the font doesn’t fail, this can probably get us over to the next step.

Press Command+B. Build succeeded!

New question: How do you run Aperture with this library? Right-click on the build product, then show it in Finder.

Remember the man page of dyld I called out earlier? That’s handy now.

In Terminal, use dyld’s environment variable DYLD_INSERT_LIBRARIES to insert our newly built framework.

Type “DYLD_INSERT_LIBRARIES=”, and drag in the framework we have just built. After that, drag in the Aperture app binary (you can get to it through right-clicking on the Aperture app in Finder, then choose “Show Package Contents”, then continue to drill down in subfolders as shown). Press return when you’re done.

Launch Aperture in Terminal with SwizzlingLibrary as an inserted library.

Now Aperture launches 🎉, but it’s stuck… forever launching. Aperture would either show nothing (or a window frozen at “Opening Aperture Library”, purely depending on luck). In Terminal, a very suspicious unrecognized selector jumps out:

2019–10–13 19:17:46.331 Aperture[52455:1100426] -[RKSourceListOutlineView _hasRowHeaderColumn]: unrecognized selector sent to instance 0x7ff9668c2800
A very suspicious unrecognized selector in Terminal.

The obvious next step is to implement that selector. There are two ways to go about this: the proper way is to implement it on a new RKSourceListOutlineView category.

Another really quick-and-dirty way is to just implement it on the NSObject we already created. Since every class including RKSourceListOutlineView inherits from NSObject, that should do the job just fine. (Bad things™ will happen if multiple classes implement this method, so proceed with caution.)

- (BOOL)_hasRowHeaderColumn { return NO; }
No row will have a header column.

Press Command+B to build again. Go back to Terminal, and repeat what we did before to launch Aperture (with the DYLD_INSERT_LIBRARIES environment variable).

Launch Aperture in Terminal with SwizzlingLibrary as an inserted library.

And just like magic, Aperture launches and works. For the first time in forever, Aperture launches on macOS Catalina! Browsing through the library, making adjustments to photos, and exporting photos all work.

For the first time in forever, Aperture runs on macOS Catalina.

Using Aperture for one more minute, we notice that some specific workflows can crash Aperture. One of them is trying to open the “File” menu. That’s not good. Luckily, the problem report tells us why.

Problem report after Aperture crashes.

The “Open Library in iPhoto” menu item can’t be validated because Aperture passed a nil bundle identifier to -[NSWorkspace URLForApplicationWithBundleIdentifier:].

The cure is to just swizzle the method and pass in the bundle identifier of iPhoto, “com.apple.iPhoto” if the supplied argument is nil.

Aperture also crashes when viewing the Accounts tab in Preferences. Aperture crashes because selector -[NSProTableHeaderView _addToolTipRects:] is missing. Tooltips are helpful, but they surely aren’t essential. A no-op stub should suffice.

Going back to Xcode, let’s stub _addToolTipRects with a no-op as promised.

- (void)_addToolTipRects { }
Stub -_addToolTipRects.

We also need to swizzle -[NSWorkspace URLForApplicationWithBundleIdentifier:] to pass in iPhoto’s bundle identifier if the existing identifier is nil.

First, implement the method we’ll use for swizzle to. If the bundle identifier is nil, just pass in the bundle identifier of iPhoto.

- (NSURL *)swizzled_URLForApplicationWithBundleIdentifier:(NSString *)bundleIdentifier {
return [self swizzled_URLForApplicationWithBundleIdentifier:bundleIdentifier ?: @"com.apple.iPhoto"];
}

💡This looks recursive but isn’t recursive at all! When swizzling, there’s no superclass for us to call. To call the original method, call the new swizzled selector.

Implement the swizzled method.

Then actually swizzle that NSWorkspace method in question at the end of +load.

method_exchangeImplementations(class_getInstanceMethod(NSClassFromString(@"NSWorkspace"), NSSelectorFromString(@"URLForApplicationWithBundleIdentifier:")), class_getInstanceMethod([self class], @selector(swizzled_URLForApplicationWithBundleIdentifier:)));

Similarly, we can also fix crashes when printing. In +load, swizzle the suspects.

method_exchangeImplementations(class_getInstanceMethod(NSClassFromString(@"RKPrintPanel"), NSSelectorFromString(@"_updatePageSizePopup")), class_getInstanceMethod(NSClassFromString(@"RKPrintPanel"), @selector(patched_updatePageSizePopup)));method_exchangeImplementations(class_getInstanceMethod(NSClassFromString(@"NSConcretePrintOperation"), NSSelectorFromString(@"runOperation")), class_getInstanceMethod(class, @selector(patched_runOperation)));method_exchangeImplementations(class_getInstanceMethod(NSClassFromString(@"RKPrinter"), NSSelectorFromString(@"paperWithID:")), class_getInstanceMethod(class, @selector(patched_paperWithID:)));

And implement the swizzled methods.

- (id)patched_paperWithID:(id)arg1 {
NSLog(@"patching paperWithID to prevent crashes");
return nil;
}
- (void)patched_runOperation {
NSLog(@"patching runOperation to run on main queue to prevent Auto Layout crashes");
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"running operation on main queue");
[self patched_runOperation];
});
}
- (void)patched_updatePageSizePopup {
NSLog(@"skipping updatePageSizePopup to prevent a crash");
}

The “Add Adjustment” and “Quick Brushes” menus are often empty. Fix in +load.

method_exchangeImplementations(class_getInstanceMethod(NSClassFromString(@"RKRedRockApp"), NSSelectorFromString(@"_delayedFinishLaunching")), class_getInstanceMethod(class, @selector(patched_delayedFinishLaunching)));

And implement the swizzled methods.

- (void)patched_delayedFinishLaunching {
NSLog(@"kick delayedFinishLaunching to the next run loop, so that the adjustments menu can populate");
[self performSelector:@selector(patched_delayedFinishLaunching) withObject:nil afterDelay:0];
}

Build our swizzling library with Command+B. Run Aperture again with the same Terminal command as before.

Launch Aperture in Terminal with SwizzlingLibrary as an inserted library.

Using Aperture’s File menu and logging into accounts in Preferences should both work now.

Horray!

Wait a minute… this is all great, but nobody would want to run a Terminal command just to launch Aperture. Let’s fix that!

Step 5. Use Asger Hautop Drewsen’s insert_dylib tool to automatically load our newly-built framework before Aperture launches.

Clone insert_dylib from Github. If you’re not familiar with Git, choose “Clone or download”, and download it as a ZIP file. Extract the ZIP file.

Double click to open insert_dylib.xcodeproj in Xcode. Press Command+B to build it through Xcode. Right click on the build product insert_dylib binary, then show it in Finder.

Build insert_dylib, and then Show in Finder

Install it by copying the your newly-built “insert_dylib” binary into /usr/local/bin. You can get to /usr/local/bin by clicking on the “Go to Folder…” menu command under the “Go” menu.

Go back to the SwizzlingLibrary Xcode project we’ve already created. Reveal the product SwizzlingLibrary.framework in Finder.

Copy the entire SwizzlingLibrary.framework directory from your build directory into Aperture’s own Frameworks folder /Applications/Aperture.app/Contents/Frameworks.

Open Terminal, and put both insert_dylib and our SwizzlingLibrary to the test! (💡: Make a backup of the Aperture binary if you want to be extra cautious. If you’ve messed up Aperture in any part of the process, delete Aperture and re-download it from the Mac App Store.)

sudo insert_dylib @executable_path/../Frameworks/SwizzlingLibrary.framework/Versions/A/SwizzlingLibrary /Applications/Aperture.app/Contents/MacOS/Aperture --inplace

Everything should settle down now. To wrap up the whole package, re-sign the Aperture app with ad-hoc signing in Terminal.

sudo codesign -fs - /Applications/Aperture.app --deep

Launch Aperture directly from Launchpad, Finder or your Dock. No need to launch it through Terminal anymore!

Aperture works just like before!

Aperture running on macOS Catalina.

Encore: Run iPhoto on macOS Catalina

Getting iPhoto to run is mostly the same as getting Aperture to run. There are a few differences to keep in mind.

  • When changing the bundle identifier in iPhoto’s Info.plist, change “com.apple.iPhoto” into “com.apple.iPhoto9”.
  • When removing code signature, run:
sudo codesign --remove-signature /Applications/iPhoto.app
  • Copy NyxAudioAnalysis.framework into /Applications/iPhoto.app/Contents/Frameworks.
  • When using install_name_tool to change the framework path, use the following command instead:
sudo install_name_tool -change "/Library/Frameworks/NyxAudioAnalysis.framework/Versions/A/NyxAudioAnalysis" "@executable_path/../Frameworks/NyxAudioAnalysis.framework/Versions/A/NyxAudioAnalysis" /Applications/iPhoto.app/Contents/Frameworks/iLifeSlideshow.framework/Versions/A/iLifeSlideshow
  • Important: The library we have already made for Aperture will work just as well for iPhoto except for one small change: You need to disable Objective-C Automatic Reference Counting in the “Build Settings” tab. After disabling ARC, re-build the library again for iPhoto.
  • (Optional): To make the “Open in iPhoto”/“Open in Aperture” menu item pair work properly, pass in “com.apple.iPhoto9” for Aperture, “com.apple.Aperture3” for iPhoto.
  • Copy SwizzlingLibrary.framework to /Applications/iPhoto.app/Contents/Frameworks just like before in Aperture.
  • When inserting our swizzling library into iPhoto, run the command with iPhoto’s path:
sudo insert_dylib @executable_path/../Frameworks/SwizzlingLibrary.framework/Versions/A/SwizzlingLibrary /Applications/iPhoto.app/Contents/MacOS/iPhoto --inplace
  • When performing an ad-hoc signing of iPhoto.app, run:
sudo codesign -fs - /Applications/iPhoto.app --deep
iPhoto running on macOS Catalina.

Reprocess RAW Photos

  • If your RAW photos show up as “Unsupported Image Format”, open the “Photos” menu, click on “Reprocess original…”, and reprocess all photos. After reprocessing your RAW photos, you will be able to preview and adjust them as before.

💡 Tip:

  • If some RAW photos still show up as “Unsupported Image Format” after reprocessing, repeat the process above to reprocess all photos again. In other words, you may need to reprocess all photos twice.
Reprocessing Original RAW Photos

Small Gotchas

If you look carefully at the screenshots in the prelude section, Go64 has revealed iPhoto and Aperture containing 32-bit components. Both apps link to QTKit, too.

macOS Catalina 10.15 Release Notes mentions “the symbols for QTKit, which relied on the QuickTime framework, are still present but the classes are non-functional.”

In practice, this means all Aperture and iPhoto features are available except for playing videos, exporting slideshows, Photo Stream, and iCloud Photo Sharing.

--

--

Tyshawn Cormier

Independent IT consultant from Watford City, North Dakota.