├── .gitignore ├── .clang-format ├── AppDelegate.h ├── main.m ├── format.sh ├── .github └── workflows │ └── release.yml ├── Info.plist ├── README.md ├── ko-kr.lproj └── Localizable.strings ├── ja.lproj └── Localizable.strings ├── en.lproj └── Localizable.strings ├── pt-br.lproj └── Localizable.strings ├── es-419.lproj └── Localizable.strings ├── es-es.lproj └── Localizable.strings ├── de-de.lproj └── Localizable.strings ├── fr-fr.lproj └── Localizable.strings ├── STPrivilegedTask.h ├── App.xib ├── STPrivilegedTask.m ├── AppDelegate.mm └── DynamicUniversalApp.xcodeproj └── project.pbxproj /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | xcuserdata/ 3 | *.xcworkspace/ 4 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | BasedOnStyle: Google 2 | ColumnLimit: 100 3 | DerivePointerAlignment: false 4 | -------------------------------------------------------------------------------- /AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface AppDelegate : NSObject 4 | @end 5 | -------------------------------------------------------------------------------- /main.m: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | int main(int argc, const char * argv[]) { 4 | return NSApplicationMain(argc, argv); 5 | } 6 | -------------------------------------------------------------------------------- /format.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit -o pipefail 4 | 5 | script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 6 | (cd $script_dir && clang-format -i *.mm *.h) 7 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: [push] 4 | 5 | defaults: 6 | run: 7 | shell: bash 8 | 9 | jobs: 10 | release: 11 | runs-on: macOS-latest 12 | timeout-minutes: 60 13 | 14 | steps: 15 | - name: Clone repo 16 | uses: actions/checkout@v2 17 | 18 | - name: Build app 19 | run: | 20 | # Remove old CommandLineTools to fix ARM builds 21 | sudo rm -rf /Library/Developer/CommandLineTools 22 | 23 | xcodebuild -project DynamicUniversalApp.xcodeproj -configuration Release 24 | 25 | - name: Store build artifact 26 | uses: actions/upload-artifact@v4 27 | with: 28 | name: build 29 | path: build/Release 30 | if-no-files-found: error 31 | -------------------------------------------------------------------------------- /Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 1 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | NSHumanReadableCopyright 26 | 27 | NSMainNibFile 28 | App 29 | NSPrincipalClass 30 | NSApplication 31 | NSServices 32 | 33 | 34 | 35 | NSSupportsSuddenTermination 36 | 37 | TargetAppName 38 | 39 | TargetDownloadURLs 40 | 41 | aarch64 42 | 43 | x86_64 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Dynamic Universal App 2 | 3 | Dynamic Universal App (DUA) is tiny bootstrap app that simplifies the user 4 | download process for macOS applications with Intel and Apple Silicon builds. 5 | It is an bandwidth efficient alternative to [universal fat binaries](https://developer.apple.com/documentation/apple-silicon/building-a-universal-macos-binary). 6 | 7 | Users download the DUA bundle (customized with the real app name and icon) 8 | instead of the real app. On launch, it will automatically download the 9 | architecture specific build of your app, replace itself with the real app, and 10 | finally switch over to the real app. 11 | 12 | figma-dua 13 | 14 | Compared to normal universal binaries or listing architecture specific 15 | download links, this has the following benefits: 16 | 17 | * Simpler download page for your users. They won't have to know what "Intel" or 18 | "Apple Silicon" means. 19 | 20 | * Faster downloads. Normal universal binaries can make your app up to 2x 21 | larger. 22 | 23 | * In the future, even faster downloads with XZ compression. Traditionally 24 | macOS apps are distributed as ZIPs or DMGs, but they offer subpar 25 | compression ratios. DUA will optionally able to download the real app from 26 | an XZ package to reduce download time even further. For basic Electron apps, 27 | XZ reduces the download from 80MB to 60MB. 28 | 29 | ### Notes 30 | 31 | DUA will not work for enterprise deployments where the end-users do not have 32 | write access to `/Applications`. You will likely want to provide direct links 33 | to architecture-specfic builds somewheree for use by enterprise admins. 34 | 35 | ### Packaging 36 | 37 | This needs to be automated, but here are manual steps: 38 | 39 | ```sh 40 | xcodebuild -project DynamicUniversalApp.xcodeproj -configuration Release 41 | 42 | APP_DIR="build/Release/DynamicUniversalApp.app" 43 | INFO_PLIST="$APP_DIR/Contents/Info.plist" 44 | /usr/libexec/PlistBuddy -c 'set CFBundleIdentifier com.figma.desktop.dynamic-universal-app' "$INFO_PLIST" 45 | /usr/libexec/PlistBuddy -c 'set TargetAppName Figma' "$INFO_PLIST" 46 | /usr/libexec/PlistBuddy -c 'set TargetDownloadURLs:aarch64 https://desktop.figma.com/mac-arm/Figma.zip' "$INFO_PLIST" 47 | /usr/libexec/PlistBuddy -c 'set TargetDownloadURLs:x86_64 https://desktop.figma.com/mac/Figma.zip' "$INFO_PLIST" 48 | 49 | /usr/libexec/PlistBuddy -c 'add CFBundleIconFile string icon.icns' "$INFO_PLIST" 50 | cp /path/to/icon.icns "$APP_DIR/Contents/Resources/icon.icns" 51 | 52 | mv DynamicUniversalApp.app Figma.app 53 | 54 | codesign --force --options=runtime --timestamp --sign "Developer ID Application: ..." Figma.app 55 | cd ~/figma/figma/desktop 56 | node scripts/notarize.js com.figma.desktop.dynamic-universal-app "$APP_DIR" 57 | ``` 58 | -------------------------------------------------------------------------------- /ko-kr.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* Title of dialog shown when automatic installation has failed. %1$@ is the name of the app. */ 2 | "errorDialog.title" = "%1$@ 자동 설치 실패"; 3 | 4 | /* Message in dialog shown when automatic installation has failed, prompting user to download manually. %1$@ is the name of the app. */ 5 | "errorDialog.downloadManuallyMessage" = "계속하려면 %1$@을(를) 수동으로 다운로드하십시오."; 6 | 7 | /* Message in dialog shown when automatic installation has failed, prompting user to contact support. This message is followed by an error shown on the next line. %1$@ is the name of the app. */ 8 | "errorDialog.contactSupportMessage" = "이 오류 정보를 사용하여 %1$@ 지원팀에 문의할 수도 있습니다."; 9 | 10 | /* Button in dialog shown when automatic installation has failed. Clicking this button will open a URL for the user to manually download the app. */ 11 | "errorDialog.downloadManuallyButton" = "수동으로 다운로드"; 12 | 13 | /* Title of a dialog prompting user to move the app into their macOS Applications folder. */ 14 | "moveToApplicationsDialog.title" = "애플리케이션 폴더로 이동"; 15 | 16 | /* Message in dialog prompting user to move the app into their macOS Applications folder. %1$@ is the name of the app. This message is broken into two parts, with "\n\n" included to generate an empty line between the two parts. */ 17 | "moveToApplicationsDialog.message" = "%1$@ 앱을 애플리케이션 폴더로 이동한 후 다시 시도하십시오.\n\n앱이 이미 애플리케이션 폴더에 있는 경우 다른 폴더로 이동한 다음 다시 애플리케이션으로 이동합니다."; 18 | 19 | /* Label for a generic confirmation button that acknowledges some message. */ 20 | "dialog.okButton" = "확인"; 21 | 22 | /* Label for the generic cancel button that closes dialogs without taking an action. */ 23 | "dialog.cancelButton" = "취소"; 24 | 25 | /* Error message shown alongside its numeric code. %1$@ is the error message itself, and %2$ld is the numeric code for that error message. */ 26 | "error.errorWithCode" = "%1$@ (코드 %2$ld)"; 27 | 28 | /* Generic error message shown when initial installer checks fail. %1$i is the numeric code of the error. */ 29 | "error.initialCheckFailed" = "초기 확인 실패: %1$i"; 30 | 31 | /* Error message shown when the installer does not have the required permissions to install the app. */ 32 | "error.wrongPermissions" = "이 앱을 자동 설치할 수 있는 권한이 없습니다."; 33 | 34 | /* Error message shown when the app fails to download due to an HTTP error. %1$lu is the numeric error code for the HTTP error. */ 35 | "error.downloadFailed" = "다운로드에 실패했습니다. HTTP: %1$lu"; 36 | 37 | /* Error message shwon when the installer fails to extract the zipped application. %1$i is the numeric code of the error. */ 38 | "error.extractFailed" = "추출 실패: %1$i"; 39 | 40 | /* Title of the installer dialog. %1$@ is the name of the app that is being installed. */ 41 | "installerDialog.title" = "%1$@ 설치 프로그램"; 42 | 43 | /* Message shown in the installer dialog indicating that the app is currently being downloaded. %1$@ is the name of the app. */ 44 | "installerDialog.downloadingMessage" = "%1$@ 다운로드 중..."; 45 | 46 | /* Message shwon in the installer dialog indicating that the app is now being installed. %1$@ is the name of the app. */ 47 | "installerDialog.installingMessage" = "%1$@ 설치 중..."; 48 | 49 | /* Label for menu item used to quit the application. %1$@ is the name of the application. */ 50 | "menu.quitItem" = "%1$@ 종료"; -------------------------------------------------------------------------------- /ja.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* Title of dialog shown when automatic installation has failed. %1$@ is the name of the app. */ 2 | "errorDialog.title" = "%1$@の自動インストールに失敗しました"; 3 | 4 | /* Message in dialog shown when automatic installation has failed, prompting user to download manually. %1$@ is the name of the app. */ 5 | "errorDialog.downloadManuallyMessage" = "続行するには%1$@を手動でダウンロードしてください。"; 6 | 7 | /* Message in dialog shown when automatic installation has failed, prompting user to contact support. This message is followed by an error shown on the next line. %1$@ is the name of the app. */ 8 | "errorDialog.contactSupportMessage" = "このエラー情報について%1$@サポートまでご連絡いただくことも可能です:"; 9 | 10 | /* Button in dialog shown when automatic installation has failed. Clicking this button will open a URL for the user to manually download the app. */ 11 | "errorDialog.downloadManuallyButton" = "手動でダウンロードする"; 12 | 13 | /* Title of a dialog prompting user to move the app into their macOS Applications folder. */ 14 | "moveToApplicationsDialog.title" = "[アプリケーション]フォルダに移動する"; 15 | 16 | /* Message in dialog prompting user to move the app into their macOS Applications folder. %1$@ is the name of the app. This message is broken into two parts, with "\n\n" included to generate an empty line between the two parts. */ 17 | "moveToApplicationsDialog.message" = "%1$@アプリを[アプリケーション]フォルダに移動してからもう一度お試しください。\n\nアプリが既に[アプリケーション]フォルダにある場合は、アプリをドラックして他のフォルダにいったん移動した後、[アプリケーション]に戻してください。"; 18 | 19 | /* Label for a generic confirmation button that acknowledges some message. */ 20 | "dialog.okButton" = "OK"; 21 | 22 | /* Label for the generic cancel button that closes dialogs without taking an action. */ 23 | "dialog.cancelButton" = "キャンセル"; 24 | 25 | /* Error message shown alongside its numeric code. %1$@ is the error message itself, and %2$ld is the numeric code for that error message. */ 26 | "error.errorWithCode" = "%1$@ (コード%2$ld)"; 27 | 28 | /* Generic error message shown when initial installer checks fail. %1$i is the numeric code of the error. */ 29 | "error.initialCheckFailed" = "初期のチェックに失敗しました: %1$i"; 30 | 31 | /* Error message shown when the installer does not have the required permissions to install the app. */ 32 | "error.wrongPermissions" = "このアプリの自動インストールに必要な権限がありません。"; 33 | 34 | /* Error message shown when the app fails to download due to an HTTP error. %1$lu is the numeric error code for the HTTP error. */ 35 | "error.downloadFailed" = "ダウンロードに失敗しました、HTTP: %1$lu"; 36 | 37 | /* Error message shwon when the installer fails to extract the zipped application. %1$i is the numeric code of the error. */ 38 | "error.extractFailed" = "展開に失敗しました: %1$i"; 39 | 40 | /* Title of the installer dialog. %1$@ is the name of the app that is being installed. */ 41 | "installerDialog.title" = "%1$@インストーラー"; 42 | 43 | /* Message shown in the installer dialog indicating that the app is currently being downloaded. %1$@ is the name of the app. */ 44 | "installerDialog.downloadingMessage" = "%1$@をダウンロードしています..."; 45 | 46 | /* Message shwon in the installer dialog indicating that the app is now being installed. %1$@ is the name of the app. */ 47 | "installerDialog.installingMessage" = "%1$@をインストールしています..."; 48 | 49 | /* Label for menu item used to quit the application. %1$@ is the name of the application. */ 50 | "menu.quitItem" = "%1$@を終了"; -------------------------------------------------------------------------------- /en.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* Title of dialog shown when automatic installation has failed. %1$@ is the name of the app. */ 2 | "errorDialog.title" = "%1$@ Automatic Installation Failed"; 3 | 4 | /* Message in dialog shown when automatic installation has failed, prompting user to download manually. %1$@ is the name of the app. */ 5 | "errorDialog.downloadManuallyMessage" = "Download %1$@ manually to continue."; 6 | 7 | /* Message in dialog shown when automatic installation has failed, prompting user to contact support. This message is followed by an error shown on the next line. %1$@ is the name of the app. */ 8 | "errorDialog.contactSupportMessage" = "You may also contact %1$@ support with this error information:"; 9 | 10 | /* Button in dialog shown when automatic installation has failed. Clicking this button will open a URL for the user to manually download the app. */ 11 | "errorDialog.downloadManuallyButton" = "Download manually"; 12 | 13 | /* Title of a dialog prompting user to move the app into their macOS Applications folder. */ 14 | "moveToApplicationsDialog.title" = "Move to Applications Folder"; 15 | 16 | /* Message in dialog prompting user to move the app into their macOS Applications folder. %1$@ is the name of the app. This message is broken into two parts, with "\n\n" included to generate an empty line between the two parts. */ 17 | "moveToApplicationsDialog.message" = "Please move the %1$@ app into the Applications folder and try again.\n\nIf the app is already in the Applications folder, drag it into some other folder and then back into Applications."; 18 | 19 | /* Label for a generic confirmation button that acknowledges some message. */ 20 | "dialog.okButton" = "OK"; 21 | 22 | /* Label for the generic cancel button that closes dialogs without taking an action. */ 23 | "dialog.cancelButton" = "Cancel"; 24 | 25 | /* Error message shown alongside its numeric code. %1$@ is the error message itself, and %2$ld is the numeric code for that error message. */ 26 | "error.errorWithCode" = "%1$@ (code %2$ld)"; 27 | 28 | /* Generic error message shown when initial installer checks fail. %1$i is the numeric code of the error. */ 29 | "error.initialCheckFailed" = "Initial check failed: %1$i"; 30 | 31 | /* Error message shown when the installer does not have the required permissions to install the app. */ 32 | "error.wrongPermissions" = "You do not have the necessary permissions required to autoinstall this app."; 33 | 34 | /* Error message shown when the app fails to download due to an HTTP error. %1$lu is the numeric error code for the HTTP error. */ 35 | "error.downloadFailed" = "Failed to download, HTTP: %1$lu"; 36 | 37 | /* Error message shwon when the installer fails to extract the zipped application. %1$i is the numeric code of the error. */ 38 | "error.extractFailed" = "Failed to extract: %1$i"; 39 | 40 | /* Title of the installer dialog. %1$@ is the name of the app that is being installed. */ 41 | "installerDialog.title" = "%1$@ Installer"; 42 | 43 | /* Message shown in the installer dialog indicating that the app is currently being downloaded. %1$@ is the name of the app. */ 44 | "installerDialog.downloadingMessage" = "Downloading %1$@..."; 45 | 46 | /* Message shwon in the installer dialog indicating that the app is now being installed. %1$@ is the name of the app. */ 47 | "installerDialog.installingMessage" = "Installing %1$@..."; 48 | 49 | /* Label for menu item used to quit the application. %1$@ is the name of the application. */ 50 | "menu.quitItem" = "Quit %1$@"; 51 | -------------------------------------------------------------------------------- /pt-br.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* Title of dialog shown when automatic installation has failed. %1$@ is the name of the app. */ 2 | "errorDialog.title" = "%1$@ Falha na instalação automática"; 3 | 4 | /* Message in dialog shown when automatic installation has failed, prompting user to download manually. %1$@ is the name of the app. */ 5 | "errorDialog.downloadManuallyMessage" = "Baixe %1$@ manualmente para continuar."; 6 | 7 | /* Message in dialog shown when automatic installation has failed, prompting user to contact support. This message is followed by an error shown on the next line. %1$@ is the name of the app. */ 8 | "errorDialog.contactSupportMessage" = "Você também pode entrar em contato com o suporte do %1$@ com estas informações de erro:"; 9 | 10 | /* Button in dialog shown when automatic installation has failed. Clicking this button will open a URL for the user to manually download the app. */ 11 | "errorDialog.downloadManuallyButton" = "Baixar manualmente"; 12 | 13 | /* Title of a dialog prompting user to move the app into their macOS Applications folder. */ 14 | "moveToApplicationsDialog.title" = "Mover para a pasta Aplicativos"; 15 | 16 | /* Message in dialog prompting user to move the app into their macOS Applications folder. %1$@ is the name of the app. This message is broken into two parts, with "\n\n" included to generate an empty line between the two parts. */ 17 | "moveToApplicationsDialog.message" = "Mova o app %1$@ para a pasta Aplicativos e tente novamente.\n\nSe o app já estiver na pasta Aplicativos, arraste-o para outra pasta e depois de volta para Aplicativos."; 18 | 19 | /* Label for a generic confirmation button that acknowledges some message. */ 20 | "dialog.okButton" = "OK"; 21 | 22 | /* Label for the generic cancel button that closes dialogs without taking an action. */ 23 | "dialog.cancelButton" = "Cancelar"; 24 | 25 | /* Error message shown alongside its numeric code. %1$@ is the error message itself, and %2$ld is the numeric code for that error message. */ 26 | "error.errorWithCode" = "%1$@ (código %2$ld)"; 27 | 28 | /* Generic error message shown when initial installer checks fail. %1$i is the numeric code of the error. */ 29 | "error.initialCheckFailed" = "Falha na verificação inicial: %1$i"; 30 | 31 | /* Error message shown when the installer does not have the required permissions to install the app. */ 32 | "error.wrongPermissions" = "Você não tem as permissões necessárias para autoinstalar este app."; 33 | 34 | /* Error message shown when the app fails to download due to an HTTP error. %1$lu is the numeric error code for the HTTP error. */ 35 | "error.downloadFailed" = "Falha ao baixar, HTTP: %1$lu"; 36 | 37 | /* Error message shwon when the installer fails to extract the zipped application. %1$i is the numeric code of the error. */ 38 | "error.extractFailed" = "Falha ao extrair: %1$i"; 39 | 40 | /* Title of the installer dialog. %1$@ is the name of the app that is being installed. */ 41 | "installerDialog.title" = "%1$@ Instalador"; 42 | 43 | /* Message shown in the installer dialog indicating that the app is currently being downloaded. %1$@ is the name of the app. */ 44 | "installerDialog.downloadingMessage" = "Baixando %1$@..."; 45 | 46 | /* Message shwon in the installer dialog indicating that the app is now being installed. %1$@ is the name of the app. */ 47 | "installerDialog.installingMessage" = "Instalando %1$@..."; 48 | 49 | /* Label for menu item used to quit the application. %1$@ is the name of the application. */ 50 | "menu.quitItem" = "Sair do %1$@"; -------------------------------------------------------------------------------- /es-419.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* Title of dialog shown when automatic installation has failed. %1$@ is the name of the app. */ 2 | "errorDialog.title" = "Error en la instalación automática de %1$@"; 3 | 4 | /* Message in dialog shown when automatic installation has failed, prompting user to download manually. %1$@ is the name of the app. */ 5 | "errorDialog.downloadManuallyMessage" = "Descarga %1$@ manualmente para continuar."; 6 | 7 | /* Message in dialog shown when automatic installation has failed, prompting user to contact support. This message is followed by an error shown on the next line. %1$@ is the name of the app. */ 8 | "errorDialog.contactSupportMessage" = "También puedes contactar al Soporte de %1$@ con la siguiente información del error:"; 9 | 10 | /* Button in dialog shown when automatic installation has failed. Clicking this button will open a URL for the user to manually download the app. */ 11 | "errorDialog.downloadManuallyButton" = "Descargar manualmente"; 12 | 13 | /* Title of a dialog prompting user to move the app into their macOS Applications folder. */ 14 | "moveToApplicationsDialog.title" = "Mover a la carpeta Aplicaciones"; 15 | 16 | /* Message in dialog prompting user to move the app into their macOS Applications folder. %1$@ is the name of the app. This message is broken into two parts, with "\n\n" included to generate an empty line between the two parts. */ 17 | "moveToApplicationsDialog.message" = "Mueve la app %1$@ a la carpeta Aplicaciones e inténtalo nuevamente.\n\nSi la app ya está en la carpeta Aplicaciones, arrástrala a otra carpeta y, luego, regresa a Aplicaciones."; 18 | 19 | /* Label for a generic confirmation button that acknowledges some message. */ 20 | "dialog.okButton" = "Aceptar"; 21 | 22 | /* Label for the generic cancel button that closes dialogs without taking an action. */ 23 | "dialog.cancelButton" = "Cancelar"; 24 | 25 | /* Error message shown alongside its numeric code. %1$@ is the error message itself, and %2$ld is the numeric code for that error message. */ 26 | "error.errorWithCode" = "%1$@ (código %2$ld)"; 27 | 28 | /* Generic error message shown when initial installer checks fail. %1$i is the numeric code of the error. */ 29 | "error.initialCheckFailed" = "Error en la comprobación inicial: %1$i"; 30 | 31 | /* Error message shown when the installer does not have the required permissions to install the app. */ 32 | "error.wrongPermissions" = "No tienes los permisos necesarios para instalar esta app automáticamente."; 33 | 34 | /* Error message shown when the app fails to download due to an HTTP error. %1$lu is the numeric error code for the HTTP error. */ 35 | "error.downloadFailed" = "No se pudo descargar, HTTP: %1$lu"; 36 | 37 | /* Error message shwon when the installer fails to extract the zipped application. %1$i is the numeric code of the error. */ 38 | "error.extractFailed" = "No se pudo extraer: %1$i"; 39 | 40 | /* Title of the installer dialog. %1$@ is the name of the app that is being installed. */ 41 | "installerDialog.title" = "Instalador de %1$@"; 42 | 43 | /* Message shown in the installer dialog indicating that the app is currently being downloaded. %1$@ is the name of the app. */ 44 | "installerDialog.downloadingMessage" = "Descargando %1$@..."; 45 | 46 | /* Message shwon in the installer dialog indicating that the app is now being installed. %1$@ is the name of the app. */ 47 | "installerDialog.installingMessage" = "Instalando %1$@..."; 48 | 49 | /* Label for menu item used to quit the application. %1$@ is the name of the application. */ 50 | "menu.quitItem" = "Salir de %1$@"; -------------------------------------------------------------------------------- /es-es.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* Title of dialog shown when automatic installation has failed. %1$@ is the name of the app. */ 2 | "errorDialog.title" = "Se ha producido un error con la instalación automática de %1$@"; 3 | 4 | /* Message in dialog shown when automatic installation has failed, prompting user to download manually. %1$@ is the name of the app. */ 5 | "errorDialog.downloadManuallyMessage" = "Descarga %1$@ manualmente para continuar."; 6 | 7 | /* Message in dialog shown when automatic installation has failed, prompting user to contact support. This message is followed by an error shown on the next line. %1$@ is the name of the app. */ 8 | "errorDialog.contactSupportMessage" = "También puedes ponerte en contacto con el servicio de asistencia de %1$@ con esta información de error:"; 9 | 10 | /* Button in dialog shown when automatic installation has failed. Clicking this button will open a URL for the user to manually download the app. */ 11 | "errorDialog.downloadManuallyButton" = "Descargar manualmente"; 12 | 13 | /* Title of a dialog prompting user to move the app into their macOS Applications folder. */ 14 | "moveToApplicationsDialog.title" = "Mover a la carpeta Aplicaciones"; 15 | 16 | /* Message in dialog prompting user to move the app into their macOS Applications folder. %1$@ is the name of the app. This message is broken into two parts, with "\n\n" included to generate an empty line between the two parts. */ 17 | "moveToApplicationsDialog.message" = "Mueve la aplicación %1$@ a la carpeta Aplicaciones y vuelva a intentarlo.\n\nSi la aplicación ya está en la carpeta Aplicaciones, arrástrala a otra carpeta y vuelve a moverla a Aplicaciones."; 18 | 19 | /* Label for a generic confirmation button that acknowledges some message. */ 20 | "dialog.okButton" = "Aceptar"; 21 | 22 | /* Label for the generic cancel button that closes dialogs without taking an action. */ 23 | "dialog.cancelButton" = "Cancelar"; 24 | 25 | /* Error message shown alongside its numeric code. %1$@ is the error message itself, and %2$ld is the numeric code for that error message. */ 26 | "error.errorWithCode" = "%1$@ (código %2$ld)"; 27 | 28 | /* Generic error message shown when initial installer checks fail. %1$i is the numeric code of the error. */ 29 | "error.initialCheckFailed" = "Error en la comprobación inicial: %1$i"; 30 | 31 | /* Error message shown when the installer does not have the required permissions to install the app. */ 32 | "error.wrongPermissions" = "No tienes los permisos necesarios para instalar automáticamente esta aplicación."; 33 | 34 | /* Error message shown when the app fails to download due to an HTTP error. %1$lu is the numeric error code for the HTTP error. */ 35 | "error.downloadFailed" = "Error en la descarga, HTTP: %1$lu"; 36 | 37 | /* Error message shwon when the installer fails to extract the zipped application. %1$i is the numeric code of the error. */ 38 | "error.extractFailed" = "Error al extraer: %1$i"; 39 | 40 | /* Title of the installer dialog. %1$@ is the name of the app that is being installed. */ 41 | "installerDialog.title" = "Instalador de %1$@"; 42 | 43 | /* Message shown in the installer dialog indicating that the app is currently being downloaded. %1$@ is the name of the app. */ 44 | "installerDialog.downloadingMessage" = "Descargando %1$@..."; 45 | 46 | /* Message shwon in the installer dialog indicating that the app is now being installed. %1$@ is the name of the app. */ 47 | "installerDialog.installingMessage" = "Instalando %1$@..."; 48 | 49 | /* Label for menu item used to quit the application. %1$@ is the name of the application. */ 50 | "menu.quitItem" = "Salir de %1$@"; -------------------------------------------------------------------------------- /de-de.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* Title of dialog shown when automatic installation has failed. %1$@ is the name of the app. */ 2 | "errorDialog.title" = "Automatische Installation von %1$@ fehlgeschlagen"; 3 | 4 | /* Message in dialog shown when automatic installation has failed, prompting user to download manually. %1$@ is the name of the app. */ 5 | "errorDialog.downloadManuallyMessage" = "Lade %1$@ manuell herunter, um fortzufahren."; 6 | 7 | /* Message in dialog shown when automatic installation has failed, prompting user to contact support. This message is followed by an error shown on the next line. %1$@ is the name of the app. */ 8 | "errorDialog.contactSupportMessage" = "Du kannst dich auch mit diesen Fehlerinformationen an den %1$@-Support wenden:"; 9 | 10 | /* Button in dialog shown when automatic installation has failed. Clicking this button will open a URL for the user to manually download the app. */ 11 | "errorDialog.downloadManuallyButton" = "Manuell herunterladen"; 12 | 13 | /* Title of a dialog prompting user to move the app into their macOS Applications folder. */ 14 | "moveToApplicationsDialog.title" = "In den Ordner „Programme“ verschieben"; 15 | 16 | /* Message in dialog prompting user to move the app into their macOS Applications folder. %1$@ is the name of the app. This message is broken into two parts, with "\n\n" included to generate an empty line between the two parts. */ 17 | "moveToApplicationsDialog.message" = "Bitte verschiebe die %1$@-App in den Ordner „Programme“ und versuche es erneut.\n\nWenn sich die App bereits im Ordner „Programme“ befindet, ziehe sie in einen anderen Ordner und dann zurück in den Ordner „Programme“."; 18 | 19 | /* Label for a generic confirmation button that acknowledges some message. */ 20 | "dialog.okButton" = "OK"; 21 | 22 | /* Label for the generic cancel button that closes dialogs without taking an action. */ 23 | "dialog.cancelButton" = "Abbrechen"; 24 | 25 | /* Error message shown alongside its numeric code. %1$@ is the error message itself, and %2$ld is the numeric code for that error message. */ 26 | "error.errorWithCode" = "%1$@ (Code %2$ld)"; 27 | 28 | /* Generic error message shown when initial installer checks fail. %1$i is the numeric code of the error. */ 29 | "error.initialCheckFailed" = "Erste Überprüfung fehlgeschlagen: %1$i"; 30 | 31 | /* Error message shown when the installer does not have the required permissions to install the app. */ 32 | "error.wrongPermissions" = "Du hast nicht die erforderlichen Berechtigungen, um diese App automatisch zu installieren."; 33 | 34 | /* Error message shown when the app fails to download due to an HTTP error. %1$lu is the numeric error code for the HTTP error. */ 35 | "error.downloadFailed" = "Herunterladen fehlgeschlagen, HTTP: %1$lu"; 36 | 37 | /* Error message shwon when the installer fails to extract the zipped application. %1$i is the numeric code of the error. */ 38 | "error.extractFailed" = "Extrahieren fehlgeschlagen: %1$i"; 39 | 40 | /* Title of the installer dialog. %1$@ is the name of the app that is being installed. */ 41 | "installerDialog.title" = "%1$@-Installationsprogramm"; 42 | 43 | /* Message shown in the installer dialog indicating that the app is currently being downloaded. %1$@ is the name of the app. */ 44 | "installerDialog.downloadingMessage" = "%1$@ wird heruntergeladen..."; 45 | 46 | /* Message shwon in the installer dialog indicating that the app is now being installed. %1$@ is the name of the app. */ 47 | "installerDialog.installingMessage" = "%1$@ wird installiert..."; 48 | 49 | /* Label for menu item used to quit the application. %1$@ is the name of the application. */ 50 | "menu.quitItem" = "Beende %1$@"; -------------------------------------------------------------------------------- /fr-fr.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* Title of dialog shown when automatic installation has failed. %1$@ is the name of the app. */ 2 | "errorDialog.title" = "Échec de l'installation automatique de %1$@"; 3 | 4 | /* Message in dialog shown when automatic installation has failed, prompting user to download manually. %1$@ is the name of the app. */ 5 | "errorDialog.downloadManuallyMessage" = "Veuillez télécharger manuellement le fichier « %1$@ » pour continuer."; 6 | 7 | /* Message in dialog shown when automatic installation has failed, prompting user to contact support. This message is followed by an error shown on the next line. %1$@ is the name of the app. */ 8 | "errorDialog.contactSupportMessage" = "Vous pouvez également contacter l'assistance %1$@ et fournir ces informations d'erreur :"; 9 | 10 | /* Button in dialog shown when automatic installation has failed. Clicking this button will open a URL for the user to manually download the app. */ 11 | "errorDialog.downloadManuallyButton" = "Télécharger manuellement"; 12 | 13 | /* Title of a dialog prompting user to move the app into their macOS Applications folder. */ 14 | "moveToApplicationsDialog.title" = "Déplacez vers le dossier Applications"; 15 | 16 | /* Message in dialog prompting user to move the app into their macOS Applications folder. %1$@ is the name of the app. This message is broken into two parts, with "\n\n" included to generate an empty line between the two parts. */ 17 | "moveToApplicationsDialog.message" = "Veuillez déplacer l'application %1$@ vers le dossier Applications et réessayer.\n\nSi l'application est déjà dans ce dossier, faites-la glisser dans un autre dossier, puis déplacez-la à nouveau dans Applications."; 18 | 19 | /* Label for a generic confirmation button that acknowledges some message. */ 20 | "dialog.okButton" = "OK"; 21 | 22 | /* Label for the generic cancel button that closes dialogs without taking an action. */ 23 | "dialog.cancelButton" = "Annuler"; 24 | 25 | /* Error message shown alongside its numeric code. %1$@ is the error message itself, and %2$ld is the numeric code for that error message. */ 26 | "error.errorWithCode" = "%1$@ (code %2$ld)"; 27 | 28 | /* Generic error message shown when initial installer checks fail. %1$i is the numeric code of the error. */ 29 | "error.initialCheckFailed" = "Échec de la vérification initiale : %1$i"; 30 | 31 | /* Error message shown when the installer does not have the required permissions to install the app. */ 32 | "error.wrongPermissions" = "Vous n'avez pas les autorisations nécessaires pour installer automatiquement cette application."; 33 | 34 | /* Error message shown when the app fails to download due to an HTTP error. %1$lu is the numeric error code for the HTTP error. */ 35 | "error.downloadFailed" = "Échec du téléchargement, erreur HTTP : %1$lu"; 36 | 37 | /* Error message shwon when the installer fails to extract the zipped application. %1$i is the numeric code of the error. */ 38 | "error.extractFailed" = "Échec de l'extraction : %1$i"; 39 | 40 | /* Title of the installer dialog. %1$@ is the name of the app that is being installed. */ 41 | "installerDialog.title" = "Programme d'installation %1$@"; 42 | 43 | /* Message shown in the installer dialog indicating that the app is currently being downloaded. %1$@ is the name of the app. */ 44 | "installerDialog.downloadingMessage" = "Téléchargement de %1$@…"; 45 | 46 | /* Message shwon in the installer dialog indicating that the app is now being installed. %1$@ is the name of the app. */ 47 | "installerDialog.installingMessage" = "Installation de %1$@…"; 48 | 49 | /* Label for menu item used to quit the application. %1$@ is the name of the application. */ 50 | "menu.quitItem" = "Quitter %1$@"; -------------------------------------------------------------------------------- /STPrivilegedTask.h: -------------------------------------------------------------------------------- 1 | /* 2 | STPrivilegedTask - NSTask-like wrapper around AuthorizationExecuteWithPrivileges 3 | Copyright (C) 2008-2021 Sveinbjorn Thordarson 4 | 5 | BSD License 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | * Neither the name of the copyright holder nor that of any other 14 | contributors may be used to endorse or promote products 15 | derived from this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 18 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 19 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 21 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 22 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 23 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 24 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 26 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | */ 28 | 29 | #import 30 | 31 | #define STPrivilegedTaskDidTerminateNotification @"STPrivilegedTaskDidTerminateNotification" 32 | 33 | // Defines error value for when AuthorizationExecuteWithPrivileges no longer exists 34 | // Rather than defining a new enum, we just create a global constant 35 | extern const OSStatus errAuthorizationFnNoLongerExists; 36 | 37 | @interface STPrivilegedTask : NSObject 38 | 39 | @property(copy) NSArray* arguments; 40 | @property(copy) NSString* currentDirectoryPath; 41 | @property(copy) NSString* launchPath; 42 | 43 | @property(readonly) NSFileHandle* outputFileHandle; 44 | @property(readonly) BOOL isRunning; 45 | @property(readonly) pid_t processIdentifier; 46 | @property(readonly) int terminationStatus; 47 | 48 | @property(copy) void (^terminationHandler)(STPrivilegedTask*); 49 | 50 | + (BOOL)authorizationFunctionAvailable; 51 | 52 | - (instancetype)initWithLaunchPath:(NSString*)path; 53 | - (instancetype)initWithLaunchPath:(NSString*)path arguments:(NSArray*)args; 54 | - (instancetype)initWithLaunchPath:(NSString*)path 55 | arguments:(NSArray*)args 56 | currentDirectory:(NSString*)cwd; 57 | 58 | + (STPrivilegedTask*)launchedPrivilegedTaskWithLaunchPath:(NSString*)path; 59 | + (STPrivilegedTask*)launchedPrivilegedTaskWithLaunchPath:(NSString*)path arguments:(NSArray*)args; 60 | + (STPrivilegedTask*)launchedPrivilegedTaskWithLaunchPath:(NSString*)path 61 | arguments:(NSArray*)args 62 | currentDirectory:(NSString*)cwd; 63 | + (STPrivilegedTask*)launchedPrivilegedTaskWithLaunchPath:(NSString*)path 64 | arguments:(NSArray*)args 65 | currentDirectory:(NSString*)cwd 66 | authorization:(AuthorizationRef)authorization; 67 | 68 | - (OSStatus)launch; 69 | - (OSStatus)launchWithAuthorization:(AuthorizationRef)authorization; 70 | - (void)terminate; // doesn't work 71 | - (void)waitUntilExit; 72 | 73 | @end 74 | -------------------------------------------------------------------------------- /App.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /STPrivilegedTask.m: -------------------------------------------------------------------------------- 1 | /* 2 | STPrivilegedTask - NSTask-like wrapper around AuthorizationExecuteWithPrivileges 3 | Copyright (C) 2008-2021 Sveinbjorn Thordarson 4 | 5 | BSD License 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | * Neither the name of the copyright holder nor that of any other 14 | contributors may be used to endorse or promote products 15 | derived from this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 18 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 19 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 21 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 22 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 23 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 24 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 26 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | */ 28 | 29 | #import "STPrivilegedTask.h" 30 | 31 | #import 32 | #import 33 | #import 34 | #import 35 | #import 36 | 37 | // New error code denoting that AuthorizationExecuteWithPrivileges no longer exists 38 | OSStatus const errAuthorizationFnNoLongerExists = -70001; 39 | 40 | // Create fn pointer to AuthorizationExecuteWithPrivileges 41 | // in case it doesn't exist in this version of macOS 42 | static OSStatus (*_AuthExecuteWithPrivsFn)(AuthorizationRef authorization, const char *pathToTool, AuthorizationFlags options, 43 | char * const *arguments, FILE **communicationsPipe) = NULL; 44 | 45 | 46 | @implementation STPrivilegedTask 47 | { 48 | NSTimer *_checkStatusTimer; 49 | } 50 | 51 | + (void)initialize { 52 | // On 10.7, AuthorizationExecuteWithPrivileges is deprecated. We want 53 | // to still use it since there's no good alternative (without requiring 54 | // code signing). We'll look up the function through dyld and fail if 55 | // it is no longer accessible. If Apple removes the function entirely 56 | // this will fail gracefully. If they keep the function and throw some 57 | // sort of exception, this won't fail gracefully, but that's a risk 58 | // we'll have to take for now. 59 | // Pattern by Andy Kim from Potion Factory LLC 60 | #pragma GCC diagnostic ignored "-Wpedantic" // stop the pedantry! 61 | #pragma clang diagnostic push 62 | _AuthExecuteWithPrivsFn = dlsym(RTLD_DEFAULT, "AuthorizationExecuteWithPrivileges"); 63 | #pragma clang diagnostic pop 64 | } 65 | 66 | - (instancetype)init { 67 | self = [super init]; 68 | if (self) { 69 | _launchPath = nil; 70 | _arguments = nil; 71 | _isRunning = NO; 72 | _outputFileHandle = nil; 73 | _terminationHandler = nil; 74 | _currentDirectoryPath = [[NSFileManager defaultManager] currentDirectoryPath]; 75 | } 76 | return self; 77 | } 78 | 79 | - (instancetype)initWithLaunchPath:(NSString *)path { 80 | self = [self init]; 81 | if (self) { 82 | self.launchPath = path; 83 | } 84 | return self; 85 | } 86 | 87 | - (instancetype)initWithLaunchPath:(NSString *)path 88 | arguments:(NSArray *)args { 89 | self = [self initWithLaunchPath:path]; 90 | if (self) { 91 | self.arguments = args; 92 | } 93 | return self; 94 | } 95 | 96 | - (instancetype)initWithLaunchPath:(NSString *)path 97 | arguments:(NSArray *)args 98 | currentDirectory:(NSString *)cwd { 99 | self = [self initWithLaunchPath:path arguments:args]; 100 | if (self) { 101 | self.currentDirectoryPath = cwd; 102 | } 103 | return self; 104 | } 105 | 106 | #pragma mark - 107 | 108 | + (STPrivilegedTask *)launchedPrivilegedTaskWithLaunchPath:(NSString *)path { 109 | STPrivilegedTask *task = [[STPrivilegedTask alloc] initWithLaunchPath:path]; 110 | [task launch]; 111 | [task waitUntilExit]; 112 | return task; 113 | } 114 | 115 | + (STPrivilegedTask *)launchedPrivilegedTaskWithLaunchPath:(NSString *)path arguments:(NSArray *)args { 116 | STPrivilegedTask *task = [[STPrivilegedTask alloc] initWithLaunchPath:path arguments:args]; 117 | [task launch]; 118 | [task waitUntilExit]; 119 | return task; 120 | } 121 | 122 | + (STPrivilegedTask *)launchedPrivilegedTaskWithLaunchPath:(NSString *)path 123 | arguments:(NSArray *)args 124 | currentDirectory:(NSString *)cwd { 125 | STPrivilegedTask *task = [[STPrivilegedTask alloc] initWithLaunchPath:path arguments:args currentDirectory:cwd]; 126 | [task launch]; 127 | [task waitUntilExit]; 128 | return task; 129 | } 130 | 131 | + (STPrivilegedTask *)launchedPrivilegedTaskWithLaunchPath:(NSString *)path 132 | arguments:(NSArray *)args 133 | currentDirectory:(NSString *)cwd 134 | authorization:(AuthorizationRef)authorization { 135 | STPrivilegedTask *task = [[STPrivilegedTask alloc] initWithLaunchPath:path arguments:args currentDirectory:cwd]; 136 | [task launchWithAuthorization:authorization]; 137 | [task waitUntilExit]; 138 | return task; 139 | } 140 | 141 | # pragma mark - 142 | 143 | // return 0 for success 144 | - (OSStatus)launch { 145 | if (_isRunning) { 146 | NSLog(@"Task already running: %@", [self description]); 147 | return 0; 148 | } 149 | 150 | if ([STPrivilegedTask authorizationFunctionAvailable] == NO) { 151 | NSLog(@"AuthorizationExecuteWithPrivileges() function not available on this system"); 152 | return errAuthorizationFnNoLongerExists; 153 | } 154 | 155 | OSStatus err = noErr; 156 | const char *toolPath = [self.launchPath fileSystemRepresentation]; 157 | 158 | AuthorizationRef authorizationRef; 159 | AuthorizationItem myItems = { kAuthorizationRightExecute, strlen(toolPath), &toolPath, 0 }; 160 | AuthorizationRights myRights = { 1, &myItems }; 161 | AuthorizationFlags flags = kAuthorizationFlagDefaults | kAuthorizationFlagInteractionAllowed | kAuthorizationFlagPreAuthorize | kAuthorizationFlagExtendRights; 162 | 163 | // Use Apple's Authentication Manager API to create an Authorization Reference 164 | err = AuthorizationCreate(NULL, kAuthorizationEmptyEnvironment, kAuthorizationFlagDefaults, &authorizationRef); 165 | if (err != errAuthorizationSuccess) { 166 | return err; 167 | } 168 | 169 | // Pre-authorize the privileged operation 170 | err = AuthorizationCopyRights(authorizationRef, &myRights, kAuthorizationEmptyEnvironment, flags, NULL); 171 | if (err != errAuthorizationSuccess) { 172 | return err; 173 | } 174 | 175 | // OK, at this point we have received authorization for the task so we launch it. 176 | err = [self launchWithAuthorization:authorizationRef]; 177 | 178 | // Free the auth ref 179 | AuthorizationFree(authorizationRef, kAuthorizationFlagDefaults); 180 | 181 | return err; 182 | } 183 | 184 | - (OSStatus)launchWithAuthorization:(AuthorizationRef)authorization { 185 | if (_isRunning) { 186 | NSLog(@"Task already running: %@", [self description]); 187 | return 0; 188 | } 189 | 190 | if ([STPrivilegedTask authorizationFunctionAvailable] == NO) { 191 | NSLog(@"AuthorizationExecuteWithPrivileges() function not available on this system"); 192 | return errAuthorizationFnNoLongerExists; 193 | } 194 | 195 | // Assuming the authorization is valid for the task. 196 | // Let's prepare to launch it. 197 | NSArray *arguments = self.arguments; 198 | NSUInteger numArgs = [arguments count]; 199 | char *args[numArgs + 1]; 200 | FILE *outputFile; 201 | 202 | const char *toolPath = [self.launchPath fileSystemRepresentation]; 203 | 204 | // First, construct an array of C strings w. all the arguments from NSArray 205 | // This is the format required by AuthorizationExecuteWithPrivileges function 206 | for (int i = 0; i < numArgs; i++) { 207 | NSString *argString = arguments[i]; 208 | const char *fsrep = [argString fileSystemRepresentation]; 209 | NSUInteger stringLength = strlen(fsrep); 210 | args[i] = calloc((stringLength + 1), sizeof(char)); 211 | snprintf(args[i], stringLength + 1, "%s", fsrep); 212 | } 213 | args[numArgs] = NULL; 214 | 215 | // Change to the specified current working directory 216 | // NB: This is process-wide and could interfere with the behaviour of concurrent tasks 217 | char *prevCwd = (char *)getcwd(nil, 0); 218 | chdir([self.currentDirectoryPath fileSystemRepresentation]); 219 | 220 | // Use Authorization Reference to execute script with privileges. 221 | // This is where the magic happens. 222 | OSStatus err = _AuthExecuteWithPrivsFn(authorization, toolPath, kAuthorizationFlagDefaults, args, &outputFile); 223 | 224 | // OK, now we're done executing, let's change back to old dir 225 | chdir(prevCwd); 226 | 227 | // Free the alloc'd argument strings 228 | for (int i = 0; i < numArgs; i++) { 229 | free(args[i]); 230 | } 231 | 232 | // We return err if execution failed 233 | if (err != errAuthorizationSuccess) { 234 | return err; 235 | } else { 236 | _isRunning = YES; 237 | } 238 | 239 | // Get file handle for the command output 240 | _outputFileHandle = [[NSFileHandle alloc] initWithFileDescriptor:fileno(outputFile) closeOnDealloc:YES]; 241 | _processIdentifier = fcntl(fileno(outputFile), F_GETOWN, 0); 242 | 243 | // Start monitoring task 244 | _checkStatusTimer = [NSTimer scheduledTimerWithTimeInterval:0.05 target:self selector:@selector(checkTaskStatus) userInfo:nil repeats:YES]; 245 | 246 | return err; 247 | } 248 | 249 | - (void)terminate { 250 | // This doesn't work without a PID, and we can't get one. Stupid Security API. 251 | // int ret = kill(pid, SIGKILL); 252 | // 253 | // if (ret != 0) { 254 | // NSLog(@"Error %d", errno); 255 | // } 256 | } 257 | 258 | // Hang until task is done 259 | - (void)waitUntilExit { 260 | if (!_isRunning) { 261 | NSLog(@"Task %@ is not running", [super description]); 262 | return; 263 | } 264 | 265 | [_checkStatusTimer invalidate]; 266 | 267 | int status; 268 | pid_t pid = 0; 269 | while ((pid = waitpid(_processIdentifier, &status, WNOHANG)) == 0) { 270 | // Do nothing 271 | } 272 | _isRunning = NO; 273 | _terminationStatus = WEXITSTATUS(status); 274 | } 275 | 276 | // Check if task has terminated 277 | - (void)checkTaskStatus { 278 | int status; 279 | pid_t pid = waitpid(_processIdentifier, &status, WNOHANG); 280 | if (pid != 0) { 281 | _isRunning = NO; 282 | _terminationStatus = WEXITSTATUS(status); 283 | [_checkStatusTimer invalidate]; 284 | [[NSNotificationCenter defaultCenter] postNotificationName:STPrivilegedTaskDidTerminateNotification object:self]; 285 | if (_terminationHandler) { 286 | _terminationHandler(self); 287 | } 288 | } 289 | } 290 | 291 | #pragma mark - 292 | 293 | + (BOOL)authorizationFunctionAvailable { 294 | if (!_AuthExecuteWithPrivsFn) { 295 | // This version of macOS has finally removed this function. 296 | return NO; 297 | } 298 | return YES; 299 | } 300 | 301 | #pragma mark - 302 | 303 | // Nice description for debugging purposes 304 | - (NSString *)description { 305 | NSString *commandDescription = [NSString stringWithString:self.launchPath]; 306 | 307 | for (NSString *arg in self.arguments) { 308 | commandDescription = [commandDescription stringByAppendingFormat:@" '%@'", arg]; 309 | } 310 | [commandDescription stringByAppendingFormat:@" (CWD:%@)", self.currentDirectoryPath]; 311 | 312 | return [[super description] stringByAppendingFormat:@" %@", commandDescription]; 313 | } 314 | 315 | @end 316 | -------------------------------------------------------------------------------- /AppDelegate.mm: -------------------------------------------------------------------------------- 1 | #import "AppDelegate.h" 2 | #import "STPrivilegedTask.h" 3 | 4 | #include 5 | 6 | #ifdef __aarch64__ 7 | #define ARCH_KEY_NAME @"aarch64" 8 | #else 9 | #define ARCH_KEY_NAME @"x86_64" 10 | #endif 11 | 12 | const NSTimeInterval kDefaultTimeoutSecs = 60 * 60 * 12; // 12 hours 13 | 14 | void showErrorModal(NSString* errorDecription) { 15 | NSDictionary* info = [[NSBundle mainBundle] infoDictionary]; 16 | NSDictionary* downloadURLs = [info objectForKey:@"TargetDownloadURLs"]; 17 | NSURL* downloadURL = [NSURL URLWithString:[downloadURLs valueForKey:ARCH_KEY_NAME]]; 18 | NSString* targetAppName = [info valueForKey:@"TargetAppName"]; 19 | 20 | NSString* errorText = @""; 21 | if (errorDecription) { 22 | NSString* contactSupportMessage = 23 | [NSString stringWithFormat:(NSLocalizedString(@"errorDialog.contactSupportMessage", nil)), 24 | targetAppName]; 25 | errorText = [NSString stringWithFormat:@"\n\n%@\n\n%@", contactSupportMessage, errorDecription]; 26 | } 27 | 28 | NSAlert* alert = [[NSAlert alloc] init]; 29 | [alert addButtonWithTitle:(NSLocalizedString(@"errorDialog.downloadManuallyButton", nil))]; 30 | [alert addButtonWithTitle:(NSLocalizedString(@"dialog.cancelButton", nil))]; 31 | [alert setMessageText:[NSString stringWithFormat:(NSLocalizedString(@"errorDialog.title", nil)), 32 | targetAppName]]; 33 | NSString* downloadManuallyMessage = 34 | [NSString stringWithFormat:(NSLocalizedString(@"errorDialog.downloadManuallyMessage", nil)), 35 | targetAppName]; 36 | [alert 37 | setInformativeText:[NSString stringWithFormat:@"%@%@", downloadManuallyMessage, errorText]]; 38 | [alert setAlertStyle:NSAlertStyleCritical]; 39 | 40 | [NSApp activateIgnoringOtherApps:YES]; 41 | if ([alert runModal] == NSAlertFirstButtonReturn) { 42 | [[NSWorkspace sharedWorkspace] openURL:downloadURL]; 43 | } 44 | 45 | [NSApp terminate:nullptr]; 46 | } 47 | 48 | void runCommand(NSString* path, NSArray* arg) { 49 | NSTask* task = [[NSTask alloc] init]; 50 | [task setLaunchPath:path]; 51 | [task setArguments:arg]; 52 | [task launch]; 53 | [task waitUntilExit]; 54 | } 55 | 56 | void showErrorModal(NSError* error) { 57 | auto* errorDescription = 58 | [NSString stringWithFormat:(NSLocalizedString(@"error.errorWithCode", nil)), 59 | error.localizedDescription, error.code]; 60 | showErrorModal(errorDescription); 61 | } 62 | 63 | bool isBundleWritable() { 64 | auto* testDir = [NSBundle.mainBundle.bundlePath stringByAppendingPathComponent:@"test"]; 65 | auto* fileManager = NSFileManager.defaultManager; 66 | bool result = [fileManager createDirectoryAtPath:testDir 67 | withIntermediateDirectories:true 68 | attributes:nil 69 | error:nil]; 70 | if (result) { 71 | [fileManager removeItemAtPath:testDir error:nil]; 72 | return true; 73 | } else { 74 | return false; 75 | } 76 | } 77 | 78 | @interface AppDelegate () 79 | @property(weak) IBOutlet NSWindow* window; 80 | @property(weak) IBOutlet NSTextField* label; 81 | @property(weak) IBOutlet NSProgressIndicator* progressIndicator; 82 | @property(weak) IBOutlet NSButton* cancelButton; 83 | @property(weak) IBOutlet NSMenuItem* quitMenuItem; 84 | 85 | @property NSURLSessionDownloadTask* task; 86 | @end 87 | 88 | @implementation AppDelegate 89 | 90 | - (void)applicationWillFinishLaunching:(NSNotification*)notification { 91 | NSFileManager *fileManager = [NSFileManager defaultManager]; 92 | 93 | // Localize menu items 94 | NSString* installerName = [NSBundle.mainBundle.infoDictionary valueForKey:@"CFBundleName"]; 95 | self.quitMenuItem.title = 96 | [NSString stringWithFormat:NSLocalizedString(@"menu.quitItem", nil), installerName]; 97 | 98 | if ( 99 | // On macOS 10.12+, app bundles downloaded from the internet are launched 100 | // from a randomized path until the user moves it to another folder with 101 | // Finder. See: https://github.com/potionfactory/LetsMove/issues/56 102 | [NSBundle.mainBundle.bundlePath hasPrefix:@"/private/var/folders/"] || 103 | 104 | // Make sure the installer isn't being launched from a read-only location 105 | // like inside a DMG. 106 | ![fileManager isWritableFileAtPath:NSBundle.mainBundle.bundlePath] 107 | ) { 108 | NSDictionary* info = NSBundle.mainBundle.infoDictionary; 109 | NSString* targetAppName = [info valueForKey:@"TargetAppName"]; 110 | 111 | NSAlert* alert = [[NSAlert alloc] init]; 112 | [alert addButtonWithTitle:NSLocalizedString(@"dialog.okButton", nil)]; 113 | [alert setMessageText:NSLocalizedString(@"moveToApplicationsDialog.title", nil)]; 114 | [alert setInformativeText:[NSString 115 | stringWithFormat:(NSLocalizedString( 116 | @"moveToApplicationsDialog.message", nil)), 117 | targetAppName]]; 118 | [alert setAlertStyle:NSAlertStyleCritical]; 119 | [alert runModal]; 120 | [NSApp terminate:nullptr]; 121 | } 122 | 123 | // First check if trying to run `sh -c ...` works. We'll be using it at 124 | // the end to relaunch the app. 125 | NSTask* checkTask = [[NSTask alloc] init]; 126 | [checkTask setLaunchPath:@"/bin/sh"]; 127 | [checkTask setArguments:@[ @"-c", @"sleep 0 && which open && which unzip" ]]; 128 | [checkTask launch]; 129 | [checkTask waitUntilExit]; 130 | if (checkTask.terminationStatus != 0) { 131 | showErrorModal([NSString stringWithFormat:(NSLocalizedString(@"error.initialCheckFailed", nil)), 132 | checkTask.terminationStatus]); 133 | return; 134 | } 135 | } 136 | 137 | - (void)applicationDidFinishLaunching:(NSNotification*)notification { 138 | NSDictionary* info = NSBundle.mainBundle.infoDictionary; 139 | NSDictionary* downloadURLs = [info objectForKey:@"TargetDownloadURLs"]; 140 | NSURL* downloadURL = [NSURL URLWithString:[downloadURLs valueForKey:ARCH_KEY_NAME]]; 141 | NSString* targetAppName = [info valueForKey:@"TargetAppName"]; 142 | 143 | if (!isBundleWritable()) { 144 | // If permission check fails, it's likely to be a standard user, let's rerun the program 145 | // as root 146 | 147 | if (geteuid() != 0) { 148 | STPrivilegedTask* task = [STPrivilegedTask new]; 149 | [task setLaunchPath:[NSBundle.mainBundle executablePath]]; 150 | [task launch]; 151 | } else { 152 | dispatch_async(dispatch_get_main_queue(), ^{ 153 | showErrorModal(NSLocalizedString(@"error.wrongPermissions", nil)); 154 | }); 155 | } 156 | return; 157 | } 158 | 159 | self.window.title = 160 | [NSString stringWithFormat:(NSLocalizedString(@"installerDialog.title", nil)), targetAppName]; 161 | self.label.stringValue = 162 | [NSString stringWithFormat:(NSLocalizedString(@"installerDialog.downloadingMessage", nil)), 163 | targetAppName]; 164 | self.cancelButton.title = NSLocalizedString(@"dialog.cancelButton", nil); 165 | 166 | [self.window setIsVisible:TRUE]; 167 | [[NSApplication sharedApplication] activateIgnoringOtherApps:YES]; 168 | 169 | // Fetch the platform specific build archive. 170 | NSURLRequest* request = [NSURLRequest requestWithURL:downloadURL 171 | cachePolicy:NSURLRequestReloadIgnoringLocalCacheData 172 | timeoutInterval:kDefaultTimeoutSecs]; 173 | self.task = [NSURLSession.sharedSession 174 | downloadTaskWithRequest:request 175 | completionHandler:^(NSURL* downloadLocation, NSURLResponse* response, NSError* error) { 176 | if (error) { 177 | dispatch_async(dispatch_get_main_queue(), ^{ 178 | showErrorModal(error); 179 | }); 180 | 181 | return; 182 | } 183 | 184 | if ([response isKindOfClass:NSHTTPURLResponse.class]) { 185 | const auto statusCode = ((NSHTTPURLResponse*)response).statusCode; 186 | if (statusCode < 200 || statusCode >= 300) { 187 | dispatch_async(dispatch_get_main_queue(), ^{ 188 | showErrorModal([NSString 189 | stringWithFormat:(NSLocalizedString(@"error.downloadFailed", nil)), 190 | statusCode]); 191 | }); 192 | 193 | return; 194 | } 195 | } 196 | 197 | dispatch_async(dispatch_get_main_queue(), ^{ 198 | self.label.stringValue = [NSString 199 | stringWithFormat:(NSLocalizedString(@"installerDialog.installingMessage", nil)), 200 | targetAppName]; 201 | }); 202 | 203 | // Big Sur and later have APIs to extract archives, but we need 204 | // to support older versions. Lets use plain old unzip to 205 | // extract the downloaded archive. 206 | // 207 | // TODO(poiru): Support XZ archives. 208 | auto* fileManager = NSFileManager.defaultManager; 209 | auto* tempDir = 210 | [NSBundle.mainBundle.bundlePath stringByAppendingPathComponent:@"download"]; 211 | 212 | NSTask* task = [[NSTask alloc] init]; 213 | [task setLaunchPath:@"/usr/bin/unzip"]; 214 | [task setArguments:@[ @"-qq", @"-o", @"-d", tempDir, downloadLocation.path ]]; 215 | [task launch]; 216 | [task waitUntilExit]; 217 | [fileManager removeItemAtPath:downloadLocation.path error:nil]; 218 | 219 | if (task.terminationStatus != 0) { 220 | [fileManager removeItemAtPath:tempDir error:nil]; 221 | dispatch_async(dispatch_get_main_queue(), ^{ 222 | showErrorModal( 223 | [NSString stringWithFormat:(NSLocalizedString(@"error.extractFailed", nil)), 224 | task.terminationStatus]); 225 | }); 226 | return; 227 | } 228 | 229 | auto* sourcePath = [tempDir 230 | stringByAppendingPathComponent:[NSString 231 | stringWithFormat:@"%@.app", targetAppName]]; 232 | auto* targetPath = NSBundle.mainBundle.bundlePath; 233 | // Rename the final bundle in the temp directory to the target directory. 234 | // Lets first try using the rename() system call because it can do that 235 | // atomically even if the the target already exists. 236 | if (rename(sourcePath.fileSystemRepresentation, 237 | targetPath.fileSystemRepresentation) != 0) { 238 | // If rename() failed, try to do this by moving the contents instead 239 | // Advantage of this approach is that we don't need any special permissions if the 240 | // user is the owner of the folder (User initiated drag and drop) Disadvantage of 241 | // this approach is that the metadata update isn't reliable, if the icon of the app 242 | // and the replaced app are different, user may need a restart before the icon 243 | // updates in the dock. 244 | 245 | NSDirectoryEnumerator* enumerator = [fileManager enumeratorAtPath:sourcePath]; 246 | NSString* file; 247 | 248 | while (file = [enumerator nextObject]) { 249 | NSError* error = nil; 250 | BOOL result = rename( 251 | [sourcePath stringByAppendingPathComponent:file].fileSystemRepresentation, 252 | [targetPath stringByAppendingPathComponent:file].fileSystemRepresentation); 253 | 254 | if (!result && error) { 255 | dispatch_async(dispatch_get_main_queue(), ^{ 256 | showErrorModal(error); 257 | }); 258 | return; 259 | } 260 | } 261 | 262 | // Trigger icon updation 263 | runCommand(@"/usr/bin/touch", @[ NSBundle.mainBundle.bundlePath ]); 264 | runCommand(@"/usr/bin/touch", 265 | @[ [NSBundle.mainBundle.bundlePath 266 | stringByAppendingPathComponent:@"Contents/Info.plist"] ]); 267 | } 268 | 269 | [fileManager removeItemAtPath:tempDir error:nil]; 270 | 271 | dispatch_async(dispatch_get_main_queue(), ^{ 272 | [self launchInstalledApp]; 273 | }); 274 | }]; 275 | [self.task resume]; 276 | 277 | if (@available(macOS 10.13, *)) { 278 | [self.task.progress addObserver:self 279 | forKeyPath:@"fractionCompleted" 280 | options:NSKeyValueObservingOptionNew 281 | context:nil]; 282 | } else { 283 | self.progressIndicator.indeterminate = TRUE; 284 | } 285 | } 286 | 287 | - (void)observeValueForKeyPath:(NSString*)keyPath 288 | ofObject:(id)object 289 | change:(NSDictionary*)change 290 | context:(void*)context { 291 | if ([keyPath isEqual:@"fractionCompleted"]) { 292 | dispatch_async(dispatch_get_main_queue(), ^{ 293 | const auto value = [[change valueForKey:NSKeyValueChangeNewKey] doubleValue]; 294 | self.progressIndicator.doubleValue = fmax(self.progressIndicator.doubleValue, value * 0.95); 295 | }); 296 | } 297 | } 298 | 299 | - (void)applicationWillTerminate:(NSNotification*)notification { 300 | [self.task cancel]; 301 | } 302 | 303 | - (IBAction)cancelClicked:(id)sender { 304 | [self.task cancel]; 305 | [NSApp terminate:nullptr]; 306 | } 307 | 308 | - (void)launchInstalledApp { 309 | // Spawn a sh process to relaunch the installed app after we exit. Otherwise 310 | // the new app might not launch if this stub app is already running at the 311 | // path. 312 | NSTask* launchTask = [[NSTask alloc] init]; 313 | [launchTask setLaunchPath:@"/bin/sh"]; 314 | [launchTask setArguments:@[ 315 | @"-c", 316 | [NSString stringWithFormat:@"sleep 1; /usr/bin/open %s \"%@\"", 317 | self.window.isMainWindow ? "" : "-g", NSBundle.mainBundle.bundlePath] 318 | ]]; 319 | [launchTask launch]; 320 | [NSApp terminate:nullptr]; 321 | } 322 | 323 | @end 324 | -------------------------------------------------------------------------------- /DynamicUniversalApp.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 2722726C263AD71300AFC28A /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 2722726B263AD71300AFC28A /* AppDelegate.mm */; }; 11 | 27227274263AD71400AFC28A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 27227273263AD71400AFC28A /* main.m */; }; 12 | 27BF31042668EF7F001B8384 /* STPrivilegedTask.m in Sources */ = {isa = PBXBuildFile; fileRef = 27BF31032668EF7F001B8384 /* STPrivilegedTask.m */; }; 13 | 27D3053E263D28BC00BBEBA5 /* App.xib in Resources */ = {isa = PBXBuildFile; fileRef = 27227270263AD71400AFC28A /* App.xib */; }; 14 | 4000AD3C283D8A4800D07897 /* en.lproj in Resources */ = {isa = PBXBuildFile; fileRef = 4000AD3B283D8A4800D07897 /* en.lproj */; }; 15 | 40B4470C28511CFC00E28ED2 /* ja.lproj in Resources */ = {isa = PBXBuildFile; fileRef = 40B4470B28511CFC00E28ED2 /* ja.lproj */; }; 16 | 611C771B2DBB0CF000427809 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 611C77132DBB0CF000427809 /* Localizable.strings */; }; 17 | 611C771C2DBB0CF000427809 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 611C77162DBB0CF000427809 /* Localizable.strings */; }; 18 | 611C771D2DBB0CF000427809 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 611C77192DBB0CF000427809 /* Localizable.strings */; }; 19 | 6972FEFB2E0F384500CDE7A4 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 6972FEF92E0F384500CDE7A4 /* Localizable.strings */; }; 20 | D0753E2D2E97271800DF12DC /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = D0753E2B2E97271800DF12DC /* Localizable.strings */; }; 21 | D0753E352E97291C00DF12DC /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = D0753E332E97291C00DF12DC /* Localizable.strings */; }; 22 | /* End PBXBuildFile section */ 23 | 24 | /* Begin PBXFileReference section */ 25 | 27227267263AD71300AFC28A /* DynamicUniversalApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DynamicUniversalApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; 26 | 2722726A263AD71300AFC28A /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 27 | 2722726B263AD71300AFC28A /* AppDelegate.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = AppDelegate.mm; sourceTree = ""; }; 28 | 27227270263AD71400AFC28A /* App.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = App.xib; sourceTree = ""; }; 29 | 27227272263AD71400AFC28A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 30 | 27227273263AD71400AFC28A /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 31 | 27BF31032668EF7F001B8384 /* STPrivilegedTask.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = STPrivilegedTask.m; sourceTree = ""; }; 32 | 4000AD3B283D8A4800D07897 /* en.lproj */ = {isa = PBXFileReference; lastKnownFileType = folder; path = en.lproj; sourceTree = ""; }; 33 | 40B4470B28511CFC00E28ED2 /* ja.lproj */ = {isa = PBXFileReference; lastKnownFileType = folder; path = ja.lproj; sourceTree = ""; }; 34 | 611C77122DBB0CF000427809 /* es-es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-es"; path = Localizable.strings; sourceTree = ""; }; 35 | 611C77152DBB0CF000427809 /* ko-kr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "ko-kr"; path = Localizable.strings; sourceTree = ""; }; 36 | 611C77182DBB0CF000427809 /* pt-br */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-br"; path = Localizable.strings; sourceTree = ""; }; 37 | 6972FEF82E0F384500CDE7A4 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-419"; path = Localizable.strings; sourceTree = ""; }; 38 | D0753E2A2E97271800DF12DC /* fr-fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "fr-fr"; path = Localizable.strings; sourceTree = ""; }; 39 | D0753E322E97291C00DF12DC /* de-de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "de-de"; path = Localizable.strings; sourceTree = ""; }; 40 | /* End PBXFileReference section */ 41 | 42 | /* Begin PBXFrameworksBuildPhase section */ 43 | 27227264263AD71300AFC28A /* Frameworks */ = { 44 | isa = PBXFrameworksBuildPhase; 45 | buildActionMask = 2147483647; 46 | files = ( 47 | ); 48 | runOnlyForDeploymentPostprocessing = 0; 49 | }; 50 | /* End PBXFrameworksBuildPhase section */ 51 | 52 | /* Begin PBXGroup section */ 53 | 2722725E263AD71300AFC28A = { 54 | isa = PBXGroup; 55 | children = ( 56 | D0753E342E97291C00DF12DC /* de-de.lproj */, 57 | D0753E2C2E97271800DF12DC /* fr-fr.lproj */, 58 | 6972FEFA2E0F384500CDE7A4 /* es-419.lproj */, 59 | 40B4470B28511CFC00E28ED2 /* ja.lproj */, 60 | 4000AD3B283D8A4800D07897 /* en.lproj */, 61 | 2722726A263AD71300AFC28A /* AppDelegate.h */, 62 | 2722726B263AD71300AFC28A /* AppDelegate.mm */, 63 | 27227270263AD71400AFC28A /* App.xib */, 64 | 27227272263AD71400AFC28A /* Info.plist */, 65 | 27BF31032668EF7F001B8384 /* STPrivilegedTask.m */, 66 | 27227273263AD71400AFC28A /* main.m */, 67 | 27227268263AD71300AFC28A /* Products */, 68 | 611C77142DBB0CF000427809 /* es-es.lproj */, 69 | 611C77172DBB0CF000427809 /* ko-kr.lproj */, 70 | 611C771A2DBB0CF000427809 /* pt-br.lproj */, 71 | ); 72 | sourceTree = ""; 73 | }; 74 | 27227268263AD71300AFC28A /* Products */ = { 75 | isa = PBXGroup; 76 | children = ( 77 | 27227267263AD71300AFC28A /* DynamicUniversalApp.app */, 78 | ); 79 | name = Products; 80 | sourceTree = ""; 81 | }; 82 | 611C77142DBB0CF000427809 /* es-es.lproj */ = { 83 | isa = PBXGroup; 84 | children = ( 85 | 611C77132DBB0CF000427809 /* Localizable.strings */, 86 | ); 87 | path = "es-es.lproj"; 88 | sourceTree = ""; 89 | }; 90 | 611C77172DBB0CF000427809 /* ko-kr.lproj */ = { 91 | isa = PBXGroup; 92 | children = ( 93 | 611C77162DBB0CF000427809 /* Localizable.strings */, 94 | ); 95 | path = "ko-kr.lproj"; 96 | sourceTree = ""; 97 | }; 98 | 611C771A2DBB0CF000427809 /* pt-br.lproj */ = { 99 | isa = PBXGroup; 100 | children = ( 101 | 611C77192DBB0CF000427809 /* Localizable.strings */, 102 | ); 103 | path = "pt-br.lproj"; 104 | sourceTree = ""; 105 | }; 106 | 6972FEFA2E0F384500CDE7A4 /* es-419.lproj */ = { 107 | isa = PBXGroup; 108 | children = ( 109 | 6972FEF92E0F384500CDE7A4 /* Localizable.strings */, 110 | ); 111 | path = "es-419.lproj"; 112 | sourceTree = ""; 113 | }; 114 | D0753E2C2E97271800DF12DC /* fr-fr.lproj */ = { 115 | isa = PBXGroup; 116 | children = ( 117 | D0753E2B2E97271800DF12DC /* Localizable.strings */, 118 | ); 119 | path = "fr-fr.lproj"; 120 | sourceTree = ""; 121 | }; 122 | D0753E342E97291C00DF12DC /* de-de.lproj */ = { 123 | isa = PBXGroup; 124 | children = ( 125 | D0753E332E97291C00DF12DC /* Localizable.strings */, 126 | ); 127 | path = "de-de.lproj"; 128 | sourceTree = ""; 129 | }; 130 | /* End PBXGroup section */ 131 | 132 | /* Begin PBXNativeTarget section */ 133 | 27227266263AD71300AFC28A /* DynamicUniversalApp */ = { 134 | isa = PBXNativeTarget; 135 | buildConfigurationList = 27227278263AD71400AFC28A /* Build configuration list for PBXNativeTarget "DynamicUniversalApp" */; 136 | buildPhases = ( 137 | 27227263263AD71300AFC28A /* Sources */, 138 | 27227264263AD71300AFC28A /* Frameworks */, 139 | 27227265263AD71300AFC28A /* Resources */, 140 | ); 141 | buildRules = ( 142 | ); 143 | dependencies = ( 144 | ); 145 | name = DynamicUniversalApp; 146 | productName = SlimBundle; 147 | productReference = 27227267263AD71300AFC28A /* DynamicUniversalApp.app */; 148 | productType = "com.apple.product-type.application"; 149 | }; 150 | /* End PBXNativeTarget section */ 151 | 152 | /* Begin PBXProject section */ 153 | 2722725F263AD71300AFC28A /* Project object */ = { 154 | isa = PBXProject; 155 | attributes = { 156 | LastUpgradeCheck = 1130; 157 | ORGANIZATIONNAME = "Figma, Inc."; 158 | TargetAttributes = { 159 | 27227266263AD71300AFC28A = { 160 | CreatedOnToolsVersion = 11.3.1; 161 | }; 162 | }; 163 | }; 164 | buildConfigurationList = 27227262263AD71300AFC28A /* Build configuration list for PBXProject "DynamicUniversalApp" */; 165 | compatibilityVersion = "Xcode 9.3"; 166 | developmentRegion = en; 167 | hasScannedForEncodings = 0; 168 | knownRegions = ( 169 | en, 170 | Base, 171 | ja, 172 | "es-es", 173 | "ko-kr", 174 | "pt-br", 175 | "ko-KR", 176 | "pt-BR", 177 | "es-ES", 178 | "es-419", 179 | "fr-fr", 180 | "fr-FR", 181 | "de-de", 182 | "de-DE", 183 | ); 184 | mainGroup = 2722725E263AD71300AFC28A; 185 | productRefGroup = 27227268263AD71300AFC28A /* Products */; 186 | projectDirPath = ""; 187 | projectRoot = ""; 188 | targets = ( 189 | 27227266263AD71300AFC28A /* DynamicUniversalApp */, 190 | ); 191 | }; 192 | /* End PBXProject section */ 193 | 194 | /* Begin PBXResourcesBuildPhase section */ 195 | 27227265263AD71300AFC28A /* Resources */ = { 196 | isa = PBXResourcesBuildPhase; 197 | buildActionMask = 2147483647; 198 | files = ( 199 | 27D3053E263D28BC00BBEBA5 /* App.xib in Resources */, 200 | 40B4470C28511CFC00E28ED2 /* ja.lproj in Resources */, 201 | 4000AD3C283D8A4800D07897 /* en.lproj in Resources */, 202 | 6972FEFB2E0F384500CDE7A4 /* Localizable.strings in Resources */, 203 | D0753E352E97291C00DF12DC /* Localizable.strings in Resources */, 204 | 611C771B2DBB0CF000427809 /* Localizable.strings in Resources */, 205 | D0753E2D2E97271800DF12DC /* Localizable.strings in Resources */, 206 | 611C771C2DBB0CF000427809 /* Localizable.strings in Resources */, 207 | 611C771D2DBB0CF000427809 /* Localizable.strings in Resources */, 208 | ); 209 | runOnlyForDeploymentPostprocessing = 0; 210 | }; 211 | /* End PBXResourcesBuildPhase section */ 212 | 213 | /* Begin PBXSourcesBuildPhase section */ 214 | 27227263263AD71300AFC28A /* Sources */ = { 215 | isa = PBXSourcesBuildPhase; 216 | buildActionMask = 2147483647; 217 | files = ( 218 | 27227274263AD71400AFC28A /* main.m in Sources */, 219 | 2722726C263AD71300AFC28A /* AppDelegate.mm in Sources */, 220 | 27BF31042668EF7F001B8384 /* STPrivilegedTask.m in Sources */, 221 | ); 222 | runOnlyForDeploymentPostprocessing = 0; 223 | }; 224 | /* End PBXSourcesBuildPhase section */ 225 | 226 | /* Begin PBXVariantGroup section */ 227 | 611C77132DBB0CF000427809 /* Localizable.strings */ = { 228 | isa = PBXVariantGroup; 229 | children = ( 230 | 611C77122DBB0CF000427809 /* es-es */, 231 | ); 232 | name = Localizable.strings; 233 | sourceTree = ""; 234 | }; 235 | 611C77162DBB0CF000427809 /* Localizable.strings */ = { 236 | isa = PBXVariantGroup; 237 | children = ( 238 | 611C77152DBB0CF000427809 /* ko-kr */, 239 | ); 240 | name = Localizable.strings; 241 | sourceTree = ""; 242 | }; 243 | 611C77192DBB0CF000427809 /* Localizable.strings */ = { 244 | isa = PBXVariantGroup; 245 | children = ( 246 | 611C77182DBB0CF000427809 /* pt-br */, 247 | ); 248 | name = Localizable.strings; 249 | sourceTree = ""; 250 | }; 251 | 6972FEF92E0F384500CDE7A4 /* Localizable.strings */ = { 252 | isa = PBXVariantGroup; 253 | children = ( 254 | 6972FEF82E0F384500CDE7A4 /* es-419 */, 255 | ); 256 | name = Localizable.strings; 257 | sourceTree = ""; 258 | }; 259 | D0753E2B2E97271800DF12DC /* Localizable.strings */ = { 260 | isa = PBXVariantGroup; 261 | children = ( 262 | D0753E2A2E97271800DF12DC /* fr-fr */, 263 | ); 264 | name = Localizable.strings; 265 | sourceTree = ""; 266 | }; 267 | D0753E332E97291C00DF12DC /* Localizable.strings */ = { 268 | isa = PBXVariantGroup; 269 | children = ( 270 | D0753E322E97291C00DF12DC /* de-de */, 271 | ); 272 | name = Localizable.strings; 273 | sourceTree = ""; 274 | }; 275 | /* End PBXVariantGroup section */ 276 | 277 | /* Begin XCBuildConfiguration section */ 278 | 27227276263AD71400AFC28A /* Debug */ = { 279 | isa = XCBuildConfiguration; 280 | buildSettings = { 281 | ALWAYS_SEARCH_USER_PATHS = NO; 282 | CLANG_ANALYZER_NONNULL = YES; 283 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 284 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 285 | CLANG_CXX_LIBRARY = "libc++"; 286 | CLANG_ENABLE_MODULES = YES; 287 | CLANG_ENABLE_OBJC_ARC = YES; 288 | CLANG_ENABLE_OBJC_WEAK = YES; 289 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 290 | CLANG_WARN_BOOL_CONVERSION = YES; 291 | CLANG_WARN_COMMA = YES; 292 | CLANG_WARN_CONSTANT_CONVERSION = YES; 293 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 294 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 295 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 296 | CLANG_WARN_EMPTY_BODY = YES; 297 | CLANG_WARN_ENUM_CONVERSION = YES; 298 | CLANG_WARN_INFINITE_RECURSION = YES; 299 | CLANG_WARN_INT_CONVERSION = YES; 300 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 301 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 302 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 303 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 304 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 305 | CLANG_WARN_STRICT_PROTOTYPES = YES; 306 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 307 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 308 | CLANG_WARN_UNREACHABLE_CODE = YES; 309 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 310 | COPY_PHASE_STRIP = NO; 311 | DEBUG_INFORMATION_FORMAT = dwarf; 312 | ENABLE_STRICT_OBJC_MSGSEND = YES; 313 | ENABLE_TESTABILITY = YES; 314 | GCC_C_LANGUAGE_STANDARD = gnu11; 315 | GCC_DYNAMIC_NO_PIC = NO; 316 | GCC_NO_COMMON_BLOCKS = YES; 317 | GCC_OPTIMIZATION_LEVEL = 0; 318 | GCC_PREPROCESSOR_DEFINITIONS = ( 319 | "DEBUG=1", 320 | "$(inherited)", 321 | ); 322 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 323 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 324 | GCC_WARN_UNDECLARED_SELECTOR = YES; 325 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 326 | GCC_WARN_UNUSED_FUNCTION = YES; 327 | GCC_WARN_UNUSED_VARIABLE = YES; 328 | MACOSX_DEPLOYMENT_TARGET = 10.14; 329 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 330 | MTL_FAST_MATH = YES; 331 | ONLY_ACTIVE_ARCH = YES; 332 | SDKROOT = macosx; 333 | }; 334 | name = Debug; 335 | }; 336 | 27227277263AD71400AFC28A /* Release */ = { 337 | isa = XCBuildConfiguration; 338 | buildSettings = { 339 | ALWAYS_SEARCH_USER_PATHS = NO; 340 | CLANG_ANALYZER_NONNULL = YES; 341 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 342 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 343 | CLANG_CXX_LIBRARY = "libc++"; 344 | CLANG_ENABLE_MODULES = YES; 345 | CLANG_ENABLE_OBJC_ARC = YES; 346 | CLANG_ENABLE_OBJC_WEAK = YES; 347 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 348 | CLANG_WARN_BOOL_CONVERSION = YES; 349 | CLANG_WARN_COMMA = YES; 350 | CLANG_WARN_CONSTANT_CONVERSION = YES; 351 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 352 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 353 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 354 | CLANG_WARN_EMPTY_BODY = YES; 355 | CLANG_WARN_ENUM_CONVERSION = YES; 356 | CLANG_WARN_INFINITE_RECURSION = YES; 357 | CLANG_WARN_INT_CONVERSION = YES; 358 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 359 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 360 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 361 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 362 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 363 | CLANG_WARN_STRICT_PROTOTYPES = YES; 364 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 365 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 366 | CLANG_WARN_UNREACHABLE_CODE = YES; 367 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 368 | COPY_PHASE_STRIP = NO; 369 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 370 | ENABLE_NS_ASSERTIONS = NO; 371 | ENABLE_STRICT_OBJC_MSGSEND = YES; 372 | GCC_C_LANGUAGE_STANDARD = gnu11; 373 | GCC_NO_COMMON_BLOCKS = YES; 374 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 375 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 376 | GCC_WARN_UNDECLARED_SELECTOR = YES; 377 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 378 | GCC_WARN_UNUSED_FUNCTION = YES; 379 | GCC_WARN_UNUSED_VARIABLE = YES; 380 | MACOSX_DEPLOYMENT_TARGET = 10.14; 381 | MTL_ENABLE_DEBUG_INFO = NO; 382 | MTL_FAST_MATH = YES; 383 | SDKROOT = macosx; 384 | }; 385 | name = Release; 386 | }; 387 | 27227279263AD71400AFC28A /* Debug */ = { 388 | isa = XCBuildConfiguration; 389 | buildSettings = { 390 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 391 | CODE_SIGN_STYLE = Automatic; 392 | COMBINE_HIDPI_IMAGES = YES; 393 | INFOPLIST_FILE = Info.plist; 394 | LD_RUNPATH_SEARCH_PATHS = ( 395 | "$(inherited)", 396 | "@executable_path/../Frameworks", 397 | ); 398 | MACOSX_DEPLOYMENT_TARGET = 10.12; 399 | PRODUCT_BUNDLE_IDENTIFIER = "com.figma.dynamic-universal-app"; 400 | PRODUCT_NAME = "$(TARGET_NAME)"; 401 | }; 402 | name = Debug; 403 | }; 404 | 2722727A263AD71400AFC28A /* Release */ = { 405 | isa = XCBuildConfiguration; 406 | buildSettings = { 407 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 408 | CODE_SIGN_STYLE = Automatic; 409 | COMBINE_HIDPI_IMAGES = YES; 410 | INFOPLIST_FILE = Info.plist; 411 | LD_RUNPATH_SEARCH_PATHS = ( 412 | "$(inherited)", 413 | "@executable_path/../Frameworks", 414 | ); 415 | MACOSX_DEPLOYMENT_TARGET = 10.12; 416 | PRODUCT_BUNDLE_IDENTIFIER = "com.figma.dynamic-universal-app"; 417 | PRODUCT_NAME = "$(TARGET_NAME)"; 418 | }; 419 | name = Release; 420 | }; 421 | /* End XCBuildConfiguration section */ 422 | 423 | /* Begin XCConfigurationList section */ 424 | 27227262263AD71300AFC28A /* Build configuration list for PBXProject "DynamicUniversalApp" */ = { 425 | isa = XCConfigurationList; 426 | buildConfigurations = ( 427 | 27227276263AD71400AFC28A /* Debug */, 428 | 27227277263AD71400AFC28A /* Release */, 429 | ); 430 | defaultConfigurationIsVisible = 0; 431 | defaultConfigurationName = Release; 432 | }; 433 | 27227278263AD71400AFC28A /* Build configuration list for PBXNativeTarget "DynamicUniversalApp" */ = { 434 | isa = XCConfigurationList; 435 | buildConfigurations = ( 436 | 27227279263AD71400AFC28A /* Debug */, 437 | 2722727A263AD71400AFC28A /* Release */, 438 | ); 439 | defaultConfigurationIsVisible = 0; 440 | defaultConfigurationName = Release; 441 | }; 442 | /* End XCConfigurationList section */ 443 | }; 444 | rootObject = 2722725F263AD71300AFC28A /* Project object */; 445 | } 446 | --------------------------------------------------------------------------------