build_macos.py 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. # Copyright (c) 2023 UltiMaker
  2. # Cura is released under the terms of the LGPLv3 or higher.
  3. import os
  4. import argparse # Command line arguments parsing and help.
  5. import subprocess
  6. from pathlib import Path
  7. ULTIMAKER_CURA_DOMAIN = os.environ.get("ULTIMAKER_CURA_DOMAIN", "nl.ultimaker.cura")
  8. def build_dmg(source_path: str, dist_path: str, filename: str, app_name: str) -> None:
  9. create_dmg_executable = os.environ.get("CREATE_DMG_EXECUTABLE", "create-dmg")
  10. arguments = [create_dmg_executable,
  11. "--window-pos", "640", "360",
  12. "--window-size", "690", "503",
  13. "--app-drop-link", "520", "272",
  14. "--volicon", f"{source_path}/packaging/icons/VolumeIcons_Cura.icns",
  15. "--icon-size", "90",
  16. "--icon", app_name, "169", "272",
  17. "--eula", f"{source_path}/packaging/cura_license.txt",
  18. "--background", f"{source_path}/packaging/MacOs/cura_background_dmg.png",
  19. f"{dist_path}/{filename}",
  20. f"{dist_path}/{app_name}"]
  21. subprocess.run(arguments)
  22. def build_pkg(dist_path: str, app_filename: str, component_filename: str, cura_version: str, installer_filename: str) -> None:
  23. """ Builds and signs the pkg installer.
  24. @param dist_path: Path to put output pkg in
  25. @param app_filename: name of the .app file to bundle inside the pkg
  26. @param component_filename: Name of the pkg component package to bundle the app in
  27. @param cura_version: The version is used when automatically replacing existing versions with the installer.
  28. @param installer_filename: Name of the installer that contains the component package
  29. """
  30. pkg_build_executable = os.environ.get("PKG_BUILD_EXECUTABLE", "pkgbuild")
  31. product_build_executable = os.environ.get("PRODUCT_BUILD_EXECUTABLE", "productbuild")
  32. codesign_identity = os.environ.get("CODESIGN_IDENTITY")
  33. # This builds the component package that contains UltiMaker-Cura.app. This component package will be bundled in a distribution package.
  34. pkg_build_arguments = [
  35. pkg_build_executable,
  36. "--identifier", f"{ULTIMAKER_CURA_DOMAIN}_{cura_version}", # If we want to replace previous version automatically remove {cure_version}
  37. "--component",
  38. Path(dist_path, app_filename),
  39. Path(dist_path, component_filename),
  40. "--install-location", "/Applications",
  41. ]
  42. if codesign_identity:
  43. pkg_build_arguments.extend(["--sign", codesign_identity])
  44. else:
  45. print("CODESIGN_IDENTITY missing. The installer is not being signed")
  46. subprocess.run(pkg_build_arguments)
  47. # This automatically generates a distribution.xml file that is used to build the installer.
  48. # If you want to make any changes to how the installer functions, this file should be changed to do that.
  49. # TODO: Use --product {property_list_file} to pull keys out of file for distribution.xml. This can be used to set min requirements
  50. distribution_creation_arguments = [
  51. product_build_executable,
  52. "--synthesize",
  53. "--package", Path(dist_path, component_filename), # Package that will be inside installer
  54. Path(dist_path, "distribution.xml"), # Output location for sythesized distributions file
  55. ]
  56. subprocess.run(distribution_creation_arguments)
  57. # This creates the distributable package (Installer)
  58. installer_creation_arguments = [
  59. product_build_executable,
  60. "--distribution", Path(dist_path, "distribution.xml"),
  61. "--package-path", dist_path, # Where to find the component packages mentioned in distribution.xml (UltiMaker-Cura.pkg)
  62. Path(dist_path, installer_filename),
  63. ]
  64. if codesign_identity:
  65. installer_creation_arguments.extend(["--sign", codesign_identity])
  66. subprocess.run(installer_creation_arguments)
  67. def notarize_file(dist_path: str, filename: str) -> None:
  68. """ Notarize a file. This takes 5+ minutes, there is indication that this step is successful."""
  69. notarize_user = os.environ.get("MAC_NOTARIZE_USER")
  70. notarize_password = os.environ.get("MAC_NOTARIZE_PASS")
  71. altool_executable = os.environ.get("ALTOOL_EXECUTABLE", "altool")
  72. notarize_arguments = [
  73. "xcrun", altool_executable,
  74. "--notarize-app",
  75. "--primary-bundle-id", ULTIMAKER_CURA_DOMAIN,
  76. "--username", notarize_user,
  77. "--password", notarize_password,
  78. "--file", Path(dist_path, filename)
  79. ]
  80. subprocess.run(notarize_arguments)
  81. def create_pkg_installer(filename: str, dist_path: str, cura_version: str, app_name: str) -> None:
  82. """ Creates a pkg installer from {filename}.app called {filename}-Installer.pkg
  83. The final package structure is UltiMaker-Cura-XXX-Installer.pkg[UltiMaker-Cura.pkg[UltiMaker-Cura.app]]. The outer
  84. pkg file is a distributable pkg (Installer). Inside the distributable pkg there is a component pkg. The component
  85. pkg contains the .app file that will be installed in the users Applications folder.
  86. @param filename: The name of the app file and the app component package file without the extension
  87. @param dist_path: The location to read the app from and save the pkg to
  88. """
  89. filename_stem = Path(filename).stem
  90. 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 This is the app file that will end up in your applications folder
  91. build_pkg(dist_path, app_name, cura_component_package_name, cura_version, filename)
  92. notarize = bool(os.environ.get("NOTARIZE_INSTALLER", "FALSE"))
  93. if notarize:
  94. notarize_file(dist_path, filename)
  95. def create_dmg(filename: str, dist_path: str, source_path: str, app_name: str) -> None:
  96. """ Creates a dmg executable from UltiMaker-Cura.app named {filename}.dmg
  97. @param filename: The name of the app file and the output dmg file without the extension
  98. @param dist_path: The location to read the app from and save the dmg to
  99. @param source_path: The location of the project source files
  100. """
  101. build_dmg(source_path, dist_path, filename, app_name)
  102. notarize_dmg = bool(os.environ.get("NOTARIZE_DMG", "TRUE"))
  103. if notarize_dmg:
  104. notarize_file(dist_path, filename)
  105. if __name__ == "__main__":
  106. parser = argparse.ArgumentParser(description = "Create installer for Cura.")
  107. parser.add_argument("source_path", type = str, help = "Path to Pyinstaller source folder")
  108. parser.add_argument("dist_path", type = str, help = "Path to Pyinstaller dist folder")
  109. parser.add_argument("cura_conan_version", type = str, help="The version of cura")
  110. parser.add_argument("filename", type = str, help = "Filename of the pkg/dmg (e.g. 'UltiMaker-Cura-5.1.0-beta-Macos-X64.pkg' or 'UltiMaker-Cura-5.1.0-beta-Macos-X64.dmg')")
  111. parser.add_argument("app_name", type = str, help = "Filename of the .app that will be contained within the dmg/pkg")
  112. args = parser.parse_args()
  113. cura_version = args.cura_conan_version.split("/")[-1]
  114. app_name = f"{args.app_name}.app"
  115. if Path(args.filename).suffix == ".pkg":
  116. create_pkg_installer(args.filename, args.dist_path, cura_version, app_name)
  117. else:
  118. create_dmg(args.filename, args.dist_path, args.source_path, app_name)