Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ build/

# clangd compile database (symlink into build/)
compile_commands.json
.playwright-mcp/
21 changes: 14 additions & 7 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -378,23 +378,30 @@ if(APPLE)
FetchContent_Declare(sparkle
URL https://github.com/sparkle-project/Sparkle/releases/download/2.6.4/Sparkle-2.6.4.tar.xz)
FetchContent_MakeAvailable(sparkle)
target_sources(${PROJECT_NAME} PRIVATE src/app/AutoUpdaterSparkle.mm)
# the C++ PCH is built with Objective-C disabled; reusing it for this
# Objective-C++ source fails ("Objective-C was disabled in precompiled
# file ... but is currently enabled"), so skip the PCH for it
set_source_files_properties(src/app/AutoUpdaterSparkle.mm
target_sources(${PROJECT_NAME} PRIVATE
src/app/AutoUpdaterSparkle.mm
src/app/MoveToApplications.mm)
# the C++ PCH is built with Objective-C disabled; reusing it for these
# Objective-C++ sources fails ("Objective-C was disabled in precompiled
# file ... but is currently enabled"), so skip the PCH for them
set_source_files_properties(src/app/AutoUpdaterSparkle.mm src/app/MoveToApplications.mm
PROPERTIES SKIP_PRECOMPILE_HEADERS ON)
target_compile_options(${PROJECT_NAME} PRIVATE -F${sparkle_SOURCE_DIR})
target_link_libraries(${PROJECT_NAME} PRIVATE "-F${sparkle_SOURCE_DIR}" "-framework Sparkle")
target_link_libraries(${PROJECT_NAME} PRIVATE "-F${sparkle_SOURCE_DIR}" "-framework Sparkle"
"-framework AppKit")
configure_file(${CMAKE_SOURCE_DIR}/packaging/Info.plist.in
${CMAKE_BINARY_DIR}/Info.plist @ONLY)
set_target_properties(${PROJECT_NAME} PROPERTIES
MACOSX_BUNDLE TRUE
MACOSX_BUNDLE_INFO_PLIST ${CMAKE_BINARY_DIR}/Info.plist
BUILD_RPATH "@executable_path/../Frameworks"
INSTALL_RPATH "@executable_path/../Frameworks")
# USE_SOURCE_PERMISSIONS: without it every file installs as 644 and
# Sparkle's Autoupdate/Updater/XPC helpers lose their execute bits, so
# launchd refuses to run them and every update fails
install(DIRECTORY ${sparkle_SOURCE_DIR}/Sparkle.framework
DESTINATION ${PROJECT_NAME}.app/Contents/Frameworks)
DESTINATION ${PROJECT_NAME}.app/Contents/Frameworks
USE_SOURCE_PERMISSIONS)
endif()

target_precompile_headers(${PROJECT_NAME} PRIVATE
Expand Down
85 changes: 85 additions & 0 deletions src/app/MoveToApplications.mm
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Offers to move the app into /Applications on launch when it is running from
// its download location (Downloads, a mounted DMG, or an App Translocation
// path). Sparkle refuses to update from any of those, so without the move the
// user hits "OpenMix can't be updated if it's running from the location it was
// downloaded to". Compiled only on macOS.

#import <AppKit/AppKit.h>
#import <Foundation/Foundation.h>

static BOOL inApplicationsFolder(NSString* path) {
NSArray<NSString*>* dirs =
NSSearchPathForDirectoriesInDomains(NSApplicationDirectory, NSAllDomainsMask, YES);
for (NSString* dir in dirs) {
if ([path hasPrefix:[dir stringByAppendingString:@"/"]])
return YES;
}
return [path hasPrefix:@"/Applications/"];
}

static void showMoveError(NSError* error) {
NSAlert* alert = [[NSAlert alloc] init];
alert.alertStyle = NSAlertStyleWarning;
alert.messageText = @"Could not move OpenMix";
alert.informativeText = [NSString
stringWithFormat:@"%@\n\nYou can move OpenMix.app to the Applications folder manually.",
error.localizedDescription];
[alert addButtonWithTitle:@"OK"];
[alert runModal];
}

extern "C" void openmix_offer_move_to_applications() {
NSString* bundlePath = [[NSBundle mainBundle] bundlePath];
if (![bundlePath hasSuffix:@".app"])
return; // bare dev binary, not an app bundle
if (inApplicationsFolder(bundlePath))
return;

NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
if ([defaults boolForKey:@"OMMoveToApplicationsDeclined"])
return;

NSAlert* alert = [[NSAlert alloc] init];
alert.messageText = @"Move OpenMix to the Applications folder?";
alert.informativeText = @"OpenMix is running from its download location. "
@"Automatic updates only work from the Applications folder.";
[alert addButtonWithTitle:@"Move to Applications"];
[alert addButtonWithTitle:@"Don't Move"];
if ([alert runModal] != NSAlertFirstButtonReturn) {
[defaults setBool:YES forKey:@"OMMoveToApplicationsDeclined"];
return;
}

NSFileManager* fm = [NSFileManager defaultManager];
NSString* appsDir = @"/Applications";
if (![fm isWritableFileAtPath:appsDir]) {
appsDir = [NSHomeDirectory() stringByAppendingPathComponent:@"Applications"];
[fm createDirectoryAtPath:appsDir withIntermediateDirectories:YES attributes:nil error:nil];
}
NSString* dest = [appsDir stringByAppendingPathComponent:[bundlePath lastPathComponent]];

NSError* error = nil;
if ([fm fileExistsAtPath:dest] && ![fm removeItemAtPath:dest error:&error]) {
showMoveError(error);
return;
}
if (![fm copyItemAtPath:bundlePath toPath:dest error:&error]) {
showMoveError(error);
return;
}

// strip quarantine so Gatekeeper does not translocate the copy on launch
NSTask* xattr = [NSTask launchedTaskWithLaunchPath:@"/usr/bin/xattr"
arguments:@[ @"-dr", @"com.apple.quarantine", dest ]];
[xattr waitUntilExit];

// tidy up the old copy when it is deletable (it is not on a mounted DMG or
// under an App Translocation mount; the stale copy is harmless there)
[fm trashItemAtURL:[NSURL fileURLWithPath:bundlePath] resultingItemURL:nil error:nil];

// relaunch from the new location once this process has exited
NSString* script =
[NSString stringWithFormat:@"sleep 0.5; /usr/bin/open -n \"%@\"", dest];
[NSTask launchedTaskWithLaunchPath:@"/bin/sh" arguments:@[ @"-c", script ]];
exit(0);
}
11 changes: 11 additions & 0 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ class FastTooltipStyle : public QProxyStyle {
};
} // namespace OpenMix

#if defined(Q_OS_MACOS)
// implemented in MoveToApplications.mm; may relaunch the app and not return
extern "C" void openmix_offer_move_to_applications();
#endif

int main(int argc, char* argv[]) {
QApplication qtApp(argc, argv);

Expand All @@ -38,6 +43,12 @@ int main(int argc, char* argv[]) {
QApplication::setOrganizationName("OpenMix");
QApplication::setWindowIcon(QIcon(":/icons/openmix.png"));

#if defined(Q_OS_MACOS)
// before any real startup work: Sparkle cannot update an app that runs
// from Downloads, a DMG, or a translocation path
openmix_offer_move_to_applications();
#endif

oclero::qlementine::icons::initializeIconTheme();

// Fusion base, but show tooltips faster than the platform default
Expand Down
Loading