├── stub ├── src │ ├── console_stub │ │ └── __init__.py │ └── gui_stub │ │ └── __init__.py └── pyproject.toml ├── .gitignore ├── {{ cookiecutter.format }} ├── Support │ └── README ├── {{ cookiecutter.class_name }} │ ├── app_packages │ │ └── README │ ├── Assets.xcassets │ │ ├── Contents.json │ │ ├── {{ cookiecutter.formal_name }}.appiconset │ │ │ ├── icon-1024.png │ │ │ ├── icon-128.png │ │ │ ├── icon-16.png │ │ │ ├── icon-256.png │ │ │ ├── icon-32.png │ │ │ ├── icon-512.png │ │ │ ├── icon-64.png │ │ │ └── Contents.json │ │ └── AccentColor.colorset │ │ │ └── Contents.json │ ├── app │ │ └── README │ ├── {{ cookiecutter.app_name }}.entitlements │ ├── Info.plist │ └── main.m ├── {{ cookiecutter.formal_name }}.xcodeproj │ ├── project.xcworkspace │ │ └── contents.xcworkspacedata │ └── project.pbxproj ├── installer │ ├── resources │ │ └── welcome.html │ ├── scripts │ │ └── postinstall │ └── Distribution.xml └── briefcase.toml ├── .github ├── dependabot.yml └── workflows │ ├── pre-commit-update.yml │ ├── ci.yml │ └── update-binary.yml ├── .pre-commit-config.yaml ├── CONTRIBUTING.md ├── cookiecutter.json ├── LICENSE ├── README.md └── CODE_OF_CONDUCT.md /stub/src/console_stub/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /stub/src/gui_stub/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .python-version 3 | .vscode 4 | .idea 5 | stub/build 6 | stub/logs 7 | -------------------------------------------------------------------------------- /{{ cookiecutter.format }}/Support/README: -------------------------------------------------------------------------------- 1 | This directory will contain the support libraries for the project. 2 | -------------------------------------------------------------------------------- /{{ cookiecutter.format }}/{{ cookiecutter.class_name }}/app_packages/README: -------------------------------------------------------------------------------- 1 | This directory exists so that 3rd party packages can be installed here. 2 | -------------------------------------------------------------------------------- /{{ cookiecutter.format }}/{{ cookiecutter.class_name }}/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /{{ cookiecutter.format }}/{{ cookiecutter.formal_name }}.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /{{ cookiecutter.format }}/{{ cookiecutter.class_name }}/app/README: -------------------------------------------------------------------------------- 1 | Your application code should be placed in this directory. 2 | 3 | The native code will be looking for a helloworld/__main__.py file as the entry point. 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | 2 | version: 2 3 | updates: 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | # Check for updates on the first Sunday of every month, 8PM UTC 8 | interval: "cron" 9 | cronjob: "0 20 * * sun#1" 10 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v6.0.0 4 | hooks: 5 | - id: check-yaml 6 | - id: check-json 7 | - id: check-xml 8 | - id: check-case-conflict 9 | - id: end-of-file-fixer 10 | - id: trailing-whitespace 11 | -------------------------------------------------------------------------------- /{{ cookiecutter.format }}/{{ cookiecutter.class_name }}/Assets.xcassets/{{ cookiecutter.formal_name }}.appiconset/icon-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beeware/briefcase-macOS-Xcode-template/HEAD/{{ cookiecutter.format }}/{{ cookiecutter.class_name }}/Assets.xcassets/{{ cookiecutter.formal_name }}.appiconset/icon-1024.png -------------------------------------------------------------------------------- /{{ cookiecutter.format }}/{{ cookiecutter.class_name }}/Assets.xcassets/{{ cookiecutter.formal_name }}.appiconset/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beeware/briefcase-macOS-Xcode-template/HEAD/{{ cookiecutter.format }}/{{ cookiecutter.class_name }}/Assets.xcassets/{{ cookiecutter.formal_name }}.appiconset/icon-128.png -------------------------------------------------------------------------------- /{{ cookiecutter.format }}/{{ cookiecutter.class_name }}/Assets.xcassets/{{ cookiecutter.formal_name }}.appiconset/icon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beeware/briefcase-macOS-Xcode-template/HEAD/{{ cookiecutter.format }}/{{ cookiecutter.class_name }}/Assets.xcassets/{{ cookiecutter.formal_name }}.appiconset/icon-16.png -------------------------------------------------------------------------------- /{{ cookiecutter.format }}/{{ cookiecutter.class_name }}/Assets.xcassets/{{ cookiecutter.formal_name }}.appiconset/icon-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beeware/briefcase-macOS-Xcode-template/HEAD/{{ cookiecutter.format }}/{{ cookiecutter.class_name }}/Assets.xcassets/{{ cookiecutter.formal_name }}.appiconset/icon-256.png -------------------------------------------------------------------------------- /{{ cookiecutter.format }}/{{ cookiecutter.class_name }}/Assets.xcassets/{{ cookiecutter.formal_name }}.appiconset/icon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beeware/briefcase-macOS-Xcode-template/HEAD/{{ cookiecutter.format }}/{{ cookiecutter.class_name }}/Assets.xcassets/{{ cookiecutter.formal_name }}.appiconset/icon-32.png -------------------------------------------------------------------------------- /{{ cookiecutter.format }}/{{ cookiecutter.class_name }}/Assets.xcassets/{{ cookiecutter.formal_name }}.appiconset/icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beeware/briefcase-macOS-Xcode-template/HEAD/{{ cookiecutter.format }}/{{ cookiecutter.class_name }}/Assets.xcassets/{{ cookiecutter.formal_name }}.appiconset/icon-512.png -------------------------------------------------------------------------------- /{{ cookiecutter.format }}/{{ cookiecutter.class_name }}/Assets.xcassets/{{ cookiecutter.formal_name }}.appiconset/icon-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beeware/briefcase-macOS-Xcode-template/HEAD/{{ cookiecutter.format }}/{{ cookiecutter.class_name }}/Assets.xcassets/{{ cookiecutter.formal_name }}.appiconset/icon-64.png -------------------------------------------------------------------------------- /{{ cookiecutter.format }}/{{ cookiecutter.class_name }}/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | }, 6 | { 7 | "appearances" : [ 8 | { 9 | "appearance" : "luminosity", 10 | "value" : "dark" 11 | } 12 | ], 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | BeeWare <3's contributions! 4 | 5 | Please be aware that BeeWare operates under a [Code of 6 | Conduct](https://beeware.org/community/behavior/code-of-conduct/). 7 | 8 | If you'd like to contribute to Briefcase development, our [contribution 9 | guide](https://briefcase.readthedocs.io/en/latest/how-to/contribute/index.html) details how 10 | to set up a development environment, and other requirements we have as part of our 11 | contribution process. 12 | -------------------------------------------------------------------------------- /{{ cookiecutter.format }}/installer/resources/welcome.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | 12 |

{{ cookiecutter.formal_name }} {{ cookiecutter.version }}

13 |

This installer will guide you through the steps necessary to install {{ cookiecutter.formal_name }}.

