Avalonia UI
首页支持GitHub 仓库English Doc
  • 👋欢迎
  • 文档
    • ⚡快速开始
      • IDE 支持
        • JetBrains Rider 设置
      • 使用 Avalonia 开发
        • Model-View-ViewModel 模式(MVVM)
        • 控件和布局
        • 数据绑定
        • 图像和动画
      • Windows
      • UserControls
      • 资产
      • 开发者工具
      • 错误和警告日志
      • 未处理的异常
      • 应用生命周期
    • 🔁数据绑定
      • 数据上下文
      • 变化通知
      • 绑定
      • 编译绑定
      • 与控件绑定
      • 转换绑定值
      • 绑定到命令
      • 绑定到任务和可观察对象
      • 使用代码进行绑定
      • 在控件模板中实现绑定
      • 绑定Classes
      • 创建和绑定到附加属性
      • Data Validation
    • 🎨样式
      • 样式
      • 选择器
      • 资源
      • 疑难解答
    • 🧰控件
      • AutoCompleteBox
      • Border
      • Buttons
        • Button
        • RepeatButton
        • RadioButton
        • ToggleButton
        • ButtonSpinner
        • SplitButton
        • ToggleSplitButton
      • Calendar
      • Canvas
      • Carousel
      • CheckBox
      • ComboBox
      • ContentControl
      • ContextMenu
      • Decorator
      • DataGrid
        • DataGridColumns
      • DatePicker
      • DockPanel
      • Expander
      • Flyouts
      • Grid
      • GridSplitter
      • Image
      • ItemsControl
      • ItemsRepeater
      • LayoutTransformControl
      • ListBox
      • MaskedTextBox
      • Menu
      • NativeMenu
      • NumericUpDown
      • Panel
      • ProgressBar
      • RelativePanel
      • ScrollBar
      • ScrollViewer
      • Separator
      • Slider
      • SplitView
      • StackPanel
      • TabControl
      • TabStrip
      • TextBlock
      • TrayIcon
      • TreeDataGrid
        • Creating a Hierarchical TreeDataGrid
        • Creating a Flat TreeDataGrid
        • TreeDataGrid column types
      • TimePicker
      • TextBox
      • ToolTip
      • TreeView
      • TransitioningContentControl
      • UserControl
      • Viewbox
      • Window
      • WrapPanel
    • 📚模板
      • 数据模板
      • 在代码中创建数据模板
      • 实现 IDataTemplate 接口
    • ✏️自定义控件
      • 控件类别
      • 定义属性
    • 🖱️输入
      • 路由事件
      • 剪贴板
      • 鼠标与触控设备
      • 快捷键
    • 🔑动画
      • 关键帧动画
      • 过渡
      • 页面过渡
    • 📐布局
      • 面板概述
      • Alignment、Margin 和 Padding
      • 创建自定义面板
    • 📦发布/分发
      • macOS
  • API 参考
    • 🗒️命名空间
      • Avalonia
      • Avalonia.Animation
        • Avalonia.Animation.Easings
        • Avalonia.Animation.Animators
      • Avalonia.Collections
      • Avalonia.Controls
      • Avalonia.Data
        • Avalonia.Data.Core.Plugins
        • Avalonia.Data.Core
        • Avalonia.Data.Converters
      • Avalonia.Diagnostics
      • Avalonia.Dialogs
  • 指南
    • 🐣基础
      • XAML 介绍
      • Code-behind
      • MVVM 架构
      • 在UI线程上操作
    • 🤿深入
      • 在树莓派上运行你的应用
      • 在树莓派上运行你的应用(使用Raspbian Lite)
      • ReactiveUI
        • 视图激活机制
        • 路由
        • 数据持久化
        • 绑定到 Sorted/Filtered 数据
    • 👩‍💻👩💻 开发人员指南
      • 🏭从源代码中构建 Avalonia
      • Avalonia 与 WPF 和 UWP 之间的比较
      • Debugging Previewer
      • Debugging the XAML compiler
      • macOS 开发
      • Release Process
      • 维护稳定的分支
  • 教程
    • 📋待办事项应用
    • 📻音乐商店应用
    • 🕸️在浏览器中运行
    • 📱为移动设备开发
  • 杂项
    • 👪社区
    • 🖥️WPF 开发者建议
    • 📋正在使用 Avalonia 的项目
    • ❔常见问题
