diff --git a/macos_development/codesign_macos.sh b/macos_development/codesign_macos.sh new file mode 100755 index 0000000..0b8c360 --- /dev/null +++ b/macos_development/codesign_macos.sh @@ -0,0 +1,36 @@ +#!/bin/bash +# +# Example Usage: +# +# ./codesign_macos.sh test.app/Contents/MacOs/test ./ +# +# To use this file properly, you need to create a signing identity, and +# replace the string with the code Apple provides. +# See ./how_to_setup_for_macos_and_ios_development.md for instructions + +CODE_SIGN_IDENTITY="" + +PATH_TO_EXECUTABLE=$1 +PATH_TO_ENTITLEMENTS_FILE=$2 + +app_entitlements_file="$PATH_TO_ENTITLEMENTS_FILE/Entitlements.xcent" + +echo "Codesigning..." +echo " Executable: ${PATH_TO_EXECUTABLE}" +echo " Entitlements {app_entitlements_file}" +echo + +# Do the actual codesigning +codesign \ + --generate-entitlement-der \ + --force \ + --timestamp\=none \ + --entitlements ${app_entitlements_file} \ + --sign $CODE_SIGN_IDENTITY \ + ${PATH_TO_EXECUTABLE} + +if [ $? -ne 0 ]; then + echo + echo " Code signing Failed" + exit 1 +fi \ No newline at end of file diff --git a/macos_development/how_to_debug_in_xcode.md b/macos_development/how_to_debug_in_xcode.md new file mode 100644 index 0000000..182020b --- /dev/null +++ b/macos_development/how_to_debug_in_xcode.md @@ -0,0 +1,41 @@ +# Debugging on OSX + +**TODO: Figure out how to set breakpoints in Xcode** +**TODO: Figure out why Xcode detaches from the process after you take a Metal snapshot, leavin the window there forever** + +## Basics + +1. Open XCode +2. Create a new Project using the External Build Tool configuration +3. Set the Build Tool for the target to either: + - `/usr/bin/true` - this is essentially a no-op if you want to be building elsewhere + - `../bin/build.sh` (or similar) - the path to your build script. IMPORTANT: it seems Xcode doesn't resolve absolute paths, just paths relative to the project location. +4. With the project open go to `Product > Scheme > Edit Scheme > Run > Info > Executable` and select the executable you want to run. +5. Cmd+R to Run. + +Debugging this way, you can have Xcode run an executable in a .app or just from any directory. There do seem to be differences in the ways some graphics systems are initialized - I get different window scaling in each case. + +## Graphics + +### OpenGL +_I didn't have any luck with any of Xcode's graphics debugging utilities while developing for OpenGL._ + +### Metal +If you want to use XCode's Metal Graphics Debugging utilities, follow these steps: + +1. `Product > Scheme > Edit Scheme > Diagnostics` +2. Turn on/off any options in the Metal section at the bottom. + +*Note: Some of these will crash your app if you fail their checks.* + +## Errors + +**The specified architecture 'arm64-*-*' is not compatible with 'x86_64-apple-macosx10.13.0' in '/Users/ps/work/minos/p_scratch/package/scratch.app/Contents/MacOS/scratch'** +_This will happen if you are trying to debug an x86/x86_64 executable on an arm machine._ + +1. Go to Product > Destination > Destination Architectures > Show Both +2. Select Intel (Rosetta) + +## Sources: +- [Basics](https://developer.apple.com/forums/thread/65025) +- [Video](https://www.youtube.com/watch?v=QVcjh6Kvnyw) \ No newline at end of file diff --git a/macos_development/how_to_setup_for_macos_and_ios_development.md b/macos_development/how_to_setup_for_macos_and_ios_development.md new file mode 100644 index 0000000..ecba9ca --- /dev/null +++ b/macos_development/how_to_setup_for_macos_and_ios_development.md @@ -0,0 +1,618 @@ +# How to Build and Run macOS and iOS Apps Without Opening Xcode\* +\*(more than once) + +Updated June 21, 2023. + +_Note (PS): This is not my document - it came from a post on the Handmade Network forums, that I can no longer find. I found it really useful and so I'm sticking it here so I can find it later, but all credit goes to whoever wrote this initially. (If you read this and know who it is, let me know so I can credit properly)._ + +## Prerequisites + +* A macOS device + +And, if you're building for iOS: + +* An iOS device +* A wired connection from the iOS device to the macOS device + +## Setting Up a Development Environment +Revisit these steps any time you add a new team member, development device, test device, or app. Otherwise, you'll do this once and won't need to revisit it until you're shipping builds to the public. + +If you're only developing non-GUI applications for macOS, first download Xcode then skip straight to the section on Building \"Hello, World!\" below. + +### Create an Apple Developer Account +Visit https://developer.apple.com/, follow the account creation steps, and pay the toll to set up a Developer account. + +### Download Xcode +Yes, you still need it, and you do need to open it at least once. It contains the platform SDKs we need. + +1. Open the App Store on the macOS device you intend to develop on (it's accessible via the Apple icon in the menu bar). +2. Login with your Apple Developer account (or another Apple account of your choosing--it doesn't need to be your Developer account). +3. Search the App Store for Xcode and install it. +4. Open Xcode. It will prompt you to install the SDKs for the platforms you wish to develop on. Keep it open once it's done, as we'll need to come back to Xcode later in the set up process. + +### Create an Apple Development Certificate +Every unique macOS device you intend to develop on requires a Development Certificate, set up in the Apple Developer portal. + +1. Log in to your Apple Developer account and visit https://developer.apple.com/account/resources/certificates/list. +2. Click the + button to add a new Certificate. +3. From the "Create a new Certificate" page, under "Software," select "Apple Development" to allow development for all Apple platforms. Under "Services," select any Services your app may require (it's fine to leave all services unchecked). Click "Continue." +4. Now we must provide Apple with a "Certificate Signing Request" from our local macOS device we want to use for development. [Follow the instructions here](https://developer.apple.com/help/account/create-certificates/create-a-certificate-signing-request) to create a Certificate Signing Request file. Upload the file you created and click "Continue." +5. Download the newly created development Certificate to your macOS device. +6. Double-click the downloaded Certificate file to install it to Keychain Access. + * It's convenient to install it to the `login` Keychain so you can use it whenever you log into the system. + +### Create an App Identifier +Every unique app you develop requires an "Identifier"--a unique string--which you set up in the Apple Developer portal. + +1. Log in to your Apple Developer account and visit https://developer.apple.com/account/resources/identifiers/list. +2. Click the + button to add a new Identifier. +3. Under "Register a new identifier," select "App IDs" (unless you're developing something other than an app, in which case you probably know which option to select). Click "Continue." +4. Select "App" as the Type and click "Continue." +5. If your app requires any of the Capabilities or App Services listed on the "Register an App ID" page, you must create a (non-user-facing) string that uniquely identifies your app. The suggested format is a reverse-domain name string, like `com.MyDomain.MyUniqueAppName`. For apps that don't require any Capabilities or App Services, it's nice to have a generic "wildcard" identifier during development; select "Wildcard" for the Bundle ID type and enter a reverse-domain name string like `com.MyDomain.*`. Any app you're working on that doesn't use any Capabilities or App Services can use this Wildcard identifier, saving you the time of having to create an identifier for every new app you make (a use case for this is locally-deployed custom tools). In any case, give the identifier a short description, like "Wildcard Development Certificate." Click "Continue," then "Register." + +### Register Development Devices +Apple allows up to 100 macOS and 100 iOS devices for development. At minimum, you need to whitelist the macOS device you're developing on and the iOS device you're deploying to. + +1. Log in to your Apple Developer account and visit https://developer.apple.com/account/resources/devices/list. +2. Click the + button to add a new Device. +3. Select the device's Platform--either iOS or macOS--and give the device an identifiable name. +4. Enter the device's UDID. To find your device's UDID: + * To find the UDID of a macOS device, click the Apple icon in the menu bar and select "About This Mac" >> "More Info..." >> "System Report" >> "Hardware" >> "Hardware Overview" (by clicking on "Hardware") >> "Hardware UUID" + * To find the UDID of an iOS device, connect the iOS device to a macOS device. Open "Finder" and select the iOS device under "Locations". Above the menu bar for "General," "Music," "Movies," etc., notice the header text with the name of the iOS device. Below the header text is smaller text describing the device model, storage capacity, and battery life. Click anywhere on that smaller text to reveal the device's Serial number and UDID. Right click on the smaller text and select "Copy UDID." +5. Select "Continue," then "Register." + +#### Enable Developer Mode on a Device +Devices running iOS 16 or later must be put into Developer Mode. For the most recent instructions, visit Apple's article on [Enabling Developer Mode on a device](https://developer.apple.com/documentation/xcode/enabling-developer-mode-on-a-device). + +To confirm your device is ready to deploy to, return to Xcode and open the "Devices and Simulators" window--either via the menu bar (Window >> Devices and Simulators) or with the keyboard shortcut Command+Shift+2. When your iOS device is connected to your macOS device via USB, it should appear under "Connected," and clicking on your device should open a pane with a table of Installed Apps (which is probably empty right now). + +### Create a Provisioning Profile +For an in-development app to be installed and run on a test device (macOS or iOS), the app needs a "Provisioning Profile." A Provisioning Profile associates your Development Certificate with your App Identifier and your Development Devices. It allows you to control which development devices can build which apps and deploy to which test devices. + +Existing Provisioning Profiles must be updated whenever you add a new development or test Device. + +1. Log in to your Apple Developer account and visit https://developer.apple.com/account/resources/profiles/list. +2. Click the + button to add a new Provisioning Profile. +3. Click "iOS App Development," then "Continue." + * Select "macOS App Development" here if you're building a macOS app. +4. Select the App Identifier you created above, then "Continue." +5. Select the Development Certificate you created above, then "Continue." + * If your team has multiple developers, be sure to include everyone's Development Certificate in the profile. You can always edit this later. +6. Select the iOS devices you registered above--these are the devices you're allowing developers to deploy to. Click "Continue." +7. Give the profile a name, like "MyApp's iOS Development Profile," then click "Generate." +8. Download the generated Provisioning Profile. + * If you created a macOS Profile, double-click the downloaded file to install it. + * If you created an iOS Profile, place it somewhere in your project directory for use during builds. The file may contain sensitive information, so it's best to not check it into version control. + +### Preparing for Code Signing +All apps running on Apple Silicon (for macOS and iOS) must be Code Signed. If you try to run an app that isn't code signed, you might get away with it for a bit, but eventually the OS will terminate your app. More often, a non-code signed app will crash before ever reaching `main()`. + +Apple's code signing process requires us to sign our code with an Apple-issued certificate. On your macOS Development Device, open Terminal and run this command to view the signing certificate(s) installed on your system: + +```sh +security find-identity -v -p codesigning +``` + +You should see at least one entry matching the name of the Development Certificate created earlier. If you don't see any entries, revisit the step above on Create an Apple Development Certificate. Be sure to download the created certificate at the end, then open the file and install it to your login keychain. + +Next, copy the 40-ish character alphanumeric identifier for your Development Certificate. You'll need this ID for Code Signing every time you do a build. This ID is private to each developer, so it shouldn't be shared or checked into version control. + +I find it most convenient to store the ID in an environment variable, which I export in my dotfile. If that makes no sense to you, or is otherwise distasteful, see the section below on "Code Signing with a Secret File." + +#### Code Signing with an Environment Variable +Copy the 40-ish character ID of your Development Certificate from the Terminal command above. We need to assign it to an environment variable we'll name `CODE_SIGN_IDENTITY`. The simplest method is to set the environment variable in your dotfile--a file of custom Terminal commands that runs whenever you open a new Terminal window. Run this command to create your dotfile (it's safe to run even if your dotfile already exists): + +```sh +touch ~/.zshrc +``` + +We need to edit the dotfile in a text editor. To edit in nano, run: + +```sh +nano ~/.zshrc +``` + +Inside the _.zshrc_ file, insert this command to set the environment variable (editing the line to use your code signing ID copied earlier): + +```sh +export CODE_SIGN_IDENTITY="ABC123" +``` + +Save and close the file. Restart the Terminal and run `echo $CODE_SIGN_IDENTITY` to validate your ID is set. + +#### Code Signing with a Secret File +Create a new text file for use during builds. We don't want this file shared with other team members, so it should remain local to the system and not be checked into version control. Open the text file and paste in the 40-ish character ID of your Development Certificate from the Terminal command above. Save and close the file, and take note of its location. We'll need it when building our app. + +## Building "Hello, World!" +Here is our app: + +```c +#include + +int main(int argc, char *argv[]) { + printf("Voulez-Vous!\n"); + return 0; +} +``` + +### Building on macOS for macOS + +#### Building Non-GUI macOS Applications +Non-GUI applications don't require any complications to build and run. Open Terminal and run the command: + +```sh +clang hello_world.c -o hello_world && ./hello_world +``` + +To build and debug your non-GUI application, run the command: + +```sh +# `--batch` tells lldb to quit when the application exits +# `-o run` tells lldb to start executing our app (`--file hello_world`) right away +clang hello_world.c -o hello_world && lldb --batch -o run --file hello_world +``` + +#### Building GUI macOS Applications +Let's imagine our "Hello, World!" app above was built as a macOS GUI app. GUI applications require a specific folder structure and must be Code Signed. + +##### macOS App Bundle Folder Structure +Your built application must conform to a specific folder structure, and must include a few extra metadata files. + +Visit this page for more details on app bundles: + +https://developer.apple.com/library/archive/documentation/CoreFoundation/Conceptual/CFBundles/BundleTypes/BundleTypes.html + +###### macOS Info.plist file +First, we need an "Info.plist" file. Create an empty text file and give it a `.plist` file extension. Paste in the contents below for a minimal Info.plist file: + +```xml + + + + + + CFBundleInfoDictionaryVersion + 6.0 + CFBundlePackageType + APPL + + + CFBundleName + HelloWorld + CFBundleDisplayName + Hello World + CFBundleExecutable + HelloWorld + CFBundleIdentifier + com.YourDomain.HelloWorld + + + + NSHighResolutionCapable + YES + + + MetalCaptureEnabled + + + +``` + +Update with Bundle, Display, Executable, and Bundle Identifier names to match your app. + +###### macOS Entitlements file +Second, we need a `.xcent` Entitlements file. Entitlements are key-value pairs that grant your app permission to use certain services or APIs. At minimum, you'll probably want the entitlement that allows debuggers to attach to your app. You'll know if you need anything else. + +Create an empty text file (any name will do) and give it a `.xcent` file extension. Paste in the contents below for a minimal Entitlements file that supports debugging: + +```xml + + + + + + com.apple.security.get-task-allow + + + +``` + +##### An Example macOS Build Script +Below is a build script example for a C application: + +```sh +#!/bin/bash + +# ----------------------------------------------------------------------------- +# @NOTE(BDJ): Customize this section to your app: +# ----------------------------------------------------------------------------- + +app_name="HelloWorld" +app_c_source_file="hello_world.c" +app_info_plist_file="macOS_Info.plist" +app_entitlements_file="macOS_DevelopmentEntitlements.xcent" + +# @NOTE(BDJ): If you saved your Code Signing ID to a secret text file, paste +# the path to that file here. Otherwise, the build script will use the +# `CODE_SIGN_IDENTITY` environment variable to look up your ID. +code_signing_identity_file="path/to/your/Apple_Code_Signing_Identity.txt" + +# @NOTE(BDJ): Choose your minimum OS version here. OSX 10.15.4 is a nice choice +# for games as it's the oldest version that both (a) still runs on Intel CPUs +# and (b) has access to Metal's frame pacing APIs. +target_version_flags="-mmacos-version-min=10.15.4" + +# ----------------------------------------------------------------------------- + +# @NOTE(BDJ): You may want to cache the results of these commands for speed +architecture=`uname -m` +compiler=`xcrun --sdk macosx --find clang` +sdk_path=`xcrun --sdk macosx --show-sdk-path` + +function build_c_app () +{ + # @NOTE(BDJ): Create the folders for our app bundle. + mkdir -p ${output_directory}/${app_name}.app/Contents/MacOS + mkdir -p ${output_directory}/${app_name}.app/Contents/Resources + + # @NOTE(BDJ): Copy our Info.plist file into the app bundle. + # + # It's very important when placing files into the output bundle structure + # that we use `mv` operations rather than `cp` operations. Using `cp` + # doesn't trigger a change in the bundled file's "inode" value. If the OS + # detects a bundled app with a changed code signature without a changed + # inode value it will kill the app. On our end, we see a crash "due to code + # signing error." Using a `mv` operation correctly updates the bundled + # file's inode value, so the OS won't complain when it sees changes. + # + # For more details on this, see Apple's documentation on "Updating Mac Software:" + # https://developer.apple.com/documentation/security/updating_mac_software?language=objc + cp -r ${app_info_plist_file} ${output_directory} && mv ${output_directory}/Info.plist ${output_directory}/${app_name}.app/Contents + + # @NOTE(BDJ): `-g` and `dsymutil` won't replace an existing .dSYM file for + # some reason, so we manually remove our existing dSYM file before + # building. + if [ -d "./${output_directory}/${app_name}.dSYM" ]; then + rm -r ${output_directory}/${app_name}.dSYM + fi + + minimal_compiler_flags="-target ${architecture}-apple-macos -isysroot ${sdk_path} ${target_version_flags} -fembed-bitcode -fPIC" + + ${compiler} ${minimal_compiler_flags} -g ${app_c_source_file} -o ${output_directory}/${app_name} && mv ${output_directory}/${app_name} ${output_directory}/${app_name}.app/Contents/MacOS +} + +function code_sign_app_bundle () +{ + # @NOTE(BDJ): If the `CODE_SIGN_IDENTITY` isn't set, try to look it up in + # a file set by the user. If that file doesn't exist, throw an error. + if [ -z "$CODE_SIGN_IDENTITY" ]; then + if [ ! -e "$code_signing_identity_file" ]; then + echo "Error: The code signing identity isn't configured. Please see the build script documentation for support." + exit 1 + fi + export CODE_SIGN_IDENTITY=$(cat $code_signing_identity_file) + fi + + # @NOTE(BDJ): Each library and executable within our app's bundle must be + # individually code-signed. We then code-sign the full app bundle as a + # final step. + codesign --generate-entitlement-der --force --timestamp\=none --sign $CODE_SIGN_IDENTITY --entitlements ${app_entitlements_file} ${output_directory}/${app_name}.app/Contents/MacOS/${app_name} && codesign --generate-entitlement-der --force --timestamp\=none --sign $CODE_SIGN_IDENTITY --entitlements ${app_entitlements_file} ${output_directory}/${app_name}.app +} + +build_c_app && code_sign_app_bundle +``` + +1. Copy the contents and save it to a file with a `.sh` extension, like `build-macos.sh` +2. Customize the first four variables at the top of the file to reflect your app's name, source file, Info.plist file location, and Entitlements file location. + * If your Code Signing ID was saved to a separate file, also provide the path to that file. +3. Mark the script as executable with this Terminal command: `chmod u+x build-macos.sh` +4. Run the script in Terminal with the command `./build-macos.sh` + +You should now be able to open and run the `.app` application built to the output folder. + +To build and debug your application, use a Terminal command like this: + +```sh +./build-macos.sh && lldb --batch -o run --file output/macos-debug-arm64/HelloWorld.app +``` + +### Building on macOS for iOS +Like macOS, your built application must conform to a specific folder structure, and must include a few extra metadata files. + +Visit this page for more details on app bundles: + +https://developer.apple.com/library/archive/documentation/CoreFoundation/Conceptual/CFBundles/BundleTypes/BundleTypes.html + +iOS's app bundle structure is different than macOS, so it requires a slightly different build process. + +###### iOS Info.plist file +First, we need an "Info.plist" file. Create an empty text file and give it a `.plist` file extension. Paste in the contents below for a minimal Info.plist file: + +```xml + + + + + + CFBundleInfoDictionaryVersion + 6.0 + CFBundlePackageType + APPL + + LSRequiresIPhoneOS + + + + CFBundleName + HelloWorld + CFBundleDisplayName + Hello World + CFBundleExecutable + HelloWorld + CFBundleIdentifier + com.YourDomain.HelloWorld + + UIRequiredDeviceCapabilities + + metal + arm64 + + + + UIStatusBarHidden + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + + +``` + +Update with Bundle, Display, Executable, and Bundle Identifier names to match your app. You may also want to edit the required Device Capabilities and Supported Orientations. + +###### iOS .mobileprovision file +Testing on device requires a provisioning profile placed inside the app's bundle. We use a development provisioning profile for day-to-day development. If you haven't already, create and download an iOS development provisioning profile from here (make sure the provisioning profile is configured for the devices you plan on deploying to): + +https://developer.apple.com/account/resources/profiles/list + +You'll need the downloaded .mobileprovision for your builds, but don't check the file into version control. + +###### iOS Entitlements file +The iOS code signing process requires us to provide an "entitlements" file which lists specialized app permissions. To create this file, we first need a development provisioning profile. Make sure to go through the steps above to configure the provisioning profile. + +Once we have a provisioning profile, run the following command in Terminal to view it (replace the path to the provisioning profile with your own): + +```sh +security cms -D -i /path/to/your/iOS_Development.mobileprovision +``` + +You should see a section within the XML for "Entitlements": + +```xml +Entitlements + + + +``` + +Create a .xcent text file for your app's Entitlements (e.g., "iOS_Development.xcent"). Like with the .mobileprovision file, this won't be checked into version control. + +Insert the following header into the file: + +```xml + + + + + +``` + +Copy the Entitlements' ` ... ` section from your provisioning profile and paste it inside the `` node of your Entitlements file. It should look something like this: + +```xml + + + + + application-identifier + ABCD1234.* + com.apple.developer.team-identifier + ABCD1234 + get-task-allow + + + +``` + +Repeat this process whenever you reconfigure your app's entitlements (which you would do either via Xcode or developer.apple.com) or change provisioning profiles. + +##### An Example iOS Build Script +Below is a build script example for a C application: + +```sh +#!/bin/bash + +# ----------------------------------------------------------------------------- +# @NOTE(BDJ): Customize this section to your app: +# ----------------------------------------------------------------------------- + +app_name="HelloWorld" +app_c_source_file="hello_world.c" +app_info_plist_file="iOS_Info.plist" +app_entitlements_file="iOS_DevelopmentEntitlements.xcent" +app_provisioning_profile="iOS_Wildcard_Development.mobileprovision" + +# @NOTE(BDJ): If you saved your Code Signing ID to a secret text file, paste +# the path to that file here. Otherwise, the build script will use the +# `CODE_SIGN_IDENTITY` environment variable to look up your ID. +code_signing_identity_file="path/to/your/Apple_Code_Signing_Identity.txt" + +# @NOTE(BDJ): Choose your minimum OS version here. +target_version_flags="-mios-version-min=13.0" + +# ----------------------------------------------------------------------------- + +architecture="arm64" + +# @NOTE(BDJ): You may want to cache the results of these commands for speed +compiler=`xcrun --sdk iphoneos --find clang` +sdk_path=`xcrun --sdk iphoneos --show-sdk-path` + +# @NOTE(BDJ): Choose your output path here: +output_directory="output/ios-debug-${architecture}" + +function build_c_app () +{ + # @NOTE(BDJ): Create the folders for our app bundle. + mkdir -p ${output_directory}/${app_name}.app + + # @NOTE(BDJ): Copy our Info.plist file into the app bundle. + cp -r ${app_info_plist_file} ${output_directory}/Info.plist && mv ${output_directory}/Info.plist ${output_directory}/${app_name}.app + + # @NOTE(BDJ): `-g` and `dsymutil` won't replace an existing .dSYM file for + # some reason, so we manually remove our existing dSYM file before + # building. + if [ -d "./${output_directory}/${app_name}.dSYM" ]; then + rm -r ${output_directory}/${app_name}.dSYM + fi + + minimal_compiler_flags="-target ${architecture}-apple-ios -isysroot ${sdk_path} ${target_version_flags} -fembed-bitcode -fPIC" + + ${compiler} ${minimal_compiler_flags} -g ${app_c_source_file} -o ${output_directory}/${app_name} && mv ${output_directory}/${app_name} ${output_directory}/${app_name}.app +} + +function code_sign_app_bundle () +{ + # @NOTE(BDJ): If the `CODE_SIGN_IDENTITY` isn't set, try to look it up in + # a file set by the user. If that file doesn't exist, throw an error. + if [ -z "$CODE_SIGN_IDENTITY" ]; then + if [ ! -e "$code_signing_identity_file" ]; then + echo "Error: The code signing identity isn't configured. Please see the build script documentation for support." + exit 1 + fi + export CODE_SIGN_IDENTITY=$(cat $code_signing_identity_file) + fi + + # @NOTE(BDJ): Copy the app's provisioning profile to the app bundle before + # code signing. + provisioning_profile_base_name=$(basename ${app_provisioning_profile}) + cp -r ${app_provisioning_profile} ${output_directory} && mv -f ${output_directory}/${provisioning_profile_base_name} ${output_directory}/${app_name}.app/embedded.mobileprovision + + # @NOTE(BDJ): Each library and executable within our app's bundle must be + # individually code-signed. We then code-sign the full app bundle as a + # final step. + codesign --generate-entitlement-der --force --timestamp\=none --sign $CODE_SIGN_IDENTITY --entitlements ${app_entitlements_file} ${output_directory}/${app_name}.app/${app_name} && codesign --generate-entitlement-der --force --timestamp\=none --sign $CODE_SIGN_IDENTITY --entitlements ${app_entitlements_file} ${output_directory}/${app_name}.app +} + +build_c_app && code_sign_app_bundle +``` + +1. Copy the contents and save it to a file with a `.sh` extension, like `build-ios.sh` +2. Customize the first five variables at the top of the file to reflect your app's name, source file, Info.plist file location, Entitlements file location, and Provisioning Profile location. + * If your Code Signing ID was saved to a separate file, also provide the path to that file. +3. Mark the script as executable with this Terminal command: `chmod u+x build-ios.sh` +4. Run the script in Terminal with the command `./build-ios.sh` + +You should now be able to deploy the `.app` application built to the output folder to an iOS device or simulator. + +##### Deploying to and Debugging on iOS Devices with iOS-Deploy +It's possible to deploy and debug iOS apps on device--without opening Xcode--using a third-party tool called "ios-deploy." + +Follow the steps on the [ios-deploy official GitHub repository](https://github.com/ios-control/ios-deploy) to build and/or install it on your system. + +To deploy your iOS app, run the Terminal command: + +```sh +ios-deploy --bundle ./output/ios-debug-arm64/HelloWorld.app +``` + +To deploy and debug your iOS app, add the `--debug` flag: + +```sh +ios-deploy --debug --bundle ./output/ios-debug-arm64/HelloWorld.app +``` + +##### Deploying to iOS Simulators +If you haven't already created an iOS Simulator instance, open the Simulator application and create a new simulator for your target device and OS version. + +Once you have a simulator created, you can deploy and launch your application with this Terminal command (be sure to update your app path and bundle identifier): + +```sh +open -a Simulator.app && xcrun simctl install booted ./output/ios-debug-arm64/HelloWorld.app && xcrun simctl launch booted com.YourDomain.HelloWorld +``` + +## Building Metal Shaders +If you're writing Metal shaders, they can also be built without opening Xcode. Add a function like this to your macOS or iOS build script to build your app's default Metal library: + +```sh +# @NOTE(BDJ): If you're building a Metal application, this is how you would +# compile your shaders and include them in your build as the default Metal +# library. +function build_shaders () +{ + shader_source_file="YOUR_SHADER_SOURCE_FILE.metal" + + # @NOTE(BDJ): Use this path for iOS: + # shader_library_output_path="${output_directory}/${app_name}.app" + # @NOTE(BDJ): Use this path for macOS: + shader_library_output_path="${output_directory}/${app_name}.app/Contents/Resources" + + # @NOTE(BDJ): Choose your Metal shader version here. 1.2 is nice because it + # still supports Intel Macs. + shader_version_flags="-std=macos-metal1.2" + + # @NOTE(BDJ): You may want to cache the results of these commands for speed: + shader_compiler=`xcrun --sdk macosx --find metal` + shader_linker=`xcrun --sdk macosx --find metallib` + + # @NOTE(BDJ): Compile the shader source, build it into a .metallib file, + # then place it in our app bundle. + ${shader_compiler} -arch air64 -emit-llvm -fpreserve-invariance -fno-implicit-module-maps -g -MO ${shader_version_flags} ${target_version_flags} -c ${shader_source_file} -o ${output_directory}/metal_shaders.air && ${shader_linker} ${output_directory}/metal_shaders.air -o ${output_directory}/default.metallib && mv ${output_directory}/default.metallib ${shader_library_output_path} +} +``` + +## Building for Public Distribution +This requires some changes to the Code Signing step of the build process. Visit Apple's page on [Preparing your app for distribution](https://developer.apple.com/documentation/xcode/preparing-your-app-for-distribution) for the most recent information. + +## When to Use Xcode +I still find myself needing to open and use Xcode for any serious CPU or GPU debugging task. Maybe one day macOS will have better debugging options! + +## Troubleshooting + +### App crashes immediately at launch from a signal +If your application is crashing before it even reaches `main()`, it's likely due to an OS security issue like code signing. You might see errors like: + +* "HelloWorld.app" will damage your computer. You should move it to the Trash. +* Error: "The identity used to sign the executable is no longer valid." + +Here are some resources related to debugging code signing issues: + +* [WWDC 2019 talk All About Notarization](https://developer.apple.com/videos/play/wwdc2019/703/) +* [Updating Mac Software](https://developer.apple.com/documentation/security/updating_mac_software?language=objc) + +If you're running macOS 11+, try whitelisting Terminal for development. + +* Navigate to System Preferences >> Privacy & Security >> Developer Tools +* Click the `+` button to add and enable Terminal for development (i.e., to "...run software locally that does not meet the system's security policy"). + +### xcrun error: SDK "ios" cannot be located +I noticed this error after first installing Xcode. It may also happen after updating Xcode, I suspect. To fix it: + +* Open Xcode +* Navigate to "Preferences" / "Settings" +* Find the "Locations" tab +* Next to "Command Line Tools:", click on the drop-down menu and select the latest version of Xcode installed. You may need to enter your macOS login password. +* Open Terminal and run `xcrun --sdk iphoneos --show-sdk-path` to verify Xcode is installed correctly. + +Alternatively, try opening Teriminal and running: + +```sh +sudo xcode-select --switch /Applications/Xcode.app +``` diff --git a/macos_development/package_macos.jai b/macos_development/package_macos.jai new file mode 100644 index 0000000..ec809e8 --- /dev/null +++ b/macos_development/package_macos.jai @@ -0,0 +1,265 @@ +/* + This is heavily based (read: copy pasted + some modifications) on Thekla's module + MacOS_Bundler.jai, released with the jai compiler. + + Usage: +*/ + +#import "Basic"; +#import "String"; +#import "Process"; +#import "File"; +#import "Compiler"; +#import "POSIX"; +FU :: #import "File_Utilities"; + +Args :: struct +{ + exe_path: string; + output_path: string; + output_name: string; + asset_directory: string; + dynamic_libs: []string; + icon_file_path: string; + debug: bool; +}; + +package_for_macos :: (args: Args) -> error: bool +{ + success := true; + + exe_name := path_filename(args.exe_path); + exe_path := path_strip_filename(args.exe_path); + + app_bundle_name := tprint("%/%.app", args.output_path, args.output_name); + resources_path := tprint("%/Contents/Resources", app_bundle_name); + macos_path := tprint("%/Contents/MacOS", app_bundle_name); + + { // Make Bundle Directories + success &= make_directory_if_it_does_not_exist(app_bundle_name, true); + success &= make_directory_if_it_does_not_exist(tprint("%/Contents", app_bundle_name), true); + success &= make_directory_if_it_does_not_exist(macos_path, true); + success &= make_directory_if_it_does_not_exist(resources_path, true); + if !success print("Unable to make directory structure\n"); + } + + // Copy Assets + if success + { + if args.asset_directory + { + success = copy_directory(args.asset_directory, resources_path); + } + if !success print("Unable to copy asset_directory\n"); + } + + out_exe_path := sprint("%/%", macos_path, args.output_name); + if success + { + success = copy_file(args.exe_path, out_exe_path); + chmod(temp_c_string(out_exe_path), 0x1FF); // add execute permissions + if !success print("Unable to copy executable to %\n", out_exe_path); + } + + // Copy Dynamic Libs + if success + { + for args.dynamic_libs + { + basename := path_filename(it); + dst := sprint("%/%", macos_path, basename); + success = copy_file(it, dst); + } + } + + icon_data_file_name: string; + if success + { + if args.icon_file_path + { + icon_data_file_name = "icon_data.icns"; + icns_output_path := tprint("%/%", resources_path, icon_data_file_name); + success = convert_image_to_icns(args.icon_file_path, icns_output_path); + } + } + + // Write plist + if success + { + builder: String_Builder; + + plist_start(*builder, PLIST_HEADER); + if icon_data_file_name { + success := plist_write_key_value(*builder, "CFBundleIconFile", icon_data_file_name); + assert(success); + } + plist_end(*builder); + + output := builder_to_string(*builder); + success = write_entire_file(tprint("%/Contents/Info.plist", app_bundle_name), output); + + free(output); + } + + // Write Entitlements + if success + { + builder: String_Builder; + + plist_start(*builder, ENTITLEMENTS_HEADER); + + if args.debug + { + plist_write_key_value(*builder, "com.apple.security.get-task-allow", true); + } + + plist_end(*builder); + + output := builder_to_string(*builder); + success = write_entire_file(tprint("%/Entitlements.xcent", args.output_path), output); + + free(output); + } + + return !success; +} + +convert_image_to_icns :: (input_path: string, output_path: string) -> bool +{ + // @TODO verify if the input image is 128x128 256x256 or 512x512 and scale the image accordingly if not + // or at least give an error + process_result, output, error := run_command("sips", "-s", "format", "icns", input_path, "--out", output_path); + return process_result.exit_code == 0; +} + +/////////////////////////////////////////////////// +// Plist Writing + +plist_start :: (builder: *String_Builder, header: string) +{ + append(builder, header); + append(builder, "\n"); + append(builder, "\n"); +} + +plist_end :: (builder: *String_Builder) +{ + append(builder, "\n"); + append(builder, "\n"); +} + +plist_write_key_value :: (builder: *String_Builder, key: string, value: Any) -> success: bool +{ + plist_write_key(builder, key); + + if (value.type.type == Type_Info_Tag.BOOL) plist_write_bool(builder, << cast(*bool) value.value_pointer); + else if (value.type.type == Type_Info_Tag.STRING) plist_write_string(builder, << cast(*string) value.value_pointer); + else return false; + + return true; +} + +plist_write_key :: (builder: *String_Builder, key: string) +{ + append(builder, ""); + append(builder, key); + append(builder, ""); +} + +plist_write_string :: (builder: *String_Builder, str: string) +{ + append(builder, "\n"); + append(builder, str); + append(builder, "\n"); +} + +plist_write_bool :: (builder: *String_Builder, value: bool) +{ + print_to_builder(builder, "<%/>", value); +} + +#scope_file + +PLIST_HEADER :: #string DONE + + +DONE + +ENTITLEMENTS_HEADER :: #string DONE + + +DONE + +/////////////////////////////////////////////////////////////// +// Utilities + +copy_directory :: (from: string, to: string) -> success: bool +{ + User_Data :: struct { + from: string; + to: string; + success: *bool; + } + + success: bool = true; + ud := User_Data.{ + from = from, + to = to, + success = *success, + }; + + visitor :: (info: *FU.File_Visit_Info, data: User_Data) + { + dest_name := advance(info.full_name, data.from.count); + dest_name = concat(data.to, dest_name); + if info.is_directory { + << data.success &= make_directory_if_it_does_not_exist(dest_name); + } else { + << data.success &= copy_file(info.full_name, dest_name); + } + } + + FU.visit_files(from, true, ud, visitor, visit_directories = true); + return success; +} + +copy_file :: (from: string, to: string) -> success: bool +{ + success := false; + + data: string; + data, success = read_entire_file(from); + if success success = write_entire_file(to, data); + return success; +} + +concat :: (strings: .. string, separator: string = "") -> string +{ + len := 0; + for strings len += it.count; + len += separator.count * (strings.count - 1); + + result := make_string(len); + at := 0; + for strings + { + memcpy(result.data + at, it.data, it.count); + if it_index < strings.count - 1 + { + memcpy(result.data + at + it.count, separator.data, separator.count); + } + at += it.count + separator.count; + } + + return result; +} + +make_string :: (count: int) -> string +{ + assert(count >= 0); + if !count return ""; + s: string = ---; + s.data = alloc(count); + s.count = count; + return s; +} diff --git a/macos_development/package_macos_example b/macos_development/package_macos_example new file mode 100755 index 0000000..95c4f54 Binary files /dev/null and b/macos_development/package_macos_example differ diff --git a/macos_development/package_macos_example.jai b/macos_development/package_macos_example.jai new file mode 100644 index 0000000..c5daea6 --- /dev/null +++ b/macos_development/package_macos_example.jai @@ -0,0 +1,10 @@ + +#load "package_macos.jai"; + +main :: () { + package_for_macos(.{ + exe_path = "/Users/ps/work/minos/p_atof/run_tree/atof_MACOS_X64_Debug", + output_path = "/Users/ps/work/jai-resources/macos_development/", + output_name = "test", + }); +} \ No newline at end of file