14 | 15 | 16 | -------------------------------------------------------------------------------- /{{ cookiecutter.format }}/installer/scripts/postinstall: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | echo "Post installation process started" 3 | 4 | if [ ! -d "/usr/local/bin" ]; then 5 | echo "Creating /usr/local/bin directory" 6 | mkdir -p /usr/local/bin 7 | fi 8 | 9 | echo "Install binary symlink" 10 | ln -si "/Library/{{ cookiecutter.formal_name }}/{{ cookiecutter.formal_name }}.app/Contents/MacOS/{{ cookiecutter.formal_name }}" /usr/local/bin/{{ cookiecutter.app_name }} 11 | 12 | echo "Post installation process finished" 13 | -------------------------------------------------------------------------------- /{{ cookiecutter.format }}/{{ cookiecutter.class_name }}/{{ cookiecutter.app_name }}.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {%- if cookiecutter.entitlements -%} 6 | {%- for entitlement, value in cookiecutter.entitlements.items() %} 7 | {{ entitlement }} 8 | {{ value|plist_value }} 9 | {%- endfor -%} 10 | {%- endif %} 11 | 12 | 13 | -------------------------------------------------------------------------------- /stub/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.briefcase] 2 | project_name = "Stub" 3 | bundle = "org.beeware" 4 | version = "1.0.0" 5 | license.file = "../LICENSE" 6 | 7 | # Use the current state of this repo as the template. 8 | template = ".." 9 | 10 | [tool.briefcase.app.gui-stub] 11 | formal_name = "GUI Stub" 12 | description = "A stub binary for GUI apps that can be integrated into the macOS app template" 13 | sources = ['src/gui_stub'] 14 | 15 | [tool.briefcase.app.console-stub] 16 | formal_name = "Console Stub" 17 | description = "A stub binary for console apps that can be integrated into the macOS app template" 18 | sources = ['src/console_stub'] 19 | console_app = true 20 | -------------------------------------------------------------------------------- /.github/workflows/pre-commit-update.yml: -------------------------------------------------------------------------------- 1 | name: Update pre-commit 2 | 3 | on: 4 | schedule: 5 | - cron: "0 20 1-7 * */7" # First Sunday of the month @ 2000 UTC 6 | # Reading this expression: At 20:00 on every day-of-month from 1 through 7 if 7 | # it's on every 7th day-of-week, i.e. any one of the first seven days of the 8 | # month as long as it is a Sunday. 9 | workflow_dispatch: 10 | 11 | jobs: 12 | pre-commit-update: 13 | name: Update pre-commit 14 | uses: beeware/.github/.github/workflows/pre-commit-update.yml@main 15 | secrets: inherit 16 | with: 17 | pre-commit-source: pre-commit 18 | create-changenote: false 19 | -------------------------------------------------------------------------------- /cookiecutter.json: -------------------------------------------------------------------------------- 1 | { 2 | "format": "Xcode", 3 | "formal_name": "App Name", 4 | "app_name": "{{ cookiecutter.formal_name|lower|replace(' ', '-') }}", 5 | "class_name": "{{ cookiecutter.formal_name.title().replace(' ','').replace('-','').replace('!','').replace('.','').replace(',','') }}", 6 | "module_name": "{{ cookiecutter.app_name|replace('-', '_') }}", 7 | "author": "Example Corporation", 8 | "bundle": "com.example", 9 | "info": "", 10 | "entitlements": "", 11 | "document_types": "", 12 | "version": "1.0", 13 | "build": "1", 14 | "python_version": "3.X.0", 15 | "console_app": false, 16 | "universal_build": true, 17 | "host_arch": "arm64", 18 | "min_os_version": "11.0", 19 | "_extensions": [ 20 | "briefcase.integrations.cookiecutter.PythonVersionExtension", 21 | "briefcase.integrations.cookiecutter.PListExtension" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /{{ cookiecutter.format }}/installer/Distribution.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ cookiecutter.formal_name }} 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | {{ cookiecutter.app_name }}.pkg 15 | 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Russell Keith-Magee. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - main 7 | workflow_call: 8 | 9 | # Cancel active CI runs for a PR before starting another run 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.ref }} 12 | cancel-in-progress: true 13 | 14 | defaults: 15 | run: 16 | shell: bash 17 | 18 | env: 19 | FORCE_COLOR: "1" 20 | 21 | jobs: 22 | pre-commit: 23 | name: Pre-commit checks 24 | uses: beeware/.github/.github/workflows/pre-commit-run.yml@main 25 | with: 26 | pre-commit-source: pre-commit 27 | 28 | verify-apps: 29 | name: Build apps 30 | needs: pre-commit 31 | uses: beeware/.github/.github/workflows/app-build-verify.yml@main 32 | with: 33 | python-version: ${{ matrix.python-version }} 34 | runner-os: macos-26 35 | framework: ${{ matrix.framework }} 36 | target-platform: macOS 37 | target-format: Xcode 38 | strategy: 39 | fail-fast: false 40 | matrix: 41 | python-version: [ "3.10", "3.11", "3.12", "3.13", "3.14" ] 42 | framework: [ "toga", "pyside6", "pygame", "console" ] 43 | # Pygame and PySide6 haven't published 3.14 wheels yet. 44 | exclude: 45 | - python-version: "3.14" 46 | framework: pyside6 47 | - python-version: "3.14" 48 | framework: pygame 49 | -------------------------------------------------------------------------------- /{{ cookiecutter.format }}/{{ cookiecutter.class_name }}/Assets.xcassets/{{ cookiecutter.formal_name }}.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "icon-16.png", 5 | "idiom" : "mac", 6 | "scale" : "1x", 7 | "size" : "16x16" 8 | }, 9 | { 10 | "filename" : "icon-32.png", 11 | "idiom" : "mac", 12 | "scale" : "2x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "icon-32.png", 17 | "idiom" : "mac", 18 | "scale" : "1x", 19 | "size" : "32x32" 20 | }, 21 | { 22 | "filename" : "icon-64.png", 23 | "idiom" : "mac", 24 | "scale" : "2x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "icon-128.png", 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "filename" : "icon-256.png", 35 | "idiom" : "mac", 36 | "scale" : "2x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "icon-256.png", 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "256x256" 44 | }, 45 | { 46 | "filename" : "icon-512.png", 47 | "idiom" : "mac", 48 | "scale" : "2x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "filename" : "icon-512.png", 53 | "idiom" : "mac", 54 | "scale" : "1x", 55 | "size" : "512x512" 56 | }, 57 | { 58 | "filename" : "icon-1024.png", 59 | "idiom" : "mac", 60 | "scale" : "2x", 61 | "size" : "512x512" 62 | } 63 | ], 64 | "info" : { 65 | "author" : "xcode", 66 | "version" : 1 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /{{ cookiecutter.format }}/briefcase.toml: -------------------------------------------------------------------------------- 1 | # Generated using Python {{ cookiecutter.python_version }} 2 | [briefcase] 3 | # This is the start of the framework-based support package era. 4 | target_version = "0.3.20" 5 | 6 | [paths] 7 | app_path = "{{ cookiecutter.class_name }}/app" 8 | app_packages_path = "{{ cookiecutter.class_name }}/app_packages" 9 | info_plist_path = "{{ cookiecutter.class_name }}/Info.plist" 10 | entitlements_path = "{{ cookiecutter.class_name }}/{{ cookiecutter.app_name }}.entitlements" 11 | 12 | support_path = "Support" 13 | {{ { 14 | "3.10": "support_revision = 12", 15 | "3.11": "support_revision = 7", 16 | "3.12": "support_revision = 7", 17 | "3.13": "support_revision = 12", 18 | "3.14": "support_revision = 8", 19 | }.get(cookiecutter.python_version|py_tag, "") }} 20 | cleanup_paths = [ 21 | "Support/Python.xcframework/**/python*/config-*-darwin", 22 | "Support/Python.xcframework/**/pkgconfig", 23 | ] 24 | icon.16 = "{{ cookiecutter.class_name }}/Assets.xcassets/{{ cookiecutter.formal_name }}.appiconset/icon-16.png" 25 | icon.32 = "{{ cookiecutter.class_name }}/Assets.xcassets/{{ cookiecutter.formal_name }}.appiconset/icon-32.png" 26 | icon.64 = "{{ cookiecutter.class_name }}/Assets.xcassets/{{ cookiecutter.formal_name }}.appiconset/icon-64.png" 27 | icon.128 = "{{ cookiecutter.class_name }}/Assets.xcassets/{{ cookiecutter.formal_name }}.appiconset/icon-128.png" 28 | icon.256 = "{{ cookiecutter.class_name }}/Assets.xcassets/{{ cookiecutter.formal_name }}.appiconset/icon-256.png" 29 | icon.512 = "{{ cookiecutter.class_name }}/Assets.xcassets/{{ cookiecutter.formal_name }}.appiconset/icon-512.png" 30 | icon.1024 = "{{ cookiecutter.class_name }}/Assets.xcassets/{{ cookiecutter.formal_name }}.appiconset/icon-1024.png" 31 | -------------------------------------------------------------------------------- /.github/workflows/update-binary.yml: -------------------------------------------------------------------------------- 1 | name: Update binary 2 | on: 3 | push: 4 | tags: 5 | - 'b*' 6 | 7 | jobs: 8 | build-stubs: 9 | name: Build stub binaries 10 | runs-on: macos-26 11 | strategy: 12 | matrix: 13 | python-version: [ "3.10", "3.11", "3.12", "3.13", "3.14" ] 14 | outputs: 15 | BUILD_NUMBER: ${{ steps.build-vars.outputs.BUILD_NUMBER }} 16 | 17 | steps: 18 | - name: Set Build Variables 19 | id: build-vars 20 | env: 21 | TAG_NAME: ${{ github.ref }} 22 | run: | 23 | export BUILD_NUMBER=$(basename $TAG_NAME) 24 | export PYTHON_TAG=$(python -c "print('${{ matrix.python-version }}'.split('-')[0])") 25 | 26 | echo "PYTHON_TAG=${PYTHON_TAG}" | tee -a $GITHUB_ENV 27 | echo "BUILD_NUMBER=${BUILD_NUMBER}" | tee -a $GITHUB_ENV 28 | 29 | - name: Checkout Template 30 | uses: actions/checkout@v6.0.1 31 | 32 | - name: Setup Python ${{ matrix.python-version }} 33 | uses: actions/setup-python@v6.1.0 34 | with: 35 | python-version: ${{ matrix.python-version }} 36 | allow-prereleases: true 37 | 38 | - name: Install Dependencies 39 | run: | 40 | python -m pip install --upgrade pip 41 | python -m pip install git+https://github.com/beeware/briefcase.git 42 | 43 | - name: Generate Xcode App Template 44 | run: | 45 | # Generate the stub app 46 | cd stub 47 | briefcase build macOS Xcode 48 | 49 | echo "Build ${{ env.PYTHON_TAG }}-${{ env.BUILD_NUMBER }} console stub artefact" 50 | mv "./build/console-stub/macos/xcode/build/Release/Console Stub.app/Contents/MacOS/Console Stub" Stub 51 | codesign --remove-signature Stub 52 | zip Console-Stub-${{ env.PYTHON_TAG }}-${{ env.BUILD_NUMBER }}.zip Stub 53 | 54 | echo "Build ${{ env.PYTHON_TAG }}-${{ env.BUILD_NUMBER }} GUI stub artefact" 55 | mv "./build/gui-stub/macos/xcode/build/Release/GUI Stub.app/Contents/MacOS/GUI Stub" Stub 56 | codesign --remove-signature Stub 57 | zip GUI-Stub-${{ env.PYTHON_TAG }}-${{ env.BUILD_NUMBER }}.zip Stub 58 | 59 | echo "Stub binaries:" 60 | ls -1 *.zip 61 | 62 | - name: Upload build artefacts 63 | uses: actions/upload-artifact@v6.0.0 64 | with: 65 | name: ${{ env.PYTHON_TAG }}-stubs 66 | path: stub/*.zip 67 | 68 | - name: Upload Release Asset to S3 69 | env: 70 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 71 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 72 | run: | 73 | python -m pip install -U pip 74 | python -m pip install -U setuptools 75 | python -m pip install awscli 76 | 77 | aws s3 cp stub/Console-Stub-${{ env.PYTHON_TAG }}-${{ env.BUILD_NUMBER }}.zip s3://briefcase-support/python/${{ env.PYTHON_TAG }}/macOS/Console-Stub-${{ env.PYTHON_TAG }}-${{ env.BUILD_NUMBER }}.zip 78 | aws s3 cp stub/GUI-Stub-${{ env.PYTHON_TAG }}-${{ env.BUILD_NUMBER }}.zip s3://briefcase-support/python/${{ env.PYTHON_TAG }}/macOS/GUI-Stub-${{ env.PYTHON_TAG }}-${{ env.BUILD_NUMBER }}.zip 79 | 80 | make-release: 81 | name: Make Release 82 | runs-on: macOS-latest 83 | needs: [ build-stubs ] 84 | steps: 85 | - name: Get build artifacts 86 | uses: actions/download-artifact@v7.0.0 87 | with: 88 | path: dist 89 | merge-multiple: true 90 | 91 | - name: Create Release 92 | uses: ncipollo/release-action@v1.20.0 93 | with: 94 | name: ${{ needs.build-stubs.outputs.BUILD_NUMBER }} 95 | tag: ${{ needs.build-stubs.outputs.BUILD_NUMBER }} 96 | draft: true 97 | body: | 98 | Build ${{ needs.build-stubs.outputs.BUILD_NUMBER }} of the Briefcase macOS stub binary. 99 | 100 | Includes support for Python 3.10-3.14. 101 | 102 | artifacts: "dist/*" 103 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Briefcase macOS Xcode Template 2 | 3 | A [Cookiecutter](https://github.com/cookiecutter/cookiecutter/) template for 4 | building Python apps that will run under macOS. 5 | 6 | ## Using this template 7 | 8 | The easiest way to use this project is to not use it at all - at least, not 9 | directly. [Briefcase](https://github.com/beeware/briefcase/) is a tool that 10 | uses this template, rolling it out using data extracted from a 11 | `pyproject.toml` configuration file. 12 | 13 | However, if you *do* want use this template directly... 14 | 15 | 1. Install [cookiecutter](https://github.com/cookiecutter/cookiecutter). 16 | This is a tool used to bootstrap complex project templates: 17 | 18 | ```text 19 | pip install cookiecutter 20 | ``` 21 | 22 | 2. Run `cookiecutter` on the template: 23 | 24 | ```text 25 | cookiecutter https://github.com/beeware/briefcase-macOS-Xcode-template 26 | ``` 27 | 28 | This will ask you for a number of details of your application, including the 29 | `name` of your application (which should be a valid PyPI identifier), and 30 | the `Formal Name` of your application (the full name you use to describe 31 | your app). The remainder of these instructions will assume a `name` of 32 | `my-project`, and a formal name of `My Project`. 33 | 34 | 3. [Obtain a Python Apple support package for macOS](https://github.com/beeware/Python-Apple-support), 35 | and extract it into the `My Project/Support` directory generated by the template. 36 | 37 | 4. Add your code to the template, into the `My Project/My Project/app`. 38 | directory. At the very minimum, you need to have an 39 | `app//__main__.py` file that will be run on startup. 40 | 41 | If your code has any dependencies, they should be installed into the 42 | `My Project/My Project/app_packages` directory. 43 | 44 | If you've done this correctly, a project with a formal name of `My Project`, 45 | with an app name of `my-project` should have a directory structure that 46 | looks something like: 47 | 48 | ```text 49 | My Project/ 50 | My Project/ 51 | app/ 52 | my_project/ 53 | __init__.py 54 | app.py 55 | app_packages/ 56 | ... 57 | ... 58 | My Project.xcodeproj/ 59 | ... 60 | Support/ 61 | ... 62 | VERSIONS 63 | briefcase.toml 64 | ``` 65 | 66 | You're now ready to open the XCode project file, build and run your project! 67 | 68 | ## Next steps 69 | 70 | Of course, running Python code isn't very interesting by itself - you'll be 71 | able to output to the console, and see that output in XCode, but if you tap the 72 | app icon on your phone, you won't see anything - because there isn't a visible 73 | console on an iPhone. 74 | 75 | To do something interesting, you'll need to work with the native macOS system 76 | libraries to draw widgets and respond to screen taps. The 77 | [Rubicon](https://github.com/beeware/rubicon-objc) Objective 78 | C bridging library can be used to interface with the macOS system libraries. 79 | Alternatively, you could use a cross-platform widget toolkit that supports macOS 80 | (such as [Toga](https://beeware.org/project/projects/libraries/toga)) 81 | to provide a GUI for your application. 82 | 83 | Regardless of whether you use Toga, or you write an application natively, the 84 | template project will try to start a Python module matching the name of the 85 | `MainModule` property in the `Info.plist` file associated with the project. 86 | If that module can't be started, any error raised will be logged, and the 87 | Python interpreter will be shut down. All console output and errors are 88 | automatically redirected to the macOS system console. 89 | 90 | If you have any external library dependencies (like Toga, or anything other 91 | third-party library), you should install the library code into the 92 | `app_packages` directory. This directory is the same as a `site_packages` 93 | directory on a desktop Python install. 94 | -------------------------------------------------------------------------------- /{{ cookiecutter.format }}/{{ cookiecutter.class_name }}/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | {{ cookiecutter.formal_name }} 9 | CFBundleIconFile 10 | {{ cookiecutter.formal_name }} 11 | CFBundleIconName 12 | {{ cookiecutter.formal_name }} 13 | CFBundleIdentifier 14 | {{ cookiecutter.bundle }}.{{ cookiecutter.app_name }} 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleName 18 | {{ cookiecutter.formal_name }} 19 | CFBundlePackageType 20 | APPL 21 | CFBundleShortVersionString 22 | {{ cookiecutter.version }} 23 | CFBundleVersion 24 | {{ cookiecutter.build }} 25 | LSMinimumSystemVersion 26 | {{ cookiecutter.min_os_version }} 27 | NSMainStoryboardFile 28 | Main 29 | NSPrincipalClass 30 | NSApplication 31 | MainModule 32 | {{ cookiecutter.module_name }} 33 | {% if cookiecutter.document_types -%} 34 | CFBundleDocumentTypes 35 | 36 | {% for document_type_id, document_type in cookiecutter.document_types.items() -%} 37 | {% set macOS = document_type.macOS | default({}) -%} 38 | 39 | CFBundleTypeName 40 | {{ document_type.description }} 41 | CFBundleTypeIconFile 42 | {{ cookiecutter.app_name }}-{{ document_type_id }}.icns 43 | CFBundleTypeRole 44 | {{ macOS.CFBundleTypeRole }} 45 | LSHandlerRank 46 | {{ macOS.LSHandlerRank }} 47 | LSItemContentTypes 48 | 49 | {%- if macOS.LSItemContentTypes is defined %} 50 | {{ macOS.LSItemContentTypes[0] }} 51 | {%- else %} 52 | {{ cookiecutter.bundle }}.{{ cookiecutter.app_name }}.{{ document_type_id }} 53 | {%- endif %} 54 | 55 | 56 | {% endfor %} 57 | 58 | {%- macro type_declaration(cookiecutter, document_type_id, document_type, macOS) -%} 59 | 60 | UTTypeConformsTo 61 | 62 | {%- for type in macOS.UTTypeConformsTo %} 63 | {{ type }} 64 | {%- endfor %} 65 | 66 | UTTypeDescription 67 | {{ document_type.description }} 68 | UTTypeIconFile 69 | {{ cookiecutter.app_name }}-{{ document_type_id }}.icns 70 | UTTypeIdentifier 71 | {% if macOS.LSItemContentTypes is defined -%} 72 | {{ macOS.LSItemContentTypes[0] }} 73 | {%- else -%} 74 | {{ cookiecutter.bundle }}.{{ cookiecutter.app_name }}.{{ document_type_id }} 75 | {%- endif %} 76 | UTTypeReferenceURL 77 | {{ document_type.url }} 78 | UTTypeTagSpecification 79 | 80 | public.filename-extension 81 | 82 | {{ document_type.extension }} 83 | 84 | public.mime-type 85 | {% if document_type.get('mime_type') %}{{ document_type.mime_type }}{% else %}application/x-{{ cookiecutter.app_name }}-{{ document_type_id }}{% endif %} 86 | 87 | 88 | {% endmacro %} 89 | UTExportedTypeDeclarations 90 | 91 | {% for document_type_id, document_type in cookiecutter.document_types.items() -%} 92 | {% set macOS = document_type.macOS | default({}) -%} 93 | {% if macOS.LSHandlerRank == "Owner" -%} 94 | {{ type_declaration(cookiecutter, document_type_id, document_type, macOS) }} 95 | {%- endif %} 96 | {%- endfor %} 97 | 98 | UTImportedTypeDeclarations 99 | 100 | {% for document_type_id, document_type in cookiecutter.document_types.items() -%} 101 | {% set macOS = document_type.macOS | default({}) -%} 102 | {% if macOS.is_core_type is false and macOS.LSHandlerRank != "Owner" -%} 103 | {{ type_declaration(cookiecutter, document_type_id, document_type, macOS) }} 104 | {%- endif %} 105 | {%- endfor %} 106 | 107 | {%- endif -%} 108 | {%- if cookiecutter.info -%} 109 | {%- for permission, value in cookiecutter.info.items() %} 110 | {{ permission }} 111 | {{ value|plist_value }} 112 | {%- endfor -%} 113 | {%- endif %} 114 | 115 | 116 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 10 | # BeeWare Community Code of Conduct 11 | 12 | ## Our pledge 13 | 14 | We pledge to make our community welcoming, safe, and equitable for all. 15 | 16 | We are committed to fostering an environment that respects and promotes the dignity, rights, and contributions of all individuals, regardless of characteristics including race, ethnicity, caste, color, age, physical characteristics, neurodiversity, disability, sex or gender, gender identity or expression, sexual orientation, language, philosophy or religion, national or social origin, socio-economic position, level of education, or other status. The same privileges of participation are extended to everyone who participates in good faith and in accordance with this Covenant. 17 | 18 | The guidelines within and enforcement of the BeeWare Community Code of Conduct apply equally to everyone participating in the BeeWare community, including members of the Code of Conduct Response Team. 19 | 20 | ## Encouraged behaviors 21 | 22 | While acknowledging differences in social norms, we all strive to meet our community's expectations for positive behavior. We also understand that our words and actions may be interpreted differently than we intend based on culture, background, or native language. 23 | 24 | With these considerations in mind, we agree to behave mindfully toward each other and act in ways that center our shared values, including: 25 | 26 | 1. Respecting the **purpose of our community**, our activities, and our ways of gathering. 27 | 2. Engaging **kindly and honestly** with others. 28 | 3. Respecting **different viewpoints** and experiences. 29 | 4. **Taking responsibility** for our actions and contributions. 30 | 5. Gracefully giving and accepting **constructive feedback**. 31 | 6. Committing to **repairing harm** when it occurs. 32 | 7. Behaving in other ways that promote and sustain the **well-being of our community**. 33 | 34 | ## Restricted behaviors 35 | 36 | We agree to restrict the following behaviors in our community. Instances, threats, and promotion of these behaviors are violations of this Code of Conduct. 37 | 38 | 1. **Harassment.** Violating explicitly expressed boundaries or engaging in unnecessary personal attention after any clear request to stop. 39 | 2. **Character attacks.** Making insulting, demeaning, or pejorative comments directed at a community member or group of people. 40 | 3. **Stereotyping or discrimination.** Characterizing anyone’s personality or behavior on the basis of a personal identity or trait. 41 | 4. **Sexualization.** Behaving in a way that would generally be considered inappropriately intimate in the context or purpose of the community. 42 | 5. **Violating confidentiality.** Sharing or acting on someone's personal or private information without their permission. 43 | 6. **Endangerment.** Causing, encouraging, or threatening violence or other harm toward any person or group. 44 | 7. Behaving in other ways that **threaten the well-being** of our community. 45 | 46 | ### Other restrictions 47 | 48 | 1. **Misleading identity.** Impersonating someone else for any reason, or pretending to be someone else to evade enforcement actions. 49 | 2. **Misrepresenting project affiliation.** Speaking or acting in a way that implies an official affiliation with the BeeWare project, where one does not exist. 50 | 3. **Failing to credit sources.** Not properly crediting the sources of content you contribute. 51 | 4. **Promotional materials.** Sharing marketing or other commercial content in a way that is outside the norms of the community. 52 | 5. **Excessive communication.** Disrespecting the time and space of others by engaging in an unacceptable volume of communication. 53 | 6. **Unhelpful communication.** Offering opinions without relevant experience in the topic being discussed, entering into an ongoing discussion without first gaining familiarity with the history of the topic, or making contributions that are off-topic or otherwise distracting. 54 | 7. **Irresponsible messaging.** Presenting content which includes, links, or describes other restricted behaviors without a relevant reason and appropriate prior warnings for consumers of that content. 55 | 8. Other conduct that could reasonably be considered **unprofessional or inappropriate**. 56 | 57 | ## Reporting an issue 58 | 59 | Tensions can occur between community members even when they are trying their best to collaborate. Not every conflict represents a code of conduct violation, and this Code of Conduct reinforces encouraged behaviors and norms that can help avoid conflicts and minimize harm. Reporting even minor issues is important, as they can be helpful in identifying patterns of behavior that may not be concerning in isolation, but when viewed collectively may be more significant. 60 | 61 | When an incident does occur, it is important to report it promptly to the BeeWare Code of Conduct Response Team. 62 | 63 | **If you believe you or anyone else is in physical danger, please notify appropriate law enforcement first.** 64 | 65 | To report a possible violation, email the Team at [conduct@beeware.org](mailto:conduct@beeware.org). If necessary, you can reach out to individual team members. On the BeeWare Discord server, you can also direct message anyone on the Response Team, or, if appropriate, mention `@moderators` in a public channel. Team members can be reached by the following usernames on Discord or GitHub, or the provided email addresses: 66 | 67 | * Russell Keith-Magee (@freakboy3742; [russell@beeware.org](mailto:russell@beeware.org)) 68 | * Kattni (@kattni; [kattni@beeware.org](mailto:kattni@beeware.org)) 69 | * Katie McLaughlin (@glasnt; [katie@beeware.org](mailto:katie@beeware.org)) 70 | * Philip James (@phildini; [philip@beeware.org](mailto:philip@beeware.org)) 71 | * Charles Whittington (@HalfWhitt; [charles@beeware.org](mailto:charles@beeware.org)) 72 | 73 | The Response Team takes reports of violations seriously and will make every effort to respond in a timely manner. They will investigate all reports of code of conduct violations, reviewing messages, logs, and recordings, or interviewing witnesses and other participants. The Team will keep investigation and enforcement actions as transparent as possible while prioritizing safety and confidentiality. In order to honor these values, enforcement actions are carried out in private with the involved parties, but communicating to the whole community may be part of a mutually agreed upon resolution. If we determine that a public statement needs to be made, the identities of all victims and reporters will remain confidential unless those individuals instruct us otherwise. 74 | 75 | In your report, please include: 76 | 77 | * **Your contact info** so we can get in touch with you if we need to follow up. 78 | * **Names (real, nicknames, or pseudonyms) of any individuals involved.** If there were other witnesses besides you, please try to include them as well. 79 | * **When and where the incident occurred.** Please be as specific as possible. 80 | * **Your account of what occurred.** If there is a publicly available record (e.g. a Discord or GitHub message) please include a link. 81 | * **Any extra context** you believe existed for the incident. 82 | * **If you believe this incident is ongoing.** 83 | * **If you believe any member of the Response Team has a conflict of interest** in adjudicating the incident. 84 | * **What, if any, corrective response** you believe would be appropriate. 85 | * **Any other information** you believe we should have. 86 | 87 | Code of Conduct Response Team members are obligated to maintain confidentiality with regard to the reporter and details of an incident. 88 | 89 | ## The Response Team's report followup 90 | 91 | You will receive a response acknowledging receipt of your report. We promise to acknowledge receipt within 24 hours (and will aim for much quicker than that). 92 | 93 | The Response Team will immediately meet to review the incident and determine: 94 | 95 | * What happened. 96 | * Whether this event constitutes a code of conduct violation. 97 | * Who the reported person is. 98 | * Whether this is an ongoing situation, or if there is a threat to anyone's physical safety. 99 | * If this is determined to be an ongoing incident or a threat to physical safety, the Response Team's immediate priority will be to protect everyone involved. This means we may delay an official response until we believe that the situation has concluded and that everyone is physically safe. 100 | * If a member of the Response Team is one of the named parties, they will not be included in any discussions, and will not be provided with any confidential details from the reporter. 101 | 102 | If anyone on the Response Team believes they have a conflict of interest in adjudicating on a reported issue, they will inform the other Response Team members, and recuse themselves from any discussion about the issue. Following this declaration, they will not be provided with any confidential details from the reporter. 103 | 104 | We'll respond within one week to the person who filed the report with either a resolution or an explanation of why the situation is not yet resolved. 105 | 106 | Once we've determined our final action, we'll contact the original reporter to let them know what action (if any) we'll be taking. We'll take into account feedback from the reporter on the appropriateness of our response, but we don't guarantee we'll act on it. 107 | 108 | Finally, to maintain transparency in the reporting and enforcement process, whenever possible, the Response Team will make a public report of the incident on [The Buzz](https://beeware.org/news/buzz), the BeeWare blog. A public report may not be made if the specifics of the incident do not allow us to preserve anonymity, or if there is potential for ongoing harm. 109 | 110 | ## Enforcement: addressing and repairing harm 111 | 112 | If an investigation by the Response Team finds that this Code of Conduct has been violated, the following enforcement ladder may be used to determine how best to repair harm, based on the incident's impact on the individuals involved and the community as a whole. Depending on the severity of a violation, lower rungs on the ladder may be skipped. 113 | 114 | 1. Warning 115 | * Event: A violation involving a single incident or series of incidents. 116 | * Consequence: A private, written warning from the Response Team. 117 | * Repair: Examples of repair include a private written apology, acknowledgement of responsibility, and seeking clarification on expectations. 118 | 119 | 2. Temporarily Limited Activities 120 | * Event: A repeated incidence of a violation that previously resulted in a warning, or the first incidence of a more serious violation. 121 | * Consequence: A private, written warning with a time-limited cooldown period designed to underscore the seriousness of the situation and give the community members involved time to process the incident. The cooldown period may be limited to particular communication channels or interactions with particular community members. 122 | * Repair: Examples of repair may include making an apology, using the cooldown period to reflect on actions and impact, and being thoughtful about re-entering community spaces after the period is over. 123 | 124 | 3. Temporary Suspension 125 | * Event: A pattern of repeated violation which the Response Team has tried to address with warnings, or a single serious violation. 126 | * Consequence: A private written warning with conditions for return from suspension. In general, temporary suspensions give the person being suspended time to reflect upon their behavior and possible corrective actions. Suspensions will be based on where the violation occurs, and may be limited to the space in which the violation occurs. In the event of a more serious violation, the suspension may apply to all spaces. 127 | * Repair: Examples of repair include respecting the spirit of the suspension, meeting the specified conditions for return, and being thoughtful about how to reintegrate with the community when the suspension is lifted. 128 | 129 | 4. Permanent Ban 130 | * Event: A pattern of repeated code of conduct violations that other steps on the ladder have failed to resolve, or a violation so serious that the Response Team determines there is no way to keep the community safe with this person as a member. 131 | * Consequence: Access to all community spaces, tools, and communication channels is removed. In general, permanent bans should be rarely used, should have strong reasoning behind them, and should only be resorted to if working through other remedies has failed to change the behavior. 132 | * Repair: There is no possible repair in cases of this severity. 133 | 134 | This enforcement ladder is intended as a guideline. It does not limit the ability of Community Managers to use their discretion and judgment, in keeping with the best interests of our community. 135 | 136 | ## Scope 137 | 138 | This Code of Conduct applies within all community spaces, including GitHub, the BeeWare Discord server, and in-person events, such as conferences, meetups, and sprints. It also applies when an individual is officially representing the community in public or other spaces. Examples of representing our community include using an official email address, posting via an official social media account, or acting as an appointed representative at an online or offline event. 139 | 140 | Behavior outside of official BeeWare spaces may also be considered as supporting evidence for a report if that behavior establishes a pattern, or represents a potential risk to the BeeWare community. 141 | 142 | This Code of Conduct operates in parallel to any Code of Conduct that is in effect in a given context (e.g., the Code of Conduct for a conference). If an incident occurs, we encourage reporting that incident to all relevant conduct groups. Known violations of other Codes of Conduct may be considered as supporting evidence for a report under this Code of Conduct. The BeeWare Code of Conduct Response Team will cooperate with other Code of Conduct teams, but will not disclose any identifying details without the prior consent of the reporting party. 143 | ## Attribution 144 | 145 | This Code of Conduct is adapted from the Contributor Covenant, version 3.0, permanently available at [https://www.contributor-covenant.org/version/3/0/](https://www.contributor-covenant.org/version/3/0/). 146 | 147 | Contributor Covenant is stewarded by the Organization for Ethical Source and licensed under [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/). 148 | 149 | For answers to common questions about Contributor Covenant, see the [FAQ](https://www.contributor-covenant.org/faq). [Translations](https://www.contributor-covenant.org/translations) are provided. There are [additional enforcement and community guideline resources](https://www.contributor-covenant.org/resources). The enforcement ladder was inspired by the work of [Mozilla’s code of conduct team](https://github.com/mozilla/inclusion). 150 | 151 | ## Changes 152 | 153 | Major substantive changes are listed here; for a complete list of changes see the GitHub commit history. 154 | 155 | * **December 15, 2025:** Updated to adapt the Contributor Covenant, version 3.0, with some modifications for BeeWare-specific guidelines and procedures. 156 | 157 | * **July 4, 2016:** Added instructions and guidelines for reporting incidents. 158 | 159 | * **December 5, 2015:** Initial Code of Conduct adopted. 160 | -------------------------------------------------------------------------------- /{{ cookiecutter.format }}/{{ cookiecutter.formal_name }}.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 55; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 0D267B6B2554BB1800AC3E85 /* app_packages in Resources */ = {isa = PBXBuildFile; fileRef = 0D267B692554BB1800AC3E85 /* app_packages */; }; 11 | 0D267B6C2554BB1800AC3E85 /* app in Resources */ = {isa = PBXBuildFile; fileRef = 0D267B6A2554BB1800AC3E85 /* app */; }; 12 | 0D354FD22551BFBD009178D1 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0D354FD12551BFBD009178D1 /* Assets.xcassets */; }; 13 | 0D354FE62551C1E1009178D1 /* AppKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0D354FE52551C1E1009178D1 /* AppKit.framework */; }; 14 | 0D354FEF2551C249009178D1 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0D354FEE2551C249009178D1 /* Cocoa.framework */; }; 15 | 0D7B44A82555E01500CBC44B /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0D7B44A72555E01500CBC44B /* Foundation.framework */; }; 16 | 0D7B44DA2556C84100CBC44B /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D354FD72551BFBD009178D1 /* main.m */; }; 17 | 6060E7722AF0B40500C04AE0 /* Python.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6060E76F2AF0B14D00C04AE0 /* Python.xcframework */; }; 18 | 6060E7732AF0B40500C04AE0 /* Python.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 6060E76F2AF0B14D00C04AE0 /* Python.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 19 | /* End PBXBuildFile section */ 20 | 21 | /* Begin PBXCopyFilesBuildPhase section */ 22 | 0D7B44EB2556C8B800CBC44B /* Embed Foundation Extensions */ = { 23 | isa = PBXCopyFilesBuildPhase; 24 | buildActionMask = 2147483647; 25 | dstPath = ""; 26 | dstSubfolderSpec = 13; 27 | files = ( 28 | ); 29 | name = "Embed Foundation Extensions"; 30 | runOnlyForDeploymentPostprocessing = 0; 31 | }; 32 | 6060E7742AF0B40500C04AE0 /* Embed Frameworks */ = { 33 | isa = PBXCopyFilesBuildPhase; 34 | buildActionMask = 2147483647; 35 | dstPath = ""; 36 | dstSubfolderSpec = 10; 37 | files = ( 38 | 6060E7732AF0B40500C04AE0 /* Python.xcframework in Embed Frameworks */, 39 | ); 40 | name = "Embed Frameworks"; 41 | runOnlyForDeploymentPostprocessing = 0; 42 | }; 43 | /* End PBXCopyFilesBuildPhase section */ 44 | 45 | /* Begin PBXFileReference section */ 46 | 0D267B692554BB1800AC3E85 /* app_packages */ = {isa = PBXFileReference; lastKnownFileType = folder; path = app_packages; sourceTree = ""; }; 47 | 0D267B6A2554BB1800AC3E85 /* app */ = {isa = PBXFileReference; lastKnownFileType = folder; path = app; sourceTree = ""; }; 48 | 0D354FC82551BFBA009178D1 /* {{ cookiecutter.formal_name }}.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "{{ cookiecutter.formal_name }}.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 49 | 0D354FD12551BFBD009178D1 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 50 | 0D354FD62551BFBD009178D1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 51 | 0D354FD72551BFBD009178D1 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 52 | 0D354FD92551BFBD009178D1 /* {{ cookiecutter.app_name }}.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = {{ cookiecutter.app_name }}.entitlements; sourceTree = ""; }; 53 | 0D354FE52551C1E1009178D1 /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = System/Library/Frameworks/AppKit.framework; sourceTree = SDKROOT; }; 54 | 0D354FEE2551C249009178D1 /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; }; 55 | 0D7B44A72555E01500CBC44B /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; 56 | 6060E76F2AF0B14D00C04AE0 /* Python.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = Python.xcframework; sourceTree = ""; }; 57 | /* End PBXFileReference section */ 58 | 59 | /* Begin PBXFrameworksBuildPhase section */ 60 | 0D354FC52551BFBA009178D1 /* Frameworks */ = { 61 | isa = PBXFrameworksBuildPhase; 62 | buildActionMask = 2147483647; 63 | files = ( 64 | 0D354FE62551C1E1009178D1 /* AppKit.framework in Frameworks */, 65 | 0D354FEF2551C249009178D1 /* Cocoa.framework in Frameworks */, 66 | 0D7B44A82555E01500CBC44B /* Foundation.framework in Frameworks */, 67 | 6060E7722AF0B40500C04AE0 /* Python.xcframework in Frameworks */, 68 | ); 69 | runOnlyForDeploymentPostprocessing = 0; 70 | }; 71 | /* End PBXFrameworksBuildPhase section */ 72 | 73 | /* Begin PBXGroup section */ 74 | 0D354FBF2551BFBA009178D1 = { 75 | isa = PBXGroup; 76 | children = ( 77 | 60A04BBB28AF5E1000DAA9E5 /* Support */, 78 | 0D354FCA2551BFBA009178D1 /* {{ cookiecutter.class_name }} */, 79 | 0D354FC92551BFBA009178D1 /* Products */, 80 | 0D354FE42551C1E1009178D1 /* Frameworks */, 81 | ); 82 | sourceTree = ""; 83 | }; 84 | 0D354FC92551BFBA009178D1 /* Products */ = { 85 | isa = PBXGroup; 86 | children = ( 87 | 0D354FC82551BFBA009178D1 /* {{ cookiecutter.formal_name }}.app */, 88 | ); 89 | name = Products; 90 | sourceTree = ""; 91 | }; 92 | 0D354FCA2551BFBA009178D1 /* {{ cookiecutter.class_name }} */ = { 93 | isa = PBXGroup; 94 | children = ( 95 | 0D267B6A2554BB1800AC3E85 /* app */, 96 | 0D267B692554BB1800AC3E85 /* app_packages */, 97 | 0D354FD12551BFBD009178D1 /* Assets.xcassets */, 98 | 0D354FD62551BFBD009178D1 /* Info.plist */, 99 | 0D354FD72551BFBD009178D1 /* main.m */, 100 | 0D354FD92551BFBD009178D1 /* {{ cookiecutter.app_name }}.entitlements */, 101 | ); 102 | path = "{{ cookiecutter.class_name }}"; 103 | sourceTree = ""; 104 | }; 105 | 0D354FE42551C1E1009178D1 /* Frameworks */ = { 106 | isa = PBXGroup; 107 | children = ( 108 | 0D354FE52551C1E1009178D1 /* AppKit.framework */, 109 | 0D354FEE2551C249009178D1 /* Cocoa.framework */, 110 | 0D7B44A72555E01500CBC44B /* Foundation.framework */, 111 | ); 112 | name = Frameworks; 113 | sourceTree = ""; 114 | }; 115 | 60A04BBB28AF5E1000DAA9E5 /* Support */ = { 116 | isa = PBXGroup; 117 | children = ( 118 | 6060E76F2AF0B14D00C04AE0 /* Python.xcframework */, 119 | ); 120 | path = Support; 121 | sourceTree = ""; 122 | }; 123 | /* End PBXGroup section */ 124 | 125 | /* Begin PBXNativeTarget section */ 126 | 0D354FC72551BFBA009178D1 /* {{ cookiecutter.formal_name }} */ = { 127 | isa = PBXNativeTarget; 128 | buildConfigurationList = 0D354FDC2551BFBD009178D1 /* Build configuration list for PBXNativeTarget "{{ cookiecutter.formal_name }}" */; 129 | buildPhases = ( 130 | 0D354FC42551BFBA009178D1 /* Sources */, 131 | 0D354FC52551BFBA009178D1 /* Frameworks */, 132 | 0D354FC62551BFBA009178D1 /* Resources */, 133 | 0D7B44EB2556C8B800CBC44B /* Embed Foundation Extensions */, 134 | 6060E7742AF0B40500C04AE0 /* Embed Frameworks */, 135 | 60A04BC128AF640400DAA9E5 /* Sign Python Binary Modules */, 136 | ); 137 | buildRules = ( 138 | ); 139 | dependencies = ( 140 | ); 141 | name = "{{ cookiecutter.formal_name }}"; 142 | productName = "{{ cookiecutter.formal_name }}"; 143 | productReference = 0D354FC82551BFBA009178D1 /* {{ cookiecutter.formal_name }}.app */; 144 | productType = "com.apple.product-type.application"; 145 | }; 146 | /* End PBXNativeTarget section */ 147 | 148 | /* Begin PBXProject section */ 149 | 0D354FC02551BFBA009178D1 /* Project object */ = { 150 | isa = PBXProject; 151 | attributes = { 152 | BuildIndependentTargetsInParallel = YES; 153 | LastUpgradeCheck = 1540; 154 | ORGANIZATIONNAME = "{{ cookiecutter.author }}"; 155 | TargetAttributes = { 156 | 0D354FC72551BFBA009178D1 = { 157 | CreatedOnToolsVersion = 12.2; 158 | }; 159 | }; 160 | }; 161 | buildConfigurationList = 0D354FC32551BFBA009178D1 /* Build configuration list for PBXProject "{{ cookiecutter.formal_name }}" */; 162 | compatibilityVersion = "Xcode 13.0"; 163 | developmentRegion = en; 164 | hasScannedForEncodings = 0; 165 | knownRegions = ( 166 | en, 167 | Base, 168 | ); 169 | mainGroup = 0D354FBF2551BFBA009178D1; 170 | productRefGroup = 0D354FC92551BFBA009178D1 /* Products */; 171 | projectDirPath = ""; 172 | projectRoot = ""; 173 | targets = ( 174 | 0D354FC72551BFBA009178D1 /* {{ cookiecutter.formal_name }} */, 175 | ); 176 | }; 177 | /* End PBXProject section */ 178 | 179 | /* Begin PBXResourcesBuildPhase section */ 180 | 0D354FC62551BFBA009178D1 /* Resources */ = { 181 | isa = PBXResourcesBuildPhase; 182 | buildActionMask = 2147483647; 183 | files = ( 184 | 0D354FD22551BFBD009178D1 /* Assets.xcassets in Resources */, 185 | 0D267B6C2554BB1800AC3E85 /* app in Resources */, 186 | 0D267B6B2554BB1800AC3E85 /* app_packages in Resources */, 187 | ); 188 | runOnlyForDeploymentPostprocessing = 0; 189 | }; 190 | /* End PBXResourcesBuildPhase section */ 191 | 192 | /* Begin PBXShellScriptBuildPhase section */ 193 | 60A04BC128AF640400DAA9E5 /* Sign Python Binary Modules */ = { 194 | isa = PBXShellScriptBuildPhase; 195 | alwaysOutOfDate = 1; 196 | buildActionMask = 2147483647; 197 | files = ( 198 | ); 199 | inputFileListPaths = ( 200 | ); 201 | inputPaths = ( 202 | ); 203 | name = "Sign Python Binary Modules"; 204 | outputFileListPaths = ( 205 | ); 206 | outputPaths = ( 207 | ); 208 | runOnlyForDeploymentPostprocessing = 0; 209 | shellPath = /bin/sh; 210 | shellScript = "set -e\necho \"Signed as $EXPANDED_CODE_SIGN_IDENTITY_NAME ($EXPANDED_CODE_SIGN_IDENTITY)\"\nfind \"$BUILT_PRODUCTS_DIR/$UNLOCALIZED_RESOURCES_FOLDER_PATH/app_packages\" -name \"*.so\" -exec /usr/bin/codesign --force --sign \"$EXPANDED_CODE_SIGN_IDENTITY\" -o runtime --timestamp=none --preserve-metadata=identifier,entitlements,flags --generate-entitlement-der {} \\; \nfind \"$BUILT_PRODUCTS_DIR/$UNLOCALIZED_RESOURCES_FOLDER_PATH/app\" -name \"*.so\" -exec /usr/bin/codesign --force --sign \"$EXPANDED_CODE_SIGN_IDENTITY\" -o runtime --timestamp=none --preserve-metadata=identifier,entitlements,flags --generate-entitlement-der {} \\; \n"; 211 | }; 212 | /* End PBXShellScriptBuildPhase section */ 213 | 214 | /* Begin PBXSourcesBuildPhase section */ 215 | 0D354FC42551BFBA009178D1 /* Sources */ = { 216 | isa = PBXSourcesBuildPhase; 217 | buildActionMask = 2147483647; 218 | files = ( 219 | 0D7B44DA2556C84100CBC44B /* main.m in Sources */, 220 | ); 221 | runOnlyForDeploymentPostprocessing = 0; 222 | }; 223 | /* End PBXSourcesBuildPhase section */ 224 | 225 | /* Begin XCBuildConfiguration section */ 226 | 0D354FDA2551BFBD009178D1 /* Debug */ = { 227 | isa = XCBuildConfiguration; 228 | buildSettings = { 229 | ALWAYS_SEARCH_USER_PATHS = NO; 230 | CLANG_ANALYZER_NONNULL = YES; 231 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 232 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 233 | CLANG_CXX_LIBRARY = "libc++"; 234 | CLANG_ENABLE_MODULES = YES; 235 | CLANG_ENABLE_OBJC_ARC = YES; 236 | CLANG_ENABLE_OBJC_WEAK = YES; 237 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 238 | CLANG_WARN_BOOL_CONVERSION = YES; 239 | CLANG_WARN_COMMA = YES; 240 | CLANG_WARN_CONSTANT_CONVERSION = YES; 241 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 242 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 243 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 244 | CLANG_WARN_EMPTY_BODY = YES; 245 | CLANG_WARN_ENUM_CONVERSION = YES; 246 | CLANG_WARN_INFINITE_RECURSION = YES; 247 | CLANG_WARN_INT_CONVERSION = YES; 248 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 249 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 250 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 251 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 252 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 253 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 254 | CLANG_WARN_STRICT_PROTOTYPES = YES; 255 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 256 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 257 | CLANG_WARN_UNREACHABLE_CODE = YES; 258 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 259 | COPY_PHASE_STRIP = NO; 260 | DEAD_CODE_STRIPPING = YES; 261 | DEBUG_INFORMATION_FORMAT = dwarf; 262 | ENABLE_STRICT_OBJC_MSGSEND = YES; 263 | ENABLE_TESTABILITY = YES; 264 | ENABLE_USER_SCRIPT_SANDBOXING = NO; 265 | FRAMEWORK_SEARCH_PATHS = ( 266 | "$(inherited)", 267 | "\"$(PROJECT_DIR)\"", 268 | ); 269 | GCC_C_LANGUAGE_STANDARD = gnu99; 270 | GCC_DYNAMIC_NO_PIC = NO; 271 | GCC_NO_COMMON_BLOCKS = YES; 272 | GCC_OPTIMIZATION_LEVEL = 0; 273 | GCC_PREPROCESSOR_DEFINITIONS = ( 274 | "DEBUG=1", 275 | "$(inherited)", 276 | ); 277 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 278 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 279 | GCC_WARN_UNDECLARED_SELECTOR = YES; 280 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 281 | GCC_WARN_UNUSED_FUNCTION = YES; 282 | GCC_WARN_UNUSED_VARIABLE = YES; 283 | MACOSX_DEPLOYMENT_TARGET = {{ cookiecutter.min_os_version }}; 284 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 285 | MTL_FAST_MATH = YES; 286 | ONLY_ACTIVE_ARCH = YES; 287 | PRODUCT_NAME = "{{ cookiecutter.formal_name }}"; 288 | SDKROOT = macosx; 289 | }; 290 | name = Debug; 291 | }; 292 | 0D354FDB2551BFBD009178D1 /* Release */ = { 293 | isa = XCBuildConfiguration; 294 | buildSettings = { 295 | ALWAYS_SEARCH_USER_PATHS = NO; 296 | CLANG_ANALYZER_NONNULL = YES; 297 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 298 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 299 | CLANG_CXX_LIBRARY = "libc++"; 300 | CLANG_ENABLE_MODULES = YES; 301 | CLANG_ENABLE_OBJC_ARC = YES; 302 | CLANG_ENABLE_OBJC_WEAK = YES; 303 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 304 | CLANG_WARN_BOOL_CONVERSION = YES; 305 | CLANG_WARN_COMMA = YES; 306 | CLANG_WARN_CONSTANT_CONVERSION = YES; 307 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 308 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 309 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 310 | CLANG_WARN_EMPTY_BODY = YES; 311 | CLANG_WARN_ENUM_CONVERSION = YES; 312 | CLANG_WARN_INFINITE_RECURSION = YES; 313 | CLANG_WARN_INT_CONVERSION = YES; 314 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 315 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 316 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 317 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 318 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 319 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 320 | CLANG_WARN_STRICT_PROTOTYPES = YES; 321 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 322 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 323 | CLANG_WARN_UNREACHABLE_CODE = YES; 324 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 325 | COPY_PHASE_STRIP = NO; 326 | DEAD_CODE_STRIPPING = YES; 327 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 328 | ENABLE_NS_ASSERTIONS = NO; 329 | ENABLE_STRICT_OBJC_MSGSEND = YES; 330 | ENABLE_USER_SCRIPT_SANDBOXING = NO; 331 | FRAMEWORK_SEARCH_PATHS = ( 332 | "$(inherited)", 333 | "\"$(PROJECT_DIR)\"", 334 | ); 335 | GCC_C_LANGUAGE_STANDARD = gnu99; 336 | GCC_NO_COMMON_BLOCKS = YES; 337 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 338 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 339 | GCC_WARN_UNDECLARED_SELECTOR = YES; 340 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 341 | GCC_WARN_UNUSED_FUNCTION = YES; 342 | GCC_WARN_UNUSED_VARIABLE = YES; 343 | MACOSX_DEPLOYMENT_TARGET = {{ cookiecutter.min_os_version }}; 344 | MTL_ENABLE_DEBUG_INFO = NO; 345 | MTL_FAST_MATH = YES; 346 | PRODUCT_NAME = "{{ cookiecutter.formal_name }}"; 347 | SDKROOT = macosx; 348 | }; 349 | name = Release; 350 | }; 351 | 0D354FDD2551BFBD009178D1 /* Debug */ = { 352 | isa = XCBuildConfiguration; 353 | buildSettings = { 354 | ARCHS = "{% if cookiecutter.universal_build %}$(ARCHS_STANDARD){% else %}{{ cookiecutter.host_arch }}{% endif %}"; 355 | ASSETCATALOG_COMPILER_APPICON_NAME = "{{ cookiecutter.formal_name }}"; 356 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 357 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 358 | CLANG_CXX_LIBRARY = "libc++"; 359 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO; 360 | CODE_SIGN_ENTITLEMENTS = "{{ cookiecutter.class_name }}/{{ cookiecutter.app_name }}.entitlements"; 361 | CODE_SIGN_IDENTITY = "-"; 362 | CODE_SIGN_STYLE = Automatic; 363 | COMBINE_HIDPI_IMAGES = YES; 364 | DEAD_CODE_STRIPPING = YES; 365 | ENABLE_HARDENED_RUNTIME = YES; 366 | FRAMEWORK_SEARCH_PATHS = ( 367 | "$(inherited)", 368 | "\"$(PROJECT_DIR)/Support\"", 369 | ); 370 | GCC_C_LANGUAGE_STANDARD = gnu99; 371 | HEADER_SEARCH_PATHS = "\"$(BUILT_PRODUCTS_DIR)/Python.framework/Headers\""; 372 | INFOPLIST_FILE = "{{ cookiecutter.class_name }}/Info.plist"; 373 | LD_RUNPATH_SEARCH_PATHS = ( 374 | "$(inherited)", 375 | "@executable_path/../Frameworks", 376 | ); 377 | MACOSX_DEPLOYMENT_TARGET = {{ cookiecutter.min_os_version }}; 378 | PRODUCT_BUNDLE_IDENTIFIER = "{{ cookiecutter.bundle }}.{{ cookiecutter.app_name }}"; 379 | PROVISIONING_PROFILE_SPECIFIER = ""; 380 | }; 381 | name = Debug; 382 | }; 383 | 0D354FDE2551BFBD009178D1 /* Release */ = { 384 | isa = XCBuildConfiguration; 385 | buildSettings = { 386 | ARCHS = "{% if cookiecutter.universal_build %}$(ARCHS_STANDARD){% else %}{{ cookiecutter.host_arch }}{% endif %}"; 387 | ASSETCATALOG_COMPILER_APPICON_NAME = "{{ cookiecutter.formal_name }}"; 388 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 389 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 390 | CLANG_CXX_LIBRARY = "libc++"; 391 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO; 392 | CODE_SIGN_ENTITLEMENTS = "{{ cookiecutter.class_name }}/{{ cookiecutter.app_name }}.entitlements"; 393 | CODE_SIGN_IDENTITY = "-"; 394 | CODE_SIGN_STYLE = Automatic; 395 | COMBINE_HIDPI_IMAGES = YES; 396 | DEAD_CODE_STRIPPING = YES; 397 | ENABLE_HARDENED_RUNTIME = YES; 398 | ENABLE_TESTABILITY = YES; 399 | FRAMEWORK_SEARCH_PATHS = ( 400 | "$(inherited)", 401 | "\"$(PROJECT_DIR)/Support\"", 402 | ); 403 | GCC_C_LANGUAGE_STANDARD = gnu99; 404 | HEADER_SEARCH_PATHS = "\"$(BUILT_PRODUCTS_DIR)/Python.framework/Headers\""; 405 | INFOPLIST_FILE = "{{ cookiecutter.class_name }}/Info.plist"; 406 | LD_RUNPATH_SEARCH_PATHS = ( 407 | "$(inherited)", 408 | "@executable_path/../Frameworks", 409 | ); 410 | MACOSX_DEPLOYMENT_TARGET = {{ cookiecutter.min_os_version }}; 411 | PRODUCT_BUNDLE_IDENTIFIER = "{{ cookiecutter.bundle }}.{{ cookiecutter.app_name }}"; 412 | }; 413 | name = Release; 414 | }; 415 | /* End XCBuildConfiguration section */ 416 | 417 | /* Begin XCConfigurationList section */ 418 | 0D354FC32551BFBA009178D1 /* Build configuration list for PBXProject "{{ cookiecutter.formal_name }}" */ = { 419 | isa = XCConfigurationList; 420 | buildConfigurations = ( 421 | 0D354FDA2551BFBD009178D1 /* Debug */, 422 | 0D354FDB2551BFBD009178D1 /* Release */, 423 | ); 424 | defaultConfigurationIsVisible = 0; 425 | defaultConfigurationName = Release; 426 | }; 427 | 0D354FDC2551BFBD009178D1 /* Build configuration list for PBXNativeTarget "{{ cookiecutter.formal_name }}" */ = { 428 | isa = XCConfigurationList; 429 | buildConfigurations = ( 430 | 0D354FDD2551BFBD009178D1 /* Debug */, 431 | 0D354FDE2551BFBD009178D1 /* Release */, 432 | ); 433 | defaultConfigurationIsVisible = 0; 434 | defaultConfigurationName = Release; 435 | }; 436 | /* End XCConfigurationList section */ 437 | }; 438 | rootObject = 0D354FC02551BFBA009178D1 /* Project object */; 439 | } 440 | -------------------------------------------------------------------------------- /{{ cookiecutter.format }}/{{ cookiecutter.class_name }}/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // A main module for starting Python projects on macOS. 4 | // 5 | #import 6 | #import 7 | #import 8 | #import 9 | #include 10 | #include 11 | #include 12 | 13 | // A global indicator 14 | char *debug_mode; 15 | 16 | NSString * format_traceback(PyObject *, PyObject *, PyObject *); 17 | {% if cookiecutter.console_app %} 18 | void info_log(NSString *format, ...); 19 | void debug_log(NSString *format, ...); 20 | {% else %} 21 | #define info_log(...) NSLog(__VA_ARGS__) 22 | #define debug_log(...) if (debug_mode) NSLog(__VA_ARGS__) 23 | {% endif %} 24 | NSBundle *get_main_bundle(void); 25 | void setup_stdout(NSBundle *); 26 | void crash_dialog(NSString *); 27 | 28 | int main(int argc, char *argv[]) { 29 | int ret = 0; 30 | PyStatus status; 31 | PyPreConfig preconfig; 32 | PyConfig config; 33 | NSBundle *mainBundle; 34 | NSString *resourcePath; 35 | NSString *frameworksPath; 36 | NSString *python_tag; 37 | NSString *python_home; 38 | NSString *app_module_name; 39 | NSString *path; 40 | NSString *traceback_str; 41 | wchar_t *wtmp_str; 42 | wchar_t *app_packages_path_str; 43 | const char *app_module_str; 44 | PyObject *app_packages_path; 45 | PyObject *app_module; 46 | PyObject *module; 47 | PyObject *module_attr; 48 | PyObject *method_args; 49 | PyObject *result; 50 | PyObject *exc_type; 51 | PyObject *exc_value; 52 | PyObject *exc_traceback; 53 | PyObject *systemExit_code; 54 | 55 | @autoreleasepool { 56 | // Set the global debug state based on the runtime environment 57 | debug_mode = getenv("BRIEFCASE_DEBUG"); 58 | 59 | // Set the resource path for the app 60 | mainBundle = get_main_bundle(); 61 | resourcePath = [mainBundle resourcePath]; 62 | frameworksPath = [mainBundle privateFrameworksPath]; 63 | 64 | // Generate an isolated Python configuration. 65 | debug_log(@"Configuring isolated Python..."); 66 | PyPreConfig_InitIsolatedConfig(&preconfig); 67 | PyConfig_InitIsolatedConfig(&config); 68 | 69 | // Configure the Python interpreter: 70 | // Enforce UTF-8 encoding for stderr, stdout, file-system encoding and locale. 71 | // See https://docs.python.org/3/library/os.html#python-utf-8-mode. 72 | preconfig.utf8_mode = 1; 73 | // Ensure the locale is set (isolated interpreters won't by default) 74 | preconfig.configure_locale = 1; 75 | // Don't buffer stdio. We want output to appears in the log immediately 76 | config.buffered_stdio = 0; 77 | // Don't write bytecode; we can't modify the app bundle 78 | // after it has been signed. 79 | config.write_bytecode = 0; 80 | // Isolated apps need to set the full PYTHONPATH manually. 81 | config.module_search_paths_set = 1; 82 | // Enable verbose logging for debug purposes 83 | // config.verbose = 1; 84 | 85 | debug_log(@"Pre-initializing Python runtime..."); 86 | status = Py_PreInitialize(&preconfig); 87 | if (PyStatus_Exception(status)) { 88 | crash_dialog([NSString stringWithFormat:@"Unable to pre-initialize Python interpreter: %s", status.err_msg, nil]); 89 | PyConfig_Clear(&config); 90 | Py_ExitStatusException(status); 91 | } 92 | 93 | // Set the home for the Python interpreter 94 | python_tag = @"{{ cookiecutter.python_version|py_tag }}"; 95 | python_home = [NSString stringWithFormat:@"%@/Python.framework/Versions/%@", frameworksPath, python_tag, nil]; 96 | debug_log(@"PythonHome: %@", python_home); 97 | wtmp_str = Py_DecodeLocale([python_home UTF8String], NULL); 98 | status = PyConfig_SetString(&config, &config.home, wtmp_str); 99 | if (PyStatus_Exception(status)) { 100 | crash_dialog([NSString stringWithFormat:@"Unable to set PYTHONHOME: %s", status.err_msg, nil]); 101 | PyConfig_Clear(&config); 102 | Py_ExitStatusException(status); 103 | } 104 | PyMem_RawFree(wtmp_str); 105 | 106 | // Determine the app module name. Look for the BRIEFCASE_MAIN_MODULE 107 | // environment variable first; if that exists, we're probably in test 108 | // mode. If it doesn't exist, fall back to the MainModule key in the 109 | // main bundle. 110 | app_module_str = getenv("BRIEFCASE_MAIN_MODULE"); 111 | if (app_module_str) { 112 | app_module_name = [[NSString alloc] initWithUTF8String:app_module_str]; 113 | } else { 114 | app_module_name = [mainBundle objectForInfoDictionaryKey:@"MainModule"]; 115 | if (app_module_name == NULL) { 116 | debug_log(@"Unable to identify app module name."); 117 | } 118 | app_module_str = [app_module_name UTF8String]; 119 | } 120 | status = PyConfig_SetBytesString(&config, &config.run_module, app_module_str); 121 | if (PyStatus_Exception(status)) { 122 | crash_dialog([NSString stringWithFormat:@"Unable to set app module name: %s", status.err_msg, nil]); 123 | PyConfig_Clear(&config); 124 | Py_ExitStatusException(status); 125 | } 126 | 127 | // Read the site config 128 | status = PyConfig_Read(&config); 129 | if (PyStatus_Exception(status)) { 130 | crash_dialog([NSString stringWithFormat:@"Unable to read site config: %s", status.err_msg, nil]); 131 | PyConfig_Clear(&config); 132 | Py_ExitStatusException(status); 133 | } 134 | 135 | // Set the full module path. This includes the stdlib, site-packages, and app code. 136 | debug_log(@"PYTHONPATH:"); 137 | 138 | // The unpacked form of the stdlib 139 | path = [NSString stringWithFormat:@"%@/lib/python%@", python_home, python_tag, nil]; 140 | debug_log(@"- %@", path); 141 | wtmp_str = Py_DecodeLocale([path UTF8String], NULL); 142 | status = PyWideStringList_Append(&config.module_search_paths, wtmp_str); 143 | if (PyStatus_Exception(status)) { 144 | crash_dialog([NSString stringWithFormat:@"Unable to set unpacked form of stdlib path: %s", status.err_msg, nil]); 145 | PyConfig_Clear(&config); 146 | Py_ExitStatusException(status); 147 | } 148 | PyMem_RawFree(wtmp_str); 149 | 150 | // Add the stdlib binary modules path 151 | path = [NSString stringWithFormat:@"%@/lib/python%@/lib-dynload", python_home, python_tag, nil]; 152 | debug_log(@"- %@", path); 153 | wtmp_str = Py_DecodeLocale([path UTF8String], NULL); 154 | status = PyWideStringList_Append(&config.module_search_paths, wtmp_str); 155 | if (PyStatus_Exception(status)) { 156 | crash_dialog([NSString stringWithFormat:@"Unable to set stdlib binary module path: %s", status.err_msg, nil]); 157 | PyConfig_Clear(&config); 158 | Py_ExitStatusException(status); 159 | } 160 | PyMem_RawFree(wtmp_str); 161 | 162 | // Add the app path 163 | path = [NSString stringWithFormat:@"%@/app", resourcePath, nil]; 164 | debug_log(@"- %@", path); 165 | wtmp_str = Py_DecodeLocale([path UTF8String], NULL); 166 | status = PyWideStringList_Append(&config.module_search_paths, wtmp_str); 167 | if (PyStatus_Exception(status)) { 168 | crash_dialog([NSString stringWithFormat:@"Unable to set app path: %s", status.err_msg, nil]); 169 | PyConfig_Clear(&config); 170 | Py_ExitStatusException(status); 171 | } 172 | PyMem_RawFree(wtmp_str); 173 | 174 | debug_log(@"Configure argc/argv..."); 175 | status = PyConfig_SetBytesArgv(&config, argc, argv); 176 | if (PyStatus_Exception(status)) { 177 | crash_dialog([NSString stringWithFormat:@"Unable to configured argc/argv: %s", status.err_msg, nil]); 178 | PyConfig_Clear(&config); 179 | Py_ExitStatusException(status); 180 | } 181 | 182 | debug_log(@"Initializing Python runtime..."); 183 | status = Py_InitializeFromConfig(&config); 184 | if (PyStatus_Exception(status)) { 185 | crash_dialog([NSString stringWithFormat:@"Unable to initialize Python interpreter: %s", status.err_msg, nil]); 186 | PyConfig_Clear(&config); 187 | Py_ExitStatusException(status); 188 | } 189 | 190 | @try { 191 | // Set up an stdout/stderr handling that is required 192 | setup_stdout(mainBundle); 193 | 194 | 195 | // Adding the app_packages as site directory. 196 | // 197 | // This adds app_packages to sys.path and executes any .pth 198 | // files in that directory. 199 | path = [NSString stringWithFormat:@"%@/app_packages", resourcePath, nil]; 200 | app_packages_path_str = Py_DecodeLocale([path UTF8String], NULL); 201 | 202 | debug_log(@"Adding app_packages as site directory: %@", path); 203 | 204 | module = PyImport_ImportModule("site"); 205 | if (module == NULL) { 206 | crash_dialog(@"Could not import site module"); 207 | exit(-11); 208 | } 209 | 210 | module_attr = PyObject_GetAttrString(module, "addsitedir"); 211 | if (module_attr == NULL || !PyCallable_Check(module_attr)) { 212 | crash_dialog(@"Could not access site.addsitedir"); 213 | exit(-12); 214 | } 215 | 216 | app_packages_path = PyUnicode_FromWideChar(app_packages_path_str, wcslen(app_packages_path_str)); 217 | if (app_packages_path == NULL) { 218 | crash_dialog(@"Could not convert app_packages path to unicode"); 219 | exit(-13); 220 | } 221 | PyMem_RawFree(app_packages_path_str); 222 | 223 | method_args = Py_BuildValue("(O)", app_packages_path); 224 | if (method_args == NULL) { 225 | crash_dialog(@"Could not create arguments for site.addsitedir"); 226 | exit(-14); 227 | } 228 | 229 | result = PyObject_CallObject(module_attr, method_args); 230 | if (result == NULL) { 231 | crash_dialog(@"Could not add app_packages directory using site.addsitedir"); 232 | exit(-15); 233 | } 234 | 235 | 236 | // Start the app module. 237 | // 238 | // From here to Py_ObjectCall(runmodule...) is effectively 239 | // a copy of Py_RunMain() (and, more specifically, the 240 | // pymain_run_module() method); we need to re-implement it 241 | // because we need to be able to inspect the error state of 242 | // the interpreter, not just the return code of the module. 243 | debug_log(@"Running app module: %@", app_module_name); 244 | module = PyImport_ImportModule("runpy"); 245 | if (module == NULL) { 246 | crash_dialog(@"Could not import runpy module"); 247 | exit(-2); 248 | } 249 | 250 | module_attr = PyObject_GetAttrString(module, "_run_module_as_main"); 251 | if (module_attr == NULL) { 252 | crash_dialog(@"Could not access runpy._run_module_as_main"); 253 | exit(-3); 254 | } 255 | 256 | app_module = PyUnicode_FromString(app_module_str); 257 | if (app_module == NULL) { 258 | crash_dialog(@"Could not convert module name to unicode"); 259 | exit(-3); 260 | } 261 | 262 | method_args = Py_BuildValue("(Oi)", app_module, 0); 263 | if (method_args == NULL) { 264 | crash_dialog(@"Could not create arguments for runpy._run_module_as_main"); 265 | exit(-4); 266 | } 267 | 268 | // Print a separator to differentiate Python startup logs from app logs 269 | debug_log(@"---------------------------------------------------------------------------"); 270 | 271 | // Invoke the app module 272 | result = PyObject_Call(module_attr, method_args, NULL); 273 | 274 | if (result == NULL) { 275 | // Retrieve the current error state of the interpreter. 276 | PyErr_Fetch(&exc_type, &exc_value, &exc_traceback); 277 | PyErr_NormalizeException(&exc_type, &exc_value, &exc_traceback); 278 | 279 | if (exc_traceback == NULL) { 280 | crash_dialog(@"Could not retrieve traceback"); 281 | exit(-5); 282 | } 283 | 284 | traceback_str = NULL; 285 | if (PyErr_GivenExceptionMatches(exc_value, PyExc_SystemExit)) { 286 | systemExit_code = PyObject_GetAttrString(exc_value, "code"); 287 | if (systemExit_code == NULL) { 288 | traceback_str = @"Could not determine exit code"; 289 | ret = -10; 290 | } else if (systemExit_code == Py_None) { 291 | // SystemExit with a code of None; documented as a 292 | // return code of 0. 293 | ret = 0; 294 | } else if (PyLong_Check(systemExit_code)) { 295 | // SystemExit with error code 296 | ret = (int) PyLong_AsLong(systemExit_code); 297 | } else { 298 | // Any other SystemExit value - convert to a string, and 299 | // use the string as the traceback, and use the 300 | // documented SystemExit return value of 1. 301 | ret = 1; 302 | traceback_str = [NSString stringWithUTF8String:PyUnicode_AsUTF8(PyObject_Str(systemExit_code))]; 303 | } 304 | } else { 305 | // Non-SystemExit; likely an uncaught exception 306 | info_log(@"---------------------------------------------------------------------------"); 307 | info_log(@"Application quit abnormally!"); 308 | ret = -6; 309 | traceback_str = format_traceback(exc_type, exc_value, exc_traceback); 310 | } 311 | 312 | if (traceback_str != NULL) { 313 | // Display stack trace in the crash dialog. 314 | crash_dialog(traceback_str); 315 | } 316 | } 317 | } 318 | @catch (NSException *exception) { 319 | crash_dialog([NSString stringWithFormat:@"Python runtime error: %@", [exception reason]]); 320 | ret = -7; 321 | } 322 | @finally { 323 | Py_Finalize(); 324 | } 325 | } 326 | 327 | exit(ret); 328 | return ret; 329 | } 330 | 331 | /** 332 | * Convert a Python traceback object into a user-suitable string, stripping off 333 | * stack context that comes from this stub binary. 334 | * 335 | * If any error occurs processing the traceback, the error message returned 336 | * will describe the mode of failure. 337 | */ 338 | NSString *format_traceback(PyObject *type, PyObject *value, PyObject *traceback) { 339 | NSRegularExpression *regex; 340 | NSString *traceback_str; 341 | PyObject *traceback_list; 342 | PyObject *traceback_module; 343 | PyObject *format_exception; 344 | PyObject *traceback_unicode; 345 | PyObject *inner_traceback; 346 | 347 | // Drop the top two stack frames; these are internal 348 | // wrapper logic, and not in the control of the user. 349 | for (int i = 0; i < 2; i++) { 350 | inner_traceback = PyObject_GetAttrString(traceback, "tb_next"); 351 | if (inner_traceback != NULL) { 352 | traceback = inner_traceback; 353 | } 354 | } 355 | 356 | // Format the traceback. 357 | traceback_module = PyImport_ImportModule("traceback"); 358 | if (traceback_module == NULL) { 359 | return @"Could not import traceback"; 360 | } 361 | 362 | format_exception = PyObject_GetAttrString(traceback_module, "format_exception"); 363 | if (format_exception && PyCallable_Check(format_exception)) { 364 | traceback_list = PyObject_CallFunctionObjArgs(format_exception, type, value, traceback, NULL); 365 | } else { 366 | return @"Could not find 'format_exception' in 'traceback' module"; 367 | } 368 | if (traceback_list == NULL) { 369 | return @"Could not format traceback"; 370 | } 371 | 372 | traceback_unicode = PyUnicode_Join(PyUnicode_FromString(""), traceback_list); 373 | traceback_str = [NSString stringWithUTF8String:PyUnicode_AsUTF8(PyObject_Str(traceback_unicode))]; 374 | 375 | // Take the opportunity to clean up the source path, 376 | // so paths only refer to the "app local" path. 377 | regex = [NSRegularExpression regularExpressionWithPattern:@"^ File \"/.*/(.*?).app/Contents/Resources/" 378 | options:NSRegularExpressionAnchorsMatchLines 379 | error:nil]; 380 | traceback_str = [regex stringByReplacingMatchesInString:traceback_str 381 | options:0 382 | range:NSMakeRange(0, [traceback_str length]) 383 | withTemplate:@" File \"$1.app/Contents/Resources/"]; 384 | return traceback_str; 385 | } 386 | 387 | {% if cookiecutter.console_app %} 388 | void info_log(NSString *format, ...) { 389 | va_list args; 390 | va_start(args, format); 391 | printf("%s\n", [[[NSString alloc] initWithFormat:format arguments:args] UTF8String]); 392 | va_end(args); 393 | } 394 | 395 | void debug_log(NSString *format, ...) { 396 | if (debug_mode) { 397 | va_list args; 398 | va_start(args, format); 399 | printf("%s\n", [[[NSString alloc] initWithFormat:format arguments:args] UTF8String]); 400 | va_end(args); 401 | } 402 | } 403 | 404 | /**************************************************************************** 405 | * In a normal macOS app, [NSBundle mainBundle] works as expected. However, 406 | * the path it generates is based on sys.argv[0], which won't be the same if 407 | * you symlink to the binary to expose a command line app. Instead, use 408 | * _NSGetExecutablePath to get the binary path, then construct the bundle 409 | * path based on the known file structure of the app bundle. 410 | ****************************************************************************/ 411 | NSBundle* get_main_bundle(void) { 412 | uint32_t path_max = PATH_MAX; 413 | char binary_path[PATH_MAX]; 414 | char resolved_binary_path[PATH_MAX]; 415 | char *bundle_path; 416 | NSBundle *mainBundle; 417 | 418 | _NSGetExecutablePath(binary_path, &path_max); 419 | realpath(binary_path, resolved_binary_path); 420 | debug_log(@"Binary: %s", resolved_binary_path); 421 | bundle_path = dirname(dirname(dirname(resolved_binary_path))); 422 | mainBundle = [NSBundle bundleWithPath:[NSString stringWithCString:bundle_path encoding:NSUTF8StringEncoding]]; 423 | debug_log(@"App Bundle: %@", mainBundle); 424 | 425 | return mainBundle; 426 | } 427 | 428 | void setup_stdout(NSBundle *mainBundle) { 429 | } 430 | 431 | void crash_dialog(NSString *details) { 432 | info_log(details); 433 | } 434 | 435 | {% else %} 436 | 437 | NSBundle* get_main_bundle(void) { 438 | return [NSBundle mainBundle]; 439 | } 440 | 441 | void setup_stdout(NSBundle *mainBundle) { 442 | int ret = 0; 443 | const char *nslog_script; 444 | 445 | // If the app is running under Xcode 15 or later, we don't need to do anything, 446 | // as stdout and stderr are automatically captured by the in-IDE console. 447 | // See https://developer.apple.com/forums/thread/705868 for details. 448 | if (getenv("IDE_DISABLED_OS_ACTIVITY_DT_MODE")) { 449 | return; 450 | } 451 | 452 | // Install the nslog script to redirect stdout/stderr if available. 453 | // Set the name of the python NSLog bootstrap script 454 | nslog_script = [ 455 | [mainBundle pathForResource:@"app_packages/nslog" 456 | ofType:@"py"] cStringUsingEncoding:NSUTF8StringEncoding]; 457 | 458 | if (nslog_script == NULL) { 459 | info_log(@"No Python NSLog handler found. stdout/stderr will not be captured."); 460 | info_log(@"To capture stdout/stderr, add 'std-nslog' to your app dependencies."); 461 | } else { 462 | debug_log(@"Installing Python NSLog handler..."); 463 | FILE *fd = fopen(nslog_script, "r"); 464 | if (fd == NULL) { 465 | crash_dialog(@"Unable to open nslog.py"); 466 | exit(-1); 467 | } 468 | 469 | ret = PyRun_SimpleFileEx(fd, nslog_script, 1); 470 | fclose(fd); 471 | if (ret != 0) { 472 | crash_dialog(@"Unable to install Python NSLog handler"); 473 | exit(ret); 474 | } 475 | } 476 | } 477 | 478 | /** 479 | * Construct and display a modal dialog to the user that contains 480 | * details of an error during application execution (usually a traceback). 481 | */ 482 | void crash_dialog(NSString *details) { 483 | // Write the error to the log 484 | NSArray *lines = [details componentsSeparatedByString:@"\n"]; 485 | for (int i = 0; i < [lines count]; i++) { 486 | NSLog(@"%@", lines[i]); 487 | } 488 | 489 | // If there's an app module override, we're running in test mode; don't show error dialogs 490 | if (getenv("BRIEFCASE_MAIN_MODULE")) { 491 | return; 492 | } 493 | 494 | // Obtain the app instance (starting it if necessary) so that we can show an error dialog 495 | NSApplication *app = [NSApplication sharedApplication]; 496 | [app setActivationPolicy:NSApplicationActivationPolicyRegular]; 497 | 498 | // Create a stack trace dialog 499 | NSAlert *alert = [[NSAlert alloc] init]; 500 | [alert setAlertStyle:NSAlertStyleCritical]; 501 | [alert setMessageText:@"Application has crashed"]; 502 | [alert setInformativeText:@"An unexpected error occurred. Please see the traceback below for more information."]; 503 | 504 | // A multiline text widget in a scroll view to contain the stack trace 505 | NSScrollView *scroll_panel = [[NSScrollView alloc] initWithFrame:NSMakeRect(0, 0, 600, 300)]; 506 | [scroll_panel setHasVerticalScroller:true]; 507 | [scroll_panel setHasHorizontalScroller:false]; 508 | [scroll_panel setAutohidesScrollers:false]; 509 | [scroll_panel setBorderType:NSBezelBorder]; 510 | 511 | NSTextView *crash_text = [[NSTextView alloc] init]; 512 | [crash_text setEditable:false]; 513 | [crash_text setSelectable:true]; 514 | [crash_text setString:details]; 515 | [crash_text setVerticallyResizable:true]; 516 | [crash_text setHorizontallyResizable:true]; 517 | [crash_text setFont:[NSFont fontWithName:@"Menlo" size:12.0]]; 518 | 519 | [scroll_panel setDocumentView:crash_text]; 520 | [alert setAccessoryView:scroll_panel]; 521 | 522 | // Show the crash dialog 523 | [alert runModal]; 524 | } 525 | 526 | {% endif %} 527 | --------------------------------------------------------------------------------