由 GitBook 提供支持
在本页
  • Making the application bundle
  • Notes on the .app executable file
  • dotnet-bundle
  • Manual
  • Signing Your App
  • Running codesign and enabling hardened runtime
  • Notarizing your software
  • App Store Packaging
  • Getting certificates
  • Sandbox and bundle
  • Sandbox entitlements and signing
  • Packaging script
  • Testing a package
  • Uploading a package to app store
  • Troubleshooting
  • App menu shows About Avalonia menu item
  • Application name in menu bar does not match
  • Packaging in GitHub Actions workflow

这有帮助吗?

在GitHub上编辑
  1. 文档
  2. 发布/分发

macOS

上一页发布/分发下一页命名空间

最后更新于2年前

这有帮助吗?

macOS applications are typically distributed in a .app . To make .NET Core and Avalonia projects work in a .app bundle, some extra legwork has to be done after your application has gone through the publishing process.

With Avalonia, you'll have a .app folder structure that looks like this:

MyProgram.app
|
----Contents\
    |
    ------_CodeSignature\ (stores code signing information)
    |     |
    |     ------CodeResources
    |
    ------MacOS\ (all your DLL files, etc. -- the output of `dotnet publish`)
    |     |
    |     ---MyProgram
    |     |
    |     ---MyProgram.dll
    |     |
    |     ---Avalonia.dll
    |
    ------Resources\
    |     |
    |     -----MyProgramIcon.icns (icon file)
    |
    ------Info.plist (stores information on your bundle identifier, version, etc.)
    ------embedded.provisionprofile (file with signing information)

Making the application bundle

There are a few options available for creating the .app file/folder structure. You can do this on any operating system, since a .app file is just a set of folders laid out in a specific format and the tooling isn't specific to one operating system. However, if you build on Windows outside of WSL, the executable may not have the right attributes for execution on macOS -- you may have to run chmod +x on the published binary output (the output generated by dotnet publish) from a Unix machine. This is the binary output that ends up in the folder MyApp.app/Contents/MacOS/, and the name should match CFBundleExecutable.

The .app structure relies on the Info.plist file being properly formatted and containing the right information. Use Xcode to edit Info.plist, it has auto-completion for all properties. Make sure that:

If you need a protocol registration or file associations - open plist files from other apps in Applications folder and check out their fields.

Example protocol:

  <key>CFBundleURLTypes</key>
  <array>
    <dict>
      <key>CFBundleURLName</key>
      <string>AppName</string>
      <key>CFBundleTypeRole</key>
      <string>Viewer</string>
      <key>CFBundleURLSchemes</key>
      <array>
        <string>i8-AppName</string>
      </array>
    </dict>
  </array>

Example file association

  <key>CFBundleDocumentTypes</key>
  <array>
    <dict>
      <key>CFBundleTypeName</key>
      <string>Sketch</string>
      <key>CFBundleTypeExtensions</key>
      <array>
        <string>sketch</string>
      </array>
      <key>CFBundleTypeIconFile</key>
      <string>icon.icns</string>
      <key>CFBundleTypeRole</key>
      <string>Viewer</string>
      <key>LSHandlerRank</key>
      <string>Default</string>
    </dict>
  </array>
<RuntimeIdentifiers>osx-x64</RuntimeIdentifiers>

Add other runtime identifiers as necessary. Each one should be separated by a semicolon (;).

Notes on the .app executable file

The file that is actually executed by macOS when starting your .app bundle will not have the standard .dll extension. If your publish folder contents, which go inside the .app bundle, do not have both a MyApp (executable) and a MyApp.dll, things are probably not generating properly, and macOS will probably not be able to start your .app properly.

  • Add the following to your .csproj file:

<PropertyGroup>
  <UseAppHost>true</UseAppHost>
</PropertyGroup>
  • Add -p:UseAppHost=true to your dotnet publish command.

dotnet-bundle

You'll first have to add the project as a PackageReference in your project. Add it to your project via NuGet package manager or by adding the following line to your .csproj file:

<PackageReference Include="Dotnet.Bundle" Version="*" />

After that, you can create your .app by executing the following on the command line:

dotnet restore -r osx-x64
dotnet msbuild -t:BundleApp -p:RuntimeIdentifier=osx-x64 -p:UseAppHost=true

You can specify other parameters for the dotnet msbuild command. For instance, if you want to publish in release mode:

dotnet msbuild -t:BundleApp -p:RuntimeIdentifier=osx-x64 -property:Configuration=Release -p:UseAppHost=true

or if you want to specify a different app name:

dotnet msbuild -t:BundleApp -p:RuntimeIdentifier=osx-x64 -p:CFBundleDisplayName=MyBestThingEver -p:UseAppHost=true

Instead of specifying CFBundleDisplayName, etc., on the command line, you can also specify them in your project file:

<PropertyGroup>
    <CFBundleName>AppName</CFBundleName> <!-- Also defines .app file name -->
    <CFBundleDisplayName>MyBestThingEver</CFBundleDisplayName>
    <CFBundleIdentifier>com.example</CFBundleIdentifier>
    <CFBundleVersion>1.0.0</CFBundleVersion>
    <CFBundlePackageType>APPL</CFBundlePackageType>
    <CFBundleSignature>????</CFBundleSignature>
    <CFBundleExecutable>AppName</CFBundleExecutable>
    <CFBundleIconFile>AppName.icns</CFBundleIconFile> <!-- Will be copied from output directory -->
    <NSPrincipalClass>NSApplication</NSPrincipalClass>
    <NSHighResolutionCapable>true</NSHighResolutionCapable>
</PropertyGroup>

By default, dotnet-bundle will put the .app file in the same place as the publish output: [project directory]/bin/{Configuration}/netcoreapp3.1/osx-x64/publish/MyBestThingEver.app.

If you created the .app on Windows, make sure to run chmod +x MyApp.app/Contents/MacOS/AppName from a Unix machine. Otherwise, the app will not start on macOS.

Manual

dotnet publish -r osx-x64 --configuration Release -p:UseAppHost=true

Create your Info.plist file, adding or modifying keys as necessary:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>CFBundleIconFile</key>
    <string>myicon-logo.icns</string>
    <key>CFBundleIdentifier</key>
    <string>com.identifier</string>
    <key>CFBundleName</key>
    <string>MyApp</string>
    <key>CFBundleVersion</key>
    <string>1.0.0</string>
    <key>LSMinimumSystemVersion</key>
    <string>10.12</string>
    <key>CFBundleExecutable</key>
    <string>MyApp.Avalonia</string>
    <key>CFBundleInfoDictionaryVersion</key>
    <string>6.0</string>
    <key>CFBundlePackageType</key>
    <string>APPL</string>
    <key>CFBundleShortVersionString</key>
    <string>1.0</string>
    <key>NSHighResolutionCapable</key>
    <true/>
</dict>
</plist>

You can then create your .app folder structure as outlined at the top of this page. If you want a script to do it for you, you can use something like this (macOS/Unix):

#!/bin/bash
APP_NAME="/path/to/your/output/MyApp.app"
PUBLISH_OUTPUT_DIRECTORY="/path/to/your/publish/output/netcoreapp3.1/osx-64/publish/."
# PUBLISH_OUTPUT_DIRECTORY should point to the output directory of your dotnet publish command.
# One example is /path/to/your/csproj/bin/Release/netcoreapp3.1/osx-x64/publish/.
# If you want to change output directories, add `--output /my/directory/path` to your `dotnet publish` command.
INFO_PLIST="/path/to/your/Info.plist"
ICON_FILE="/path/to/your/myapp-logo.icns"

if [ -d "$APP_NAME" ]
then
    rm -rf "$APP_NAME"
fi

mkdir "$APP_NAME"

mkdir "$APP_NAME/Contents"
mkdir "$APP_NAME/Contents/MacOS"
mkdir "$APP_NAME/Contents/Resources"

cp "$INFO_PLIST" "$APP_NAME/Contents/Info.plist"
cp "$ICON_FILE" "$APP_NAME/Contents/Resources/$ICON_FILE"
cp -a "$PUBLISH_OUTPUT_DIRECTORY" "$APP_NAME/Contents/MacOS"

If you created the .app on Windows, make sure to run chmod +x MyApp.app/Contents/MacOS/AppName from a Unix machine. Otherwise, the app will not start on macOS.

Signing Your App

You'll need a Mac computer for this step, unfortunately, as we have to run the codesign command line tool that comes with Xcode.

Running codesign and enabling hardened runtime

You'll also need to have the Xcode command line tools installed. You can get those by installing Xcode and running it or by running xcode-select --install on the command line and following the prompts to install the tools

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>com.apple.security.cs.allow-jit</key>
    <true/>
    <key>com.apple.security.automation.apple-events</key>
    <true/>
</dict>
</plist>

Then, run this script to do all the code signing for you:

#!/bin/bash
APP_NAME="/path/to/your/output/MyApp.app"
ENTITLEMENTS="/path/to/your/MyAppEntitlements.entitlements"
SIGNING_IDENTITY="Developer ID: MyCompanyName" # matches Keychain Access certificate name

find "$APP_NAME/Contents/MacOS/"|while read fname; do
    if [[ -f $fname ]]; then
        echo "[INFO] Signing $fname"
        codesign --force --timestamp --options=runtime --entitlements "$ENTITLEMENTS" --sign "$SIGNING_IDENTITY" "$fname"
    fi
done

echo "[INFO] Signing app file"

codesign --force --timestamp --options=runtime --entitlements "$ENTITLEMENTS" --sign "$SIGNING_IDENTITY" "$APP_NAME"

Once your app is code signed, you can verify that it signed properly by making sure that the following command outputs no errors:

codesign --verify --verbose /path/to/MyApp.app

Notarizing your software

  1. Make sure your .app is code signed properly

  2. Stick your .app in a .zip file, e.g. MyApp.zip. Note that using zip will make notarization fail, instead use ditto like so: ditto -c -k --sequesterRsrc --keepParent MyApp.app MyApp.zip

  3. Run xcrun altool --notarize-app -f MyApp.zip --primary-bundle-id com.unique-identifier-for-this-upload -u username -p password. You can use a password in your keychain by passing -p "@keychain:AC_PASSWORD", where AC_PASSWORD is the key. The account has to be registered as an Apple Developer.

  4. If the upload is successful, you'll get a UUID back for your request token like this: 28fad4c5-68b3-4dbf-a0d4-fbde8e6a078f

  5. You can check notarization status using that token like this: xcrun altool --notarization-info 28fad4c5-68b3-4dbf-a0d4-fbde8e6a078f -u username -p password. This could take some time -- eventually it will succeed or fail.

  6. If it succeeds, you have to staple the notarization to the app: xcrun stapler staple MyApp.app. You can validate this by running xcrun stapler validate MyApp.app.

Once notarization is complete, you should be able to distribute your application!

If you distribute your app in a .dmg, you will want to modify the steps slightly:

  1. Notarize your .app as normal (in a .zip file)

  2. Add your notarized and stapled (xcrun stapler) app into the DMG (the DMG now has the notarized/stapled .app file inside of it).

  3. Notarize your .dmg file (same basic xcrun altool command, just with the .dmg file for the -f flag instead of the .zip)

  4. Staple the notarization to the .dmg file: xcrun stapler staple MyApp.dmg

App Store Packaging

You need a lot of things:

  • Apple Developer Account, with your Apple ID connected to it.

  • Transporter app installed from App Store.

  • Latest Xcode installed with your Apple ID authorized into it.

  • Two certificates: 3rd Party Mac Developer Installer for signing .pkg file and 3rd Party Mac Developer Application for signing a bundle.

  • Two entitlements: One for signing .app and other for signing app helpers.

  • Your bundle is signed correctly.

  • Your .dylib files doesn't contain any non-ARM/x64 architectures. You can remove these by using lipo command line tool.

Getting certificates

  • go to Xcode > Preferences > Account > Manage Certificates...

  • Add them if they do not exists.

  • Export them with a password.

  • Open them and import into KeyChain Access.

  • In KeyChain you should see this certificates 3rd Party Mac Developer Installer and Apple Distribution. If cert names are started with another strings - you've created a wrong certificate. Try again.

  • Expand imported keys in KeyChain and double click on a private key inside.

  • Go to Access Control Tab.

  • Select Allow all applications to access this item in case you don't want to enter a Mac profile password for every file sign.

Sandbox and bundle

App Store required app to be launched inside a sandbox. That means app will have no access to everything and cannot harm user's PC.

Your app should be ready for this and do not crash if any folder is read/write protected.

.NET 6 apps will not crash inside a sandbox only if it's published with single file option enabled. Example:

dotnet publish src/MyApp.csproj -c Release -f net6.0 -r osx-x64 --self-contained true -p:PublishSingleFile=true

Most important rules from the article:

  • .dll files are not considered as a code by Apple. So it should be placed inside /Resources folder and can be not signed.

  • /MacOS files should contain only executable mach-o - you app executable and any other helper executables

  • All other mach-o .dylib files should be inside Frameworks/ folder.

To satisfy this requirement without a lot of pain you can use relative symlinks from MacOS/ folder to Resources/ and Frameworks/ folders. As an example:

ln -s fromFile toFile

Also it's better to rewrite your app's resources access scheme to directly access Resources/ folder without using any symlinks, because over symlinks you might get I/O access issues in sandbox.

Sandbox entitlements and signing

First for the entitlements file is to sign all helper executables inside .app/Content/MacOS/ folder. It should look like this.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>com.apple.security.app-sandbox</key>
    <true/>
    <key>com.apple.security.inherit</key>
    <true/>
</dict>
</plist>

Second is to sign app executable and a whole app bundle. It should contain all app's permissions. Here is an example:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>com.apple.security.cs.allow-jit</key>
    <true/>
    <key>com.apple.security.cs.allow-unsigned-executable-memory</key>
    <true/>
    <key>com.apple.security.cs.disable-library-validation</key>
    <true/>
    <key>com.apple.security.cs.allow-dyld-environment-variables</key>
    <true/>
    <key>com.apple.security.app-sandbox</key>
    <true/>
    <key>com.apple.security.temporary-exception.mach-lookup.global-name</key>
    <array>
        <string>com.apple.coreservices.launchservicesd</string>
    </array>
</dict>
</plist>

Also here is some optional parameters your app can need:

    <key>com.apple.security.network.client</key>
    <true/>
    <key>com.apple.security.network.server</key>
    <true/>
    <key>com.apple.security.automation.apple-events</key>
    <true/>
    <key>com.apple.security.files.user-selected.read-write</key>
    <true/>
    <key>com.apple.security.files.bookmarks.document-scope</key>
    <true/>
    <key>com.apple.security.application-groups</key>
    <array>
      <string>[Your Team ID].[Your App ID]</string>
    </array>

Packaging script

Here is example packaging script with comments

#cleanup folders
rm -rf "App/AppName.app/Contents/MacOS/" 
rm -rf "App/AppName.app/Contents/CodeResources" 
rm -rf "App/AppName.app/Contents/_CodeSignature" 
rm -rf "App/AppName.app/Contents/embedded.provisionprofile" 
mkdir -p "App/AppName.app/Contents/Frameworks/"
mkdir -p "App/AppName.app/Contents/MacOS/"

#Build app
dotnet publish ../../ProjectFolder/AppName.csproj -c release -f net5.0 -r osx-x64 --self-contained true -p:PublishSingleFile=true

#Move app
cd ..
cd ..
cp -R -f ProjectFolder/bin/release/net5.0/osx-x64/publish/* "build/osx/App/AppName.app/Contents/MacOS/"
cd "build/osx/"

APP_ENTITLEMENTS="AppEntitlements.entitlements"
APP_SIGNING_IDENTITY="3rd Party Mac Developer Application: [***]"
INSTALLER_SIGNING_IDENTITY="3rd Party Mac Developer Installer: [***]"
APP_NAME="App/AppName.app"

#<here is moving your app resources to Resources folder using relative symlinks>

#<here is moving your .dylib files to Frameworks folder using relative symlinks>

echo "[INFO] Switch provisionprofile to AppStore"
\cp -R -f AppNameAppStore.provisionprofile "App/AppName.app/Contents/embedded.provisionprofile"

echo "[INFO] Fix libuv.dylib architectures"
lipo -remove i386 "App/AppName.app/Contents/Frameworks/libuv.dylib" "App/AppName.app/Contents/Frameworks/libuv.dylib"

find "$APP_NAME/Contents/Frameworks/"|while read fname; do
    if [[ -f $fname ]]; then
        echo "[INFO] Signing $fname"
        codesign --force --sign "$APP_SIGNING_IDENTITY" "$fname"
    fi
done

echo "[INFO] Signing app executable"
codesign --force --entitlements "$FILE_ENTITLEMENTS" --sign "$APP_SIGNING_IDENTITY" "App/AppName.app/Contents/MacOS/AppName"

echo "[INFO] Signing app bundle"
codesign --force --entitlements "$APP_ENTITLEMENTS" --sign "$APP_SIGNING_IDENTITY" "$APP_NAME"

echo "[INFO] Creating AppName.pkg"
productbuild --component App/AppName.app /Applications --sign "$INSTALLER_SIGNING_IDENTITY" AppName.pkg

Testing a package

Copy your .app into Applications folder and launch it. If it launches correctly - you did everything right. If it crashes - open Console app and check a crash report.

Uploading a package to app store

Open a Transporter app, sign in, select your *.pkg package and wait for validation and uploading to app store.

If you will receive any errors - fix them, package app again, remove file in Transporter and select it again.

When upload succeeds - you will see your package in App Store Connect.

Troubleshooting

App menu shows About Avalonia menu item

This means that your application most likely does not specify a menu. On startup, Avalonia creates the default menu items for an application and automatically adds the About Avalonia item when no menu has been configured. This can be resolved by adding one to your App.xaml:

<Application xmlns="https://github.com/avaloniaui"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="using:RoadCaptain.App.RouteBuilder"
             x:Class="RoadCaptain.App.RouteBuilder.App">
	<NativeMenu.Menu>
		<NativeMenu>
			<NativeMenuItem Header="About MyApp" Click="AboutMenuItem_OnClick" />
		</NativeMenu>
	</NativeMenu.Menu>
</Application>

The rest of the macOS default menu items will still be generated by Avalonia.

Application name in menu bar does not match

When you run an app from a bundle the name for the app that is shown in the menu bar is taken from the Info.plist in the bundle instead of the Name property in App.xaml.

If the names do not match, verify that the values for CFBundleName, CFBundleDisplayName and the Name property are the same.

Note that CFBundleName is limited to 15 characters, if your application name is longer you must set CFBundleDisplayName.

Packaging in GitHub Actions workflow

Building the app in a CI/CD pipeline is straightforward using the dotnet command. For code signing and notarization to work a little extra work is required.

codesign and notarytool read the certificate and credentials to talk to the notarization service from a Keychain on the build machine:

# Create a new keychain
security create-keychain -p "${{ secrets.KEYCHAIN_PASSWORD}}" build.keychain
# Set it as the default keychain
security default-keychain -s build.keychain
# Unlock the keychain so it can be used without an authorisation prompt
security unlock-keychain -p "${{ secrets.KEYCHAIN_PASSWORD}}" build.keychain

KEYCHAIN_PASSWORD is a password you generate specifically for this keychain. It can be generated on each build or a password you use for every build.

Next, the certificate for signing needs to be imported into the keychain. Because GitHub secrets only support strings, the certificate .p12 file needs to be stored in base64 encoded form. In the pipeline the string is decoded to a file and added to the keychain:

# Decode certificate to file
echo "${{ secrets.MACOS_CERTIFICATE }}" | base64 --decode > certificate.p12
# Import into keychain
security import certificate.p12 -k build.keychain -P "${{ secrets.MACOS_CERTIFICATE_PWD}}" -T /usr/bin/codesign

MACOS_CERTIFICATE is the base64 encoded .p12 file, MACOS_CERTIFICATE_PWD is the password to the .p12 file.

To prevent authorisation prompt popups during code signing, instruct keychain to allow codesign access:

# Allow codesign to access keychain
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "${{ secrets.KEYCHAIN_PASSWORD}}" build.keychain

As Apple requires multi-factor authentication (MFA) on developer accounts, notarytool uses a dedicated app-password that you can generate on the Apple developer site. We'll add the app-password for notarytool so it can be used later:

xcrun notarytool store-credentials "AC_PASSWORD" --apple-id "${{ secrets.APPLE_ID }}" --team-id ${{ env.TEAM_ID }} --password "${{ secrets.NOTARY_TOOL_PASSWORD }}"

TEAM_ID is the team id in App Store Connect, APPLE_ID is your Apple account e-mail address, NOTARY_TOOL_PASSWORD is the app-password you generated.

To use these steps in your GitHub Actions workflow add them as a step to the job that builds your app:

jobs:
  build_osx:
    runs_on: macos-11
    env:
      TEAM_ID: MY_TEAM_ID
    steps:
    - name: Setup Keychain
      run: |
        security create-keychain -p "${{ secrets.KEYCHAIN_PASSWORD}}" build.keychain
        security default-keychain -s build.keychain
        security unlock-keychain -p "${{ secrets.KEYCHAIN_PASSWORD}}" build.keychain
        echo "${{ secrets.MACOS_CERTIFICATE }}" | base64 --decode > certificate.p12
        security import certificate.p12 -k build.keychain -P "${{ secrets.MACOS_CERTIFICATE_PWD}}" -T /usr/bin/codesign
        security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "${{ secrets.KEYCHAIN_PASSWORD}}" build.keychain
        xcrun notarytool store-credentials "AC_PASSWORD" --apple-id "${{ secrets.APPLE_ID }}" --team-id ${{ env.TEAM_ID }} --password "${{ secrets.NOTARY_TOOL_PASSWORD }}"

When configured like this you will not have to specify a specific keychain file for codesign or notarytool to use.

The next steps are to publish the app and sign it. Start by adding this environment variable to the job:

    env:
      SIGNING_IDENTITY: thumbprint_of_certificate_added_to_keychain

And then add these steps:

    - name: Publish app
      run: dotnet publish -c Release -r osx-x64 -o $RUNNER_TEMP/MyApp.app/Contents/MacOS MyApp.csproj
    - name: Codesign app
      run: |
        find "$RUNNER_TEMP/MyApp.app/Contents/MacOS/"|while read fname; do
          if [ -f "$fname" ]
          then
              echo "[INFO] Signing $fname"
              codesign --force --timestamp --options=runtime --entitlements MyApp.entitlements --sign "${{ env.$SIGNING_IDENTITY }}" "$fname"
          fi
        done
        codesign --force --timestamp --options=runtime --entitlements MyApp.entitlements --sign "${{ env.SIGNING_IDENTITY }}" "$RUNNER_TEMP/MyApp.app"

Note: RUNNER_TEMP is an environment variable provided by GitHub Actions

After code signing the app bundle can now be notarized, by adding this step to the job:

    - name: Notarise app
      run: |
        ditto -c -k --sequesterRsrc --keepParent "$RUNNER_TEMP/MyApp.app" "$RUNNER_TEMP/MyApp.zip"
        xcrun notarytool submit "$RUNNER_TEMP/MyApp.zip" --wait --keychain-profile "AC_PASSWORD"
        xcrun stapler staple "$RUNNER_TEMP/MyApp.app"

When you run this workflow you will have an app bundle that is signed and notarized, ready for packaging in a disk image or installer.

To verify that code signing worked you will need to download it first to trigger the quarantine functionality of macOS. You can do this by e-mailing it to yourself or using a service like WeTransfer or similar.

Once you've downloaded the app bundle and want to start it you should see the popup from macOS saying that the app was scanned and no malware was found.

For more information on Info.plist, see .

The value of matches the binary name generated by dotnet publish -- typically this is the same as your .dll assembly name without .dll.

is set to the display name for your application. If this is longer than 15 characters, set too.

is set to the name of your icns icon file (including extension)

is set to a unique identifier, typically in reverse-DNS format -- e.g. com.myapp.macos.

is set to true (<true/> in the Info.plist).

is set to the version for your bundle, e.g. 1.4.2.

is set to the user-visible string for your application's version, e.g. Major.Minor.Patch.

More documentation on possible Info.plist keys is available .

If at any point the tooling gives you an error that your assets file doesn't have a target for osx-64, add the following to the top <PropertyGroup> in your .csproj:

have caused the MyApp executable (also called the "app host" in the linked documentation) to not be generated. You need this file to be generated in order for your .app to function properly. To make sure this gets generated, do one of the following:

is a that publishes your project and then creates the .app file for you.

For more information on the parameters you can send, see the .

First, publish your application ():

Once you have your .app file created, you'll probably want to sign your app so that it can be notarized and distributed to your users without Gatekeeper giving you a hassle. Notarization is required for apps distributed outside the app store starting in macOS 10.15 (Catalina), and you'll have to enable and run codesign on your .app in order to notarize it successfully.

Enabling hardened runtime is done in the same step as code signing. You have to codesign everything in the .app bundle under the Contents/MacOS folder, which is easiest to do with a script since there are a lot of files. In order to sign your files, you need an Apple developer account. In order to notarize your app, you'll need to do the following steps with a , which requires a paid Apple developer subscription.

First, enable Hardened Runtime with by creating an MyAppEntitlements.entitlements file:

The --options=runtime part of the codesign line is what enables the hardened runtime with your app. Because , we add some exceptions to use JIT-compiled code and allow for Apple Events to be sent. The JIT-compiled code exception is required to run Avalonia apps under hardened runtime. We add the second exception for Apple Events to fix an error that shows up in Console.app.

Note: Microsoft lists as being required for .NET Core. The only one that is actually needed to run an Avalonia app is com.apple.security.cs.allow-jit. The others may impose security risks with your application. Use with caution.

Notarization allows your app to be distributed outside the macOS App Store. You can read more about it . If you run into any issues during the process, Apple has a helpful document of potential fixes .

For more information on customizing your notarization workflow and more flags you may need to send when running xcrun altool, .

The following steps were modified from :

Your app satisfies .

Your app satisfies .

Your app is registered in .

App Store Provision Profile - get it for your app .

Your app content is .

Your app is ready to be launched from inside a .

Your app content should be bundled correctly. .

You should read all and choose the ones your app requires.

📦
application bundle
Apple's documentation here
CFBundleExecutable
CFBundleName
CFBundleDisplayName
CFBundleIconFile
CFBundleIdentifier
NSHighResolutionCapable
CFBundleVersion
CFBundleShortVersionString
here
runtime identifiers
Some recent changes in the way .NET Core is distributed and notarized on macOS
dotnet-bundle
NuGet package
dotnet-bundle documentation
dotnet publish documentation
hardened runtime
Developer ID certificate
exceptions
.NET Core may not be fully compatible with hardened runtime
some other hardened runtime exceptions
here
here
check out Apple's documentation
this StackOverflow post
App Store Review Guidelines
macOS Human Interface Guidelines
App Store Connect
here
bundled correctly
sandbox
Here's an article from Apple with a lot of useful info
entitlements documentation