├── 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 |
--------------------------------------------------------------------------------