Last active
January 4, 2024 17:22
-
-
Save PicoMitchell/9957fad24740831ffcc848bff48d2750 to your computer and use it in GitHub Desktop.
"rosetter" is a shell/CLI tool to enable (or disable) opening apps using Rosetta on Apple Silicon Macs (as if you had clicked the "Open using Rosetta" checkbox in Finder).
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/sh | |
rosetter() ( # Notice "(" instead of "{" for this function, see THIS IS A SUBSHELL FUNCTION comments below. | |
# ABOUT ROSETTER | |
# "rosetter" is a shell/CLI tool to enable (or disable) opening apps using Rosetta on Apple Silicon Macs (as if you had clicked the "Open using Rosetta" checkbox in Finder). | |
# To enable opening apps using Rosetta, simply pass "rosetter" the app path(s) and/or bundle identifier(s) as arguments that you would like to enable opening using Rosetta (multiple app paths and/or bundle identifiers can be enabled at once). | |
# To disable opening apps using Rosetta, include the "--disable" (or "-d") argument along with the app path(s) and/or bundle identifier(s). | |
## | |
## Created by Pico Mitchell (of Random Applications) on 04/08/23 | |
## | |
## https://randomapplications.com/rosetter | |
## | |
## MIT License | |
## | |
## Copyright (c) 2023 Pico Mitchell (Random Applications) | |
## | |
## Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), | |
## to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, | |
## and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: | |
## | |
## The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. | |
## | |
## THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
## FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, | |
## WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | |
## | |
# THIS IS A SUBSHELL FUNCTION | |
# Subshell functions are entirely self contained. | |
# All of the variables (and functions) within a subshell function only exist within the scope of the subshell function (like a regular subshell). | |
# This means that every variable does NOT need to be declared as "local" and even altering "PATH" only affects the scope of this subshell function. | |
readonly ROSETTER_VERSION='2023.4.16-2' | |
PATH='/usr/bin:/bin:/usr/sbin:/sbin' # "export" is not required since PATH is already exported in the environment, therefore modifying it modifies the already exported variable. | |
if [ "$(sysctl -in hw.optional.arm64)" != '1' ]; then | |
>&2 echo 'rosetter ERROR: This is NOT an Apple Silicon Mac ("rosetter" cannot do anything useful on Intel Macs since Rosetta does not exist).' | |
return 255 | |
fi | |
run_as_logged_in_user_if_needed() { # Based On: https://github.com/freegeek-pdx/mkuser/blob/0880ebf90669553e28e40cd87cd9ee9e5fd64a1e/mkuser.sh#L147-L164 (Copyright (c) Free Geek - MIT License) | |
# This function is inspired by and based on https://scriptingosx.com/2020/08/running-a-command-as-another-user/ but with the added | |
# ability to be called whether or not the script is running as root and only run the command as the logged in user when needed. | |
if [ "$(id -u)" -eq 0 ]; then | |
logged_in_user_id="$(echo 'show State:/Users/ConsoleUser' | scutil | awk '(($1 == "Name") && (($NF == "loginwindow") || ($NF ~ /^_/))) { exit } ($1 == "UID") { print $NF; exit }')" # From: https://github.com/freegeek-pdx/macOS-Testing-and-Deployment-Scripts/blob/5b0d100bbc1f5baced895dd5a7c37a2d6de549fa/Other%20Scripts/get_marketing_model_name.sh#L102-L114 (Copyright (c) Free Geek - MIT License) | |
if [ -z "${logged_in_user_id}" ]; then | |
echo 'rosetter ERROR: Running as root and no user is logged in to run as (Rosetta state is set separately for each user so "rosetter" must be run as a user).' | |
return 254 | |
else # Must only run the command as the logged in user if running as root since using "sudo -u" would fail if running as a standard user (which cannot run "sudo" commands). | |
launchctl asuser "${logged_in_user_id}" sudo -u "#${logged_in_user_id}" "$@" | |
fi | |
else # If script is not running as root, just run the command normally which will run as the user running the script (which may or may not be the logged in user if the script itself is being run via "sudo -u"). | |
"$@" | |
fi | |
} | |
# Suppress ShellCheck warning that expressions don't expand in single quotes since this is intended. | |
# "`", "${var}", and "$(var)" within this JXA code are actually JavaScript/JXA syntax and not shell syntax. | |
# No shell variables (or command substitution) are used in this JXA code, so it is single quoted. | |
# shellcheck disable=SC2016 | |
rosetter_output="$(run_as_logged_in_user_if_needed osascript -l 'JavaScript' -e ' | |
"use strict" | |
ObjC.import("AppKit") // For "URLsForApplicationsWithBundleIdentifier" on macOS 12 Monterey and newer. | |
ObjC.import("LaunchServices") // For "LSCopyApplicationURLsForBundleIdentifier" on macOS 11 Big Sur. | |
ObjC.bindFunction("_LSSetArchitecturePreferenceForApplicationURL", ["int", ["id", "id"]]) // Private "_LSSetArchitecturePreferenceForApplicationURL" function discovered by: https://github.com/tapthaker/SetArchPrefForURL#how-does-this-work- | |
ObjC.bindFunction("_LSCopyArchitecturePreferenceForApplicationURL", ["id", ["id"]]) // Private "_LSCopyArchitecturePreferenceForApplicationURL" function found in: https://github.com/phracker/MacOSX-SDKs/blob/041600eda65c6a668f66cb7d56b7d1da3e8bcc93/MacOSX11.0.sdk/System/Library/Frameworks/CoreServices.framework/Versions/A/CoreServices.tbd#L1035 | |
// About "ObjC.bindFunction" to access private/unbridged functions: https://developer.apple.com/library/archive/releasenotes/InterapplicationCommunication/RN-JavaScriptForAutomation/Articles/OSX10-10.html#//apple_ref/doc/uid/TP40014508-CH109-SW29 | |
function run(argv) { | |
const rosetterVersion = argv.pop(), appPathsOrBundleIdentifier = [] | |
let isDisabling = false, isQuiet = false | |
argv.forEach(thisArg => { | |
if (thisArg) | |
switch (thisArg.toLowerCase()) { | |
case "--disable": | |
case "-d": | |
isDisabling = true | |
break | |
case "--quiet": | |
case "-q": | |
isQuiet = true | |
break | |
case "-dq": | |
case "-qd": | |
isDisabling = true | |
isQuiet = true | |
break | |
default: | |
appPathsOrBundleIdentifier.push(thisArg) | |
} | |
}) | |
if (appPathsOrBundleIdentifier.length == 0) | |
return `rosetter: Version ${rosetterVersion} | |
Copyright (c) ${new Date().getFullYear()} Pico Mitchell - MIT License | |
https://randomapplications.com/rosetter | |
USAGE: rosetter [ --disable | -d ] [ --quiet | -q ] [ /path/to/app | app.bundle.identifier ... ] | |
rosetter ERROR: No application path(s) and/or bundle identifier(s) specified.` | |
let rosetterOutput = "" | |
let newArchValue = "x86_64" | |
if (isDisabling) { | |
rosetterOutput += "rosetter NOTICE: Will DISABLE opening using Rosetta for all specified apps." | |
newArchValue = "arm64" | |
} | |
const fileManager = $.NSFileManager.defaultManager, sharedWorkspace = $.NSWorkspace.sharedWorkspace | |
appPathsOrBundleIdentifier.forEach(thisAppPathOrBundleIdentifier => { | |
const theseAppPaths = [] | |
if (thisAppPathOrBundleIdentifier) { | |
if (fileManager.fileExistsAtPath(thisAppPathOrBundleIdentifier)) | |
theseAppPaths.push(thisAppPathOrBundleIdentifier) | |
else { | |
const theseAppURLs = (sharedWorkspace.respondsToSelector("URLsForApplicationsWithBundleIdentifier:") | |
? sharedWorkspace.URLsForApplicationsWithBundleIdentifier(thisAppPathOrBundleIdentifier).js // This NSWorkspace method was added in macOS 12 Monterey. | |
: ObjC.castRefToObject($.LSCopyApplicationURLsForBundleIdentifier($.CFStringCreateWithCString($.kCFAllocatorDefault, thisAppPathOrBundleIdentifier, $.kCFStringEncodingUTF8), $())).js // Otherwise fallback on this previous LaunchServices function for macOS 11 Big Sur. | |
) | |
if (Array.isArray(theseAppURLs) && theseAppURLs.length > 0) | |
theseAppURLs.forEach(thisAppURL => theseAppPaths.push(thisAppURL.path.js)) | |
} | |
} | |
if (theseAppPaths.length == 0) | |
rosetterOutput += (rosetterOutput ? "\n" : "") + `rosetter ERROR: Path or bundle identifier "${thisAppPathOrBundleIdentifier}" not found.` | |
else | |
theseAppPaths.forEach(thisAppPath => { | |
if (rosetterOutput) rosetterOutput += "\n" | |
thisAppPath = $.NSURL.fileURLWithPathRelativeToURL($(thisAppPath).stringByResolvingSymlinksInPath, $.NSURL.fileURLWithPath(fileManager.currentDirectoryPath)).URLByResolvingSymlinksInPath.path.js | |
const thisAppInfoPlistIsDirectoryRef = Ref() | |
if (!thisAppPath || !fileManager.fileExistsAtPathIsDirectory(`${thisAppPath}/Contents/Info.plist`, thisAppInfoPlistIsDirectoryRef) || thisAppInfoPlistIsDirectoryRef[0]) | |
rosetterOutput += `rosetter ERROR: "${thisAppPath}" is not an application.` | |
else { | |
const thisAppBundle = $.NSBundle.bundleWithPath(thisAppPath) | |
if (!thisAppBundle || thisAppBundle.isNil()) | |
rosetterOutput += `rosetter ERROR: "${thisAppPath}" is not a valid application bundle.` | |
else { | |
const thisAppBundleIdentifier = thisAppBundle.bundleIdentifier | |
if (!thisAppBundleIdentifier || thisAppBundleIdentifier.isNil()) | |
rosetterOutput += `rosetter ERROR: "${thisAppPath}" is not a valid application (no bundle identifier).` | |
else { | |
const thisAppBundleArchitectures = ObjC.deepUnwrap(thisAppBundle.executableArchitectures) | |
if (!thisAppBundleArchitectures || (thisAppBundleArchitectures.length == 0)) | |
rosetterOutput += `rosetter ERROR: "${thisAppPath}" is not a valid application (no executable).` | |
else if (!thisAppBundleArchitectures.includes(16777228)) // https://developer.apple.com/documentation/foundation/1495005-mach-o_architecture/nsbundleexecutablearchitecturearm64 | |
rosetterOutput += `rosetter ERROR: "${thisAppPath}" is Intel only (it will always open using Rosetta).` | |
else if (!thisAppBundleArchitectures.includes(16777223)) // https://developer.apple.com/documentation/foundation/1495005-mach-o_architecture/nsbundleexecutablearchitecturex86_64 | |
rosetterOutput += `rosetter ERROR: "${thisAppPath}" is Apple Silicon only (it cannot open using Rosetta).` | |
else { | |
const thisAppURL = $.NSURL.fileURLWithPath(thisAppPath), currentArchValue = $._LSCopyArchitecturePreferenceForApplicationURL(thisAppURL).js, currentUserName = $.NSUserName().js | |
if ((currentArchValue == newArchValue) || (!currentArchValue && isDisabling)) | |
rosetterOutput += `rosetter: "${thisAppPath}" is already set to ${isDisabling ? "NOT " : ""}open using Rosetta for "${currentUserName}".` | |
else if (($._LSSetArchitecturePreferenceForApplicationURL(thisAppURL, newArchValue) == 1) && ($._LSCopyArchitecturePreferenceForApplicationURL(thisAppURL).js == newArchValue)) | |
rosetterOutput += `rosetter: ${currentArchValue ? "Updated" : "Set"} "${thisAppPath}" to ${isDisabling ? "NOT " : ""}open using Rosetta for "${currentUserName}".` | |
else | |
rosetterOutput += `rosetter ERROR: FAILED to set "${thisAppPath}" to ${isDisabling ? "NOT " : ""}open using Rosetta for "${currentUserName}" (THIS SHOULD NOT HAVE HAPPENED).` | |
} | |
} | |
} | |
} | |
}) | |
}) | |
return (isQuiet ? `rosetter QUIET: ${(rosetterOutput.includes("rosetter ERROR:") ? "ERROR" : "SUCCESS")}` : rosetterOutput) | |
} | |
' -- "$@" "${ROSETTER_VERSION}" 2> /dev/null)" | |
if [ -z "${rosetter_output}" ]; then | |
>&2 echo 'rosetter ERROR: Unknown error occurred (THIS SHOULD NOT HAVE HAPPENED).' | |
return 253 | |
elif [ -z "${rosetter_output##*rosetter ERROR:*}" ]; then | |
>&2 printf '%s\n' "${rosetter_output}" | |
return 1 | |
elif [ "${rosetter_output}" = 'rosetter QUIET: ERROR' ]; then | |
return 1 | |
elif [ "${rosetter_output}" = 'rosetter QUIET: SUCCESS' ]; then | |
return 0 | |
fi | |
printf '%s\n' "${rosetter_output}" | |
return 0 | |
) | |
rosetter "$@" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment