Merge pull request #14039 from Ultimaker/CURA-6867_pkg_for_macos

[CURA-6867] Build PKG Installer for MacOS
This commit is contained in:
Jelle Spijker 2023-01-11 14:04:58 +01:00 committed by GitHub
commit 639e014b36
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 134 additions and 82 deletions

View File

@ -50,7 +50,7 @@ on:
required: true
type: boolean
build_macos:
description: 'Build for MacOs'
description: 'Build for MacOS'
default: true
required: true
type: boolean

View File

@ -62,6 +62,7 @@ env:
MAC_NOTARIZE_USER: ${{ secrets.MAC_NOTARIZE_USER }}
MAC_NOTARIZE_PASS: ${{ secrets.MAC_NOTARIZE_PASS }}
MACOS_CERT_P12: ${{ secrets.MACOS_CERT_P12 }}
MACOS_CERT_INSTALLER_P12: ${{ secrets.MACOS_CERT_INSTALLER_P12 }}
MACOS_CERT_PASS: ${{ secrets.MACOS_CERT_PASS }}
MACOS_CERT_USER: ${{ secrets.MACOS_CERT_USER }}
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
@ -119,7 +120,7 @@ jobs:
- name: Install MacOS system requirements
if: ${{ runner.os == 'Macos' }}
run: brew install autoconf automake ninja create-dmg
run: brew install autoconf automake ninja
- name: Install Linux system requirements
if: ${{ runner.os == 'Linux' }}
@ -152,14 +153,25 @@ jobs:
if: ${{ runner.os == 'Linux' }}
run: echo -n "$GPG_PRIVATE_KEY" | base64 --decode | gpg --import
- name: Configure Macos keychain (Bash)
id: macos-keychain
- name: Configure Macos keychain Developer Cert(Bash)
id: macos-keychain-developer-cert
if: ${{ runner.os == 'Macos' }}
uses: apple-actions/import-codesign-certs@v1
with:
keychain-password: ${{ secrets.MACOS_KEYCHAIN_PASSWORD }}
p12-file-base64: ${{ secrets.MACOS_CERT_P12 }}
p12-password: ${{ secrets.MACOS_CERT_PASSPHRASE }}
- name: Configure Macos keychain Installer Cert (Bash)
id: macos-keychain-installer-cert
if: ${{ runner.os == 'Macos' }}
uses: apple-actions/import-codesign-certs@v1
with:
keychain-password: ${{ secrets.MACOS_KEYCHAIN_PASSWORD }}
create-keychain: false # keychain is created in previous use of action.
p12-file-base64: ${{ secrets.MACOS_CERT_INSTALLER_P12 }}
p12-password: ${{ secrets.MACOS_CERT_PASSPHRASE }}
- name: Create PFX certificate from BASE64_PFX_CONTENT secret
if: ${{ runner.os == 'Windows' }}
id: create-pfx
@ -204,7 +216,7 @@ jobs:
if: ${{ runner.os == 'Macos' }}
run: security unlock -p $TEMP_KEYCHAIN_PASSWORD signing_temp.keychain
env:
TEMP_KEYCHAIN_PASSWORD: ${{ steps.macos-keychain.outputs.keychain-password }}
TEMP_KEYCHAIN_PASSWORD: ${{ steps.macos-keychain-developer-cert.outputs.keychain-password }}
# FIXME: This is a workaround to ensure that we use and pack a shared library for OpenSSL 1.1.1l. We currently compile
# OpenSSL statically for CPython, but our Python Dependenies (such as PyQt6) require a shared library.
@ -239,7 +251,7 @@ jobs:
if "${{ runner.os }}" == "Windows":
installer_ext = "msi" if "${{ inputs.msi_installer }}" == "true" else "exe"
elif "${{ runner.os }}" == "macOS":
installer_ext = "dmg"
installer_ext = "pkg"
else:
installer_ext = "AppImage"
output_env = os.environ["GITHUB_OUTPUT"]
@ -296,9 +308,9 @@ jobs:
run: python ../cura_inst/packaging/AppImage/create_appimage.py ./UltiMaker-Cura $CURA_VERSION_FULL "${{ steps.filename.outputs.FULL_INSTALLER_FILENAME }}"
working-directory: dist
- name: Create the MacOS dmg (Bash)
if: ${{ inputs.installer && runner.os == 'Macos' }}
run: python ../cura_inst/packaging/dmg/dmg_sign_noterize.py ../cura_inst . "${{ steps.filename.outputs.FULL_INSTALLER_FILENAME }}"
- name: Create the MacOS pkg (Bash)
if: ${{ github.event.inputs.installer == 'true' && runner.os == 'Macos' }}
run: python ../cura_inst/packaging/MacOS/build_macos.py . "${{ steps.filename.outputs.FULL_INSTALLER_FILENAME }}"
working-directory: dist
- name: Upload the artifacts

View File

@ -141,6 +141,7 @@ class UMBUNDLE(BUNDLE):
"CFBundleIconFile": os.path.basename(self.icon),
"CFBundleInfoDictionaryVersion": "6.0",
"CFBundlePackageType": "APPL",
"CFBundleVersionString": self.version,
"CFBundleShortVersionString": self.version,
}

View File

@ -317,7 +317,7 @@ class CuraConan(ConanFile):
self._generate_cura_version(Path(self.source_folder, "cura"))
if self.options.devtools:
entitlements_file = "'{}'".format(Path(self.source_folder, "packaging", "dmg", "cura.entitlements"))
entitlements_file = "'{}'".format(Path(self.source_folder, "packaging", "MacOS", "cura.entitlements"))
self._generate_pyinstaller_spec(location = self.generators_folder,
entrypoint_location = "'{}'".format(Path(self.source_folder, self._um_data()["runinfo"]["entrypoint"])).replace("\\", "\\\\"),
icon_path = "'{}'".format(Path(self.source_folder, "packaging", self._um_data()["pyinstaller"]["icon"][str(self.settings.os)])).replace("\\", "\\\\"),
@ -445,7 +445,7 @@ echo "CURA_APP_NAME={{ cura_app_name }}" >> ${{ env_prefix }}GITHUB_ENV
self._generate_cura_version(Path(self._site_packages, "cura"))
entitlements_file = "'{}'".format(Path(self.cpp_info.res_paths[2], "dmg", "cura.entitlements"))
entitlements_file = "'{}'".format(Path(self.cpp_info.res_paths[2], "MacOS", "cura.entitlements"))
self._generate_pyinstaller_spec(location = self._base_dir,
entrypoint_location = "'{}'".format(Path(self.cpp_info.bin_paths[0], self._um_data()["runinfo"]["entrypoint"])).replace("\\", "\\\\"),
icon_path = "'{}'".format(Path(self.cpp_info.res_paths[2], self._um_data()["pyinstaller"]["icon"][str(self.settings.os)])).replace("\\", "\\\\"),

View File

@ -0,0 +1,110 @@
# Copyright (c) 2023 UltiMaker
# Cura is released under the terms of the LGPLv3 or higher.
import os
import argparse # Command line arguments parsing and help.
import subprocess
from pathlib import Path
ULTIMAKER_CURA_DOMAIN = os.environ.get("ULTIMAKER_CURA_DOMAIN", "nl.ultimaker.cura")
def build_pkg(dist_path: str, app_filename: str, component_filename: str, installer_filename: str) -> None:
""" Builds and signs the pkg installer.
@param dist_path: Path to put output pkg in
@param app_filename: name of the .app file to bundle inside the pkg
@param component_filename: Name of the pkg component package to bundle the app in
@param installer_filename: Name of the installer that contains the component package
"""
pkg_build_executable = os.environ.get("PKG_BUILD_EXECUTABLE", "pkgbuild")
product_build_executable = os.environ.get("PRODUCT_BUILD_EXECUTABLE", "productbuild")
codesign_identity = os.environ.get("CODESIGN_IDENTITY")
# This builds the component package that contains UltiMaker-Cura.app. This component package will be bundled in a distribution package.
pkg_build_arguments = [
pkg_build_executable,
"--component",
Path(dist_path, app_filename),
Path(dist_path, component_filename),
"--install-location", "/Applications",
]
if codesign_identity:
pkg_build_arguments.extend(["--sign", codesign_identity])
else:
print("CODESIGN_IDENTITY missing. The installer is not being signed")
subprocess.run(pkg_build_arguments)
# This automatically generates a distribution.xml file that is used to build the installer.
# If you want to make any changes to how the installer functions, this file should be changed to do that.
# TODO: Use --product {property_list_file} to pull keys out of file for distribution.xml. This can be used to set min requirements
distribution_creation_arguments = [
product_build_executable,
"--synthesize",
"--package", Path(dist_path, component_filename), # Package that will be inside installer
Path(dist_path, "distribution.xml"), # Output location for sythesized distributions file
]
subprocess.run(distribution_creation_arguments)
# This creates the distributable package (Installer)
installer_creation_arguments = [
product_build_executable,
"--distribution", Path(dist_path, "distribution.xml"),
"--package-path", dist_path, # Where to find the component packages mentioned in distribution.xml (UltiMaker-Cura.pkg)
Path(dist_path, installer_filename),
]
if codesign_identity:
installer_creation_arguments.extend(["--sign", codesign_identity])
subprocess.run(installer_creation_arguments)
def notarize_file(dist_path: str, filename: str) -> None:
""" Notarize a file. This takes 5+ minutes, there is indication that this step is successful."""
notarize_user = os.environ.get("MAC_NOTARIZE_USER")
notarize_password = os.environ.get("MAC_NOTARIZE_PASS")
altool_executable = os.environ.get("ALTOOL_EXECUTABLE", "altool")
notarize_arguments = [
"xcrun", altool_executable,
"--notarize-app",
"--primary-bundle-id", ULTIMAKER_CURA_DOMAIN,
"--username", notarize_user,
"--password", notarize_password,
"--file", Path(dist_path, filename)
]
subprocess.run(notarize_arguments)
def create_pkg_installer(filename: str, dist_path: str) -> None:
""" Creates a pkg installer from {filename}.app called {filename}-Installer.pkg
The final package structure is UltiMaker-Cura-XXX-Installer.pkg[UltiMaker-Cura.pkg[UltiMaker-Cura.app]]. The outer
pkg file is a distributable pkg (Installer). Inside the distributable pkg there is a component pkg. The component
pkg contains the .app file that will be installed in the users Applications folder.
@param filename: The name of the app file and the app component package file without the extension
@param dist_path: The location to read the app from and save the pkg to
"""
filename_stem = Path(filename).stem
cura_component_package_name = f"{filename_stem}-Component.pkg" # This is a component package that is nested inside the installer, it contains the UltiMaker-Cura.app file
app_name = "UltiMaker-Cura.app" # This is the app file that will end up in your applications folder
build_pkg(dist_path, app_name, cura_component_package_name, filename)
notarize = bool(os.environ.get("NOTARIZE_INSTALLER", "FALSE"))
if notarize:
notarize_file(dist_path, filename)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description = "Create installer for Cura.")
parser.add_argument("dist_path", type = str, help="Path to Pyinstaller dist folder")
parser.add_argument("filename", type = str, help = "Filename of the pkg (e.g. 'UltiMaker-Cura-5.1.0-beta-Macos-X64.pkg')")
args = parser.parse_args()
create_pkg_installer(args.filename, args.dist_path)

View File

Before

Width:  |  Height:  |  Size: 381 KiB

After

Width:  |  Height:  |  Size: 381 KiB

View File

@ -1,71 +0,0 @@
# Copyright (c) 2022 UltiMaker
# Cura is released under the terms of the LGPLv3 or higher.
import os
import argparse # Command line arguments parsing and help.
import subprocess
ULTIMAKER_CURA_DOMAIN = os.environ.get("ULTIMAKER_CURA_DOMAIN", "nl.ultimaker.cura")
def build_dmg(source_path: str, dist_path: str, filename: str) -> None:
create_dmg_executable = os.environ.get("CREATE_DMG_EXECUTABLE", "create-dmg")
arguments = [create_dmg_executable,
"--window-pos", "640", "360",
"--window-size", "690", "503",
"--app-drop-link", "520", "272",
"--volicon", f"{source_path}/packaging/icons/VolumeIcons_Cura.icns",
"--icon-size", "90",
"--icon", "UltiMaker-Cura.app", "169", "272",
"--eula", f"{source_path}/packaging/cura_license.txt",
"--background", f"{source_path}/packaging/dmg/cura_background_dmg.png",
f"{dist_path}/{filename}",
f"{dist_path}/UltiMaker-Cura.app"]
subprocess.run(arguments)
def sign(dist_path: str, filename: str) -> None:
codesign_executable = os.environ.get("CODESIGN", "codesign")
codesign_identity = os.environ.get("CODESIGN_IDENTITY")
arguments = [codesign_executable,
"-s", codesign_identity,
"--timestamp",
"-i", f"{ULTIMAKER_CURA_DOMAIN}.dmg", # TODO: check if this really should have the extra dmg. We seem to be doing this also in the old Rundeck scripts
f"{dist_path}/{filename}"]
subprocess.run(arguments)
def notarize(dist_path: str, filename: str) -> None:
notarize_user = os.environ.get("MAC_NOTARIZE_USER")
notarize_password = os.environ.get("MAC_NOTARIZE_PASS")
altool_executable = os.environ.get("ALTOOL_EXECUTABLE", "altool")
arguments = [
"xcrun", altool_executable,
"--notarize-app",
"--primary-bundle-id", ULTIMAKER_CURA_DOMAIN,
"--username", notarize_user,
"--password", notarize_password,
"--file", f"{dist_path}/{filename}"
]
subprocess.run(arguments)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description = "Create dmg of Cura.")
parser.add_argument("source_path", type=str, help="Path to Conan install Cura folder.")
parser.add_argument("dist_path", type=str, help="Path to Pyinstaller dist folder")
parser.add_argument("filename", type = str, help = "Filename of the dmg (e.g. 'UltiMaker-Cura-5.1.0-beta-Linux-X64.dmg')")
args = parser.parse_args()
build_dmg(args.source_path, args.dist_path, args.filename)
sign(args.dist_path, args.filename)
notarize_dmg = bool(os.environ.get("NOTARIZE_DMG", "TRUE"))
if notarize_dmg:
notarize(args.dist_path, args.filename)