syncmoss 0.4.0__tar.gz → 0.4.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (208) hide show
  1. syncmoss-0.4.2/.gitattributes +3 -0
  2. syncmoss-0.4.2/.github/workflows/_MacOS.yml +548 -0
  3. {syncmoss-0.4.0 → syncmoss-0.4.2}/COPYING.txt +7 -1
  4. syncmoss-0.4.2/NOTICE.txt +120 -0
  5. {syncmoss-0.4.0/syncmoss.egg-info → syncmoss-0.4.2}/PKG-INFO +39 -3
  6. {syncmoss-0.4.0 → syncmoss-0.4.2}/README.md +38 -2
  7. {syncmoss-0.4.0 → syncmoss-0.4.2}/bundle/MacOS.spec +8 -1
  8. syncmoss-0.4.2/bundle/SYNCmoss.png +0 -0
  9. {syncmoss-0.4.0 → syncmoss-0.4.2}/bundle/Windows.spec +6 -0
  10. {syncmoss-0.4.0 → syncmoss-0.4.2}/bundle/bundle.py +5 -0
  11. syncmoss-0.4.2/bundle/dmg_background.pdf +0 -0
  12. syncmoss-0.4.2/bundle/licenses/GPL-3.0.txt +674 -0
  13. syncmoss-0.4.2/bundle/licenses/LGPL-3.0.txt +165 -0
  14. {syncmoss-0.4.0 → syncmoss-0.4.2}/bundle/macos_bundle.py +6 -0
  15. syncmoss-0.4.2/bundle/third_party_licenses.py +82 -0
  16. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/Calibration.py +3 -3
  17. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/Library/alpha-Fe (metallic iron, bcc).mdl +1 -1
  18. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/Library_window.py +1 -1
  19. syncmoss-0.4.2/syncmoss/bliss_channel.py +82 -0
  20. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/constants.py +5 -0
  21. syncmoss-0.4.2/syncmoss/error_reporter.py +431 -0
  22. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/fitting_io.py +55 -8
  23. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/instrumental_io.py +27 -29
  24. syncmoss-0.4.2/syncmoss/license_window.py +201 -0
  25. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/main.py +17 -3
  26. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/model_io.py +245 -88
  27. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/models.py +41 -5
  28. syncmoss-0.4.2/syncmoss/parameters/Be.txt +1 -0
  29. syncmoss-0.4.2/syncmoss/parameters/Calibration.dat +513 -0
  30. syncmoss-0.4.2/syncmoss/parameters/INSth.txt +1 -0
  31. syncmoss-0.4.2/syncmoss/parameters/KB.txt +1 -0
  32. syncmoss-0.4.2/syncmoss/parameters/calibr.png +0 -0
  33. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/parameters/help.md +98 -4
  34. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/parameters/models_description.md +14 -12
  35. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/parameters_table.py +160 -12
  36. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/results_table.py +60 -30
  37. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/sms_theory.py +7 -2
  38. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/spectrum_io.py +11 -3
  39. syncmoss-0.4.2/syncmoss/spectrum_parameters.py +402 -0
  40. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/spectrum_plotter.py +22 -6
  41. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/supp_menu.py +280 -12
  42. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/syncmoss_main.py +619 -132
  43. {syncmoss-0.4.0 → syncmoss-0.4.2/syncmoss.egg-info}/PKG-INFO +39 -3
  44. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss.egg-info/SOURCES.txt +24 -0
  45. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss.egg-info/scm_file_list.json +24 -0
  46. syncmoss-0.4.2/tests/syncmoss_test.py +676 -0
  47. syncmoss-0.4.2/tests/test_bliss_channel.py +200 -0
  48. syncmoss-0.4.2/tests/test_chi2_spread.py +54 -0
  49. syncmoss-0.4.2/tests/test_contact_author.py +88 -0
  50. syncmoss-0.4.2/tests/test_error_reporter.py +254 -0
  51. syncmoss-0.4.2/tests/test_expression_fit_start.py +89 -0
  52. syncmoss-0.4.2/tests/test_impurity_presets.py +173 -0
  53. {syncmoss-0.4.0 → syncmoss-0.4.2}/tests/test_instrumental_metadata.py +4 -0
  54. {syncmoss-0.4.0 → syncmoss-0.4.2}/tests/test_instrumental_recentre.py +21 -5
  55. syncmoss-0.4.2/tests/test_interrupt.py +310 -0
  56. syncmoss-0.4.2/tests/test_library_dir.py +239 -0
  57. syncmoss-0.4.2/tests/test_license_window.py +131 -0
  58. syncmoss-0.4.2/tests/test_results_table_image.py +76 -0
  59. syncmoss-0.4.2/tests/test_save_result_model.py +386 -0
  60. {syncmoss-0.4.0 → syncmoss-0.4.2}/tests/test_search_guards.py +3 -9
  61. syncmoss-0.4.2/tests/test_spectrum_parameters.py +390 -0
  62. syncmoss-0.4.2/tests/test_table_marks.py +201 -0
  63. syncmoss-0.4.2/tests/test_take_result.py +148 -0
  64. syncmoss-0.4.0/.github/workflows/_MacOS.yml +0 -329
  65. syncmoss-0.4.0/NOTICE.txt +0 -50
  66. syncmoss-0.4.0/syncmoss/parameters/Be.txt +0 -1
  67. syncmoss-0.4.0/syncmoss/parameters/Calibration.dat +0 -513
  68. syncmoss-0.4.0/syncmoss/parameters/INSth.txt +0 -1
  69. syncmoss-0.4.0/syncmoss/parameters/KB.txt +0 -1
  70. syncmoss-0.4.0/syncmoss/parameters/calibr.png +0 -0
  71. syncmoss-0.4.0/tests/syncmoss_test.py +0 -359
  72. {syncmoss-0.4.0 → syncmoss-0.4.2}/.github/workflows/Tests.yml +0 -0
  73. {syncmoss-0.4.0 → syncmoss-0.4.2}/.github/workflows/_PyPI.yml +0 -0
  74. {syncmoss-0.4.0 → syncmoss-0.4.2}/.github/workflows/_TagRelease.yml +0 -0
  75. {syncmoss-0.4.0 → syncmoss-0.4.2}/.github/workflows/_Tests.yml +0 -0
  76. {syncmoss-0.4.0 → syncmoss-0.4.2}/.github/workflows/_UploadRelease.yml +0 -0
  77. {syncmoss-0.4.0 → syncmoss-0.4.2}/.github/workflows/_Version.yml +0 -0
  78. {syncmoss-0.4.0 → syncmoss-0.4.2}/.github/workflows/_Wheels.yml +0 -0
  79. {syncmoss-0.4.0 → syncmoss-0.4.2}/.github/workflows/_WindowsExe.yml +0 -0
  80. {syncmoss-0.4.0 → syncmoss-0.4.2}/.github/workflows/release.yml +0 -0
  81. {syncmoss-0.4.0 → syncmoss-0.4.2}/.gitignore +0 -0
  82. {syncmoss-0.4.0 → syncmoss-0.4.2}/.vscode/settings.json +0 -0
  83. {syncmoss-0.4.0 → syncmoss-0.4.2}/LICENSE +0 -0
  84. {syncmoss-0.4.0 → syncmoss-0.4.2}/Lib_test/test.mdl +0 -0
  85. {syncmoss-0.4.0 → syncmoss-0.4.2}/docs/thick_models.md +0 -0
  86. {syncmoss-0.4.0 → syncmoss-0.4.2}/docs/thick_vs_thin.md +0 -0
  87. {syncmoss-0.4.0 → syncmoss-0.4.2}/pyproject.toml +0 -0
  88. {syncmoss-0.4.0 → syncmoss-0.4.2}/requirements.txt +0 -0
  89. {syncmoss-0.4.0 → syncmoss-0.4.2}/setup.cfg +0 -0
  90. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/Hamiltonian_helper.py +0 -0
  91. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/Library/Aegirine.mdl +0 -0
  92. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/Library/Akaganeite.mdl +0 -0
  93. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/Library/Almandine.mdl +0 -0
  94. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/Library/Antitaenite.mdl +0 -0
  95. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/Library/Arsenopyrite.mdl +0 -0
  96. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/Library/Bornite.mdl +0 -0
  97. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/Library/Bridgmanite (Fe,Al-bearing) @100 GPa.mdl +0 -0
  98. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/Library/Bridgmanite (Fe,Al-bearing).mdl +0 -0
  99. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/Library/Celadonite.mdl +0 -0
  100. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/Library/Cementite.mdl +0 -0
  101. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/Library/Chalcopyrite.mdl +0 -0
  102. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/Library/Clinopyroxene (augite).mdl +0 -0
  103. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/Library/Copiapite.mdl +0 -0
  104. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/Library/Cubanite.mdl +0 -0
  105. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/Library/Epidote.mdl +0 -0
  106. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/Library/Fayalite (Fe-olivine).mdl +0 -0
  107. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/Library/Ferrihydrite (2-line).mdl +0 -0
  108. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/Library/Ferropericlase.mdl +0 -0
  109. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/Library/Goethite.mdl +0 -0
  110. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/Library/Green rust (fougerite, carbonate).mdl +0 -0
  111. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/Library/Haegg carbide.mdl +0 -0
  112. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/Library/Hedenbergite.mdl +0 -0
  113. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/Library/Hematite.mdl +0 -0
  114. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/Library/Heterosite (FePO4).mdl +0 -0
  115. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/Library/Ilmenite.mdl +0 -0
  116. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/Library/Iron phosphide.mdl +0 -0
  117. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/Library/Isocubanite.mdl +0 -0
  118. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/Library/Jarosite.mdl +0 -0
  119. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/Library/Kamacite.mdl +0 -0
  120. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/Library/Lepidocrocite.mdl +0 -0
  121. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/Library/Maghemite.mdl +0 -0
  122. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/Library/Magnetite.mdl +0 -0
  123. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/Library/Marcasite.mdl +0 -0
  124. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/Library/Melanterite.mdl +0 -0
  125. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/Library/Orthopyroxene (enstatite-ferrosilite).mdl +0 -0
  126. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/Library/Prussian blue (ferric ferrocyanide).mdl +0 -0
  127. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/Library/Pyrite.mdl +0 -0
  128. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/Library/Pyrrhotite.mdl +0 -0
  129. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/Library/Rhomboclase.mdl +0 -0
  130. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/Library/Roemerite.mdl +0 -0
  131. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/Library/Siderite.mdl +0 -0
  132. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/Library/Szomolnokite.mdl +0 -0
  133. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/Library/Taenite.mdl +0 -0
  134. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/Library/Talnakhite.mdl +0 -0
  135. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/Library/Tetrataenite.mdl +0 -0
  136. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/Library/Triphylite (LiFePO4).mdl +0 -0
  137. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/Library/Troilite.mdl +0 -0
  138. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/Library/Vivianite.mdl +0 -0
  139. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/Library/Voltaite.mdl +0 -0
  140. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/Library_io.py +0 -0
  141. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/icons/CheckBox.png +0 -0
  142. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/icons/CheckBox_.png +0 -0
  143. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/icons/CheckBox_L.png +0 -0
  144. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/icons/CheckBox_L2.png +0 -0
  145. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/icons/DU.png +0 -0
  146. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/icons/Switch.png +0 -0
  147. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/icons/UD.png +0 -0
  148. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/icons/icon_r.ico +0 -0
  149. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/instrumental_window.py +0 -0
  150. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/legacy.py +0 -0
  151. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/minimi_lib.py +0 -0
  152. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/models_description_window.py +0 -0
  153. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/models_positions.py +0 -0
  154. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/parameters/ABSorber3.txt +0 -0
  155. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/parameters/GCMS.txt +0 -0
  156. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/parameters/INS_APS.txt +0 -0
  157. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/parameters/INSexp.txt +0 -0
  158. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/parameters/INSint.txt +0 -0
  159. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/parameters/NFS.txt +0 -0
  160. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/support_math.py +0 -0
  161. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/test_graf.txt +0 -0
  162. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/test_param.txt +0 -0
  163. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/theme_dark.json +0 -0
  164. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss/theme_light.json +0 -0
  165. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss.egg-info/dependency_links.txt +0 -0
  166. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss.egg-info/entry_points.txt +0 -0
  167. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss.egg-info/requires.txt +0 -0
  168. {syncmoss-0.4.0 → syncmoss-0.4.2}/syncmoss.egg-info/top_level.txt +0 -0
  169. {syncmoss-0.4.0 → syncmoss-0.4.2}/tests/conftest.py +0 -0
  170. {syncmoss-0.4.0 → syncmoss-0.4.2}/tests/data/Distr_corr.mdl +0 -0
  171. {syncmoss-0.4.0 → syncmoss-0.4.2}/tests/data/Distr_distr.mdl +0 -0
  172. {syncmoss-0.4.0 → syncmoss-0.4.2}/tests/data/alpha_fe_sms_000.mca +0 -0
  173. {syncmoss-0.4.0 → syncmoss-0.4.2}/tests/data/calibration_000_golden.json +0 -0
  174. {syncmoss-0.4.0 → syncmoss-0.4.2}/tests/data/generate_cal_fixtures.py +0 -0
  175. {syncmoss-0.4.0 → syncmoss-0.4.2}/tests/data/synthetic_alpha_fe_cms_linear.mca +0 -0
  176. {syncmoss-0.4.0 → syncmoss-0.4.2}/tests/golden_models.json +0 -0
  177. {syncmoss-0.4.0 → syncmoss-0.4.2}/tests/test_calibration.py +0 -0
  178. {syncmoss-0.4.0 → syncmoss-0.4.2}/tests/test_calibration_helpers.py +0 -0
  179. {syncmoss-0.4.0 → syncmoss-0.4.2}/tests/test_calibration_path.py +0 -0
  180. {syncmoss-0.4.0 → syncmoss-0.4.2}/tests/test_distr_composition.py +0 -0
  181. {syncmoss-0.4.0 → syncmoss-0.4.2}/tests/test_distr_expansion.py +0 -0
  182. {syncmoss-0.4.0 → syncmoss-0.4.2}/tests/test_distr_mdl.py +0 -0
  183. {syncmoss-0.4.0 → syncmoss-0.4.2}/tests/test_distr_targets.py +0 -0
  184. {syncmoss-0.4.0 → syncmoss-0.4.2}/tests/test_expression_validation.py +0 -0
  185. {syncmoss-0.4.0 → syncmoss-0.4.2}/tests/test_golden_models.py +0 -0
  186. {syncmoss-0.4.0 → syncmoss-0.4.2}/tests/test_gui_smoke.py +0 -0
  187. {syncmoss-0.4.0 → syncmoss-0.4.2}/tests/test_hamiltonian_texture.py +0 -0
  188. {syncmoss-0.4.0 → syncmoss-0.4.2}/tests/test_instrumental_method_choice.py +0 -0
  189. {syncmoss-0.4.0 → syncmoss-0.4.2}/tests/test_instrumental_window.py +0 -0
  190. {syncmoss-0.4.0 → syncmoss-0.4.2}/tests/test_integral_points.py +0 -0
  191. {syncmoss-0.4.0 → syncmoss-0.4.2}/tests/test_legacy.py +0 -0
  192. {syncmoss-0.4.0 → syncmoss-0.4.2}/tests/test_library_io.py +0 -0
  193. {syncmoss-0.4.0 → syncmoss-0.4.2}/tests/test_model_append.py +0 -0
  194. {syncmoss-0.4.0 → syncmoss-0.4.2}/tests/test_model_io.py +0 -0
  195. {syncmoss-0.4.0 → syncmoss-0.4.2}/tests/test_model_only_mode.py +0 -0
  196. {syncmoss-0.4.0 → syncmoss-0.4.2}/tests/test_nbaseline_results.py +0 -0
  197. {syncmoss-0.4.0 → syncmoss-0.4.2}/tests/test_parameter_links.py +0 -0
  198. {syncmoss-0.4.0 → syncmoss-0.4.2}/tests/test_polarization.py +0 -0
  199. {syncmoss-0.4.0 → syncmoss-0.4.2}/tests/test_recon_composition.py +0 -0
  200. {syncmoss-0.4.0 → syncmoss-0.4.2}/tests/test_recon_mdl.py +0 -0
  201. {syncmoss-0.4.0 → syncmoss-0.4.2}/tests/test_recon_results.py +0 -0
  202. {syncmoss-0.4.0 → syncmoss-0.4.2}/tests/test_reference_shift.py +0 -0
  203. {syncmoss-0.4.0 → syncmoss-0.4.2}/tests/test_sdw_grid.py +0 -0
  204. {syncmoss-0.4.0 → syncmoss-0.4.2}/tests/test_sms_theory.py +0 -0
  205. {syncmoss-0.4.0 → syncmoss-0.4.2}/tests/test_support_math.py +0 -0
  206. {syncmoss-0.4.0 → syncmoss-0.4.2}/tests/test_texture_s1.py +0 -0
  207. {syncmoss-0.4.0 → syncmoss-0.4.2}/tests/test_theory_search.py +0 -0
  208. {syncmoss-0.4.0 → syncmoss-0.4.2}/tests/test_thick_gui.py +0 -0
@@ -0,0 +1,3 @@
1
+ # Verbatim license texts: keep LF on every OS, so Windows checkouts (and the
2
+ # Windows bundle) carry the files byte-for-byte as published by gnu.org.
3
+ bundle/licenses/*.txt text eol=lf
@@ -0,0 +1,548 @@
1
+ name: _macOS (PyInstaller universal2 app)
2
+
3
+ # Modelled on crispy's release workflow (github.com/mretegan/crispy):
4
+ # 1. build the app natively on an Apple Silicon AND an Intel runner,
5
+ # 2. fuse the two bundles into one universal2 SYNCmoss.app,
6
+ # 3. sign it (Developer ID + hardened runtime), pack the drag-and-drop DMG
7
+ # (window laid out as crispy's create-dmg.sh does), notarize, staple.
8
+ #
9
+ # Signing and notarization run only when the Apple secrets are set (repository
10
+ # Settings -> Secrets and variables -> Actions; release.yml forwards them).
11
+ # Without them the job still ships a universal2 DMG, ad-hoc signed and not
12
+ # notarized, named SYNCmoss-macOS-universal2.dmg. A notarized one is named
13
+ # SYNCmoss-macOS-universal2-signed.dmg; _TagRelease.yml / _UploadRelease.yml
14
+ # tell the two apart by that name.
15
+ #
16
+ # APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_BASE64
17
+ # the "Developer ID Application" certificate WITH its private key,
18
+ # exported from Keychain Access as .p12, then: base64 -i cert.p12 | pbcopy
19
+ # APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_PASSWORD password of that .p12
20
+ # APPLE_ID Apple Account e-mail of the developer
21
+ # APPLE_TEAM_ID 10-character Team ID (developer.apple.com -> Membership)
22
+ # APPLE_APPLICATION_SPECIFIC_PASSWORD app-specific password (account.apple.com -> Sign-In and Security)
23
+ # APPLE_KEYCHAIN_PASSWORD optional: password of the throwaway CI keychain
24
+ # (a random one is used when unset)
25
+
26
+ on:
27
+ workflow_call:
28
+ secrets:
29
+ APPLE_ID:
30
+ required: false
31
+ APPLE_TEAM_ID:
32
+ required: false
33
+ APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_PASSWORD:
34
+ required: false
35
+ APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_BASE64:
36
+ required: false
37
+ APPLE_KEYCHAIN_PASSWORD:
38
+ required: false
39
+ APPLE_APPLICATION_SPECIFIC_PASSWORD:
40
+ required: false
41
+
42
+ jobs:
43
+ build:
44
+ name: Build macOS app (${{ matrix.arch }})
45
+ runs-on: ${{ matrix.runner }}
46
+ timeout-minutes: 60
47
+ strategy:
48
+ fail-fast: false
49
+ matrix:
50
+ include:
51
+ - runner: macos-15
52
+ arch: arm64
53
+ - runner: macos-15-intel
54
+ arch: x86_64
55
+ env:
56
+ MACOSX_DEPLOYMENT_TARGET: "11"
57
+ CFLAGS: "-mmacosx-version-min=11"
58
+ LDFLAGS: "-mmacosx-version-min=11"
59
+ steps:
60
+ - uses: actions/checkout@v6
61
+
62
+ - uses: actions/setup-python@v6
63
+ with:
64
+ python-version: "3.13"
65
+ cache: "pip"
66
+
67
+ - name: Check the runner architecture
68
+ run: |
69
+ MACHINE="$(python -c 'import platform; print(platform.machine())')"
70
+ echo "Python runs as ${MACHINE}"
71
+ if [ "${MACHINE}" != "${{ matrix.arch }}" ]; then
72
+ echo "::error::expected a ${{ matrix.arch }} runner, got ${MACHINE}"
73
+ exit 1
74
+ fi
75
+
76
+ - name: Download wheel artifact
77
+ uses: actions/download-artifact@v6
78
+ with:
79
+ name: wheels
80
+ path: wheels
81
+
82
+ # numba 0.62.x (llvmlite 0.45.x) is the last release with macOS x86_64
83
+ # wheels. The pin applies to BOTH architectures: the universal2 app holds
84
+ # one copy of the pure-Python code, so the two builds must resolve the
85
+ # same versions (checked again before the merge). --only-binary stops pip
86
+ # from quietly building an sdist on one architecture only.
87
+ - name: Install dependencies
88
+ run: |
89
+ python -m pip install --upgrade pip setuptools wheel
90
+ python -m pip install --only-binary=:all: "numba==0.62.1" -r requirements.txt pyinstaller wheels/*.whl
91
+ python -m pip list --format=freeze
92
+
93
+ - name: Build the app
94
+ run: python bundle/macos_bundle.py --dist-dir bundle/MacOS/${{ matrix.arch }} --work-dir bundle/build/MacOS/${{ matrix.arch }}
95
+
96
+ # tar keeps the symlinks (Qt frameworks) and executable bits that
97
+ # upload-artifact would drop.
98
+ - name: Archive the app bundle
99
+ run: |
100
+ set -euo pipefail
101
+ DIST="bundle/MacOS/${{ matrix.arch }}"
102
+ APP_PATH=""
103
+ for candidate in "${DIST}/SYNCmoss.app" "${DIST}/SYNCmoss/SYNCmoss.app"; do
104
+ if [ -d "${candidate}" ]; then
105
+ APP_PATH="${candidate}"
106
+ break
107
+ fi
108
+ done
109
+ if [ -z "${APP_PATH}" ]; then
110
+ echo "::error::SYNCmoss.app not found in the ${{ matrix.arch }} build output"
111
+ find "${DIST}" -maxdepth 3 -print
112
+ exit 1
113
+ fi
114
+ echo "Executable architectures: $(lipo -archs "${APP_PATH}/Contents/MacOS/SYNCmoss")"
115
+ mkdir -p macos-artifact
116
+ tar -cpzf macos-artifact/SYNCmoss.app.tar.gz -C "$(dirname "${APP_PATH}")" SYNCmoss.app
117
+ python -m pip list --format=freeze > macos-artifact/pip-freeze.txt
118
+ echo "APP_PATH=${APP_PATH}" >> "${GITHUB_ENV}"
119
+
120
+ - name: Upload the app bundle
121
+ uses: actions/upload-artifact@v5
122
+ with:
123
+ name: macos-app-${{ matrix.arch }}
124
+ path: macos-artifact/
125
+ if-no-files-found: error
126
+ retention-days: 1
127
+
128
+ # Runs each architecture natively (the x86_64 slice is never exercised
129
+ # later). After archiving, so nothing the test writes reaches the artifact.
130
+ - name: Smoke-test the frozen app
131
+ run: |
132
+ "${APP_PATH}/Contents/MacOS/SYNCmoss" --test
133
+
134
+ universal2:
135
+ name: Merge, sign and notarize the universal2 app
136
+ needs: build
137
+ runs-on: macos-15
138
+ timeout-minutes: 90
139
+ steps:
140
+ - uses: actions/checkout@v6
141
+
142
+ - uses: actions/setup-python@v6
143
+ with:
144
+ python-version: "3.13"
145
+
146
+ - name: Download the arm64 app
147
+ uses: actions/download-artifact@v6
148
+ with:
149
+ name: macos-app-arm64
150
+ path: bundles/arm64
151
+
152
+ - name: Download the x86_64 app
153
+ uses: actions/download-artifact@v6
154
+ with:
155
+ name: macos-app-x86_64
156
+ path: bundles/x86_64
157
+
158
+ - name: Check both builds used the same packages
159
+ run: |
160
+ if ! diff -u bundles/arm64/pip-freeze.txt bundles/x86_64/pip-freeze.txt; then
161
+ echo "::error::arm64 and x86_64 resolved different package versions, so the universal2 app would mix them; pin the differing packages in the build job's install step"
162
+ exit 1
163
+ fi
164
+
165
+ - name: Extract the app bundles
166
+ run: |
167
+ set -euo pipefail
168
+ mkdir -p arm64 x86_64
169
+ tar -xpf bundles/arm64/SYNCmoss.app.tar.gz -C arm64
170
+ tar -xpf bundles/x86_64/SYNCmoss.app.tar.gz -C x86_64
171
+
172
+ # Adapted from crispy's package/merge-universal.py: walk both bundles,
173
+ # fuse every pair of thin Mach-O files with lipo, copy everything else
174
+ # (identical files, already-universal binaries, data files, symlinks),
175
+ # then require every Mach-O file of the result to carry both slices.
176
+ - name: Merge into a universal2 app
177
+ shell: python
178
+ run: |
179
+ import filecmp
180
+ import os
181
+ import shutil
182
+ import subprocess
183
+ import sys
184
+
185
+ ARM_APP = "arm64/SYNCmoss.app"
186
+ X86_APP = "x86_64/SYNCmoss.app"
187
+ OUT_APP = "dist/universal2/SYNCmoss.app"
188
+
189
+ THIN_MAGICS = {b"\xfe\xed\xfa\xce", b"\xfe\xed\xfa\xcf", b"\xce\xfa\xed\xfe", b"\xcf\xfa\xed\xfe"}
190
+ FAT_MAGICS = {b"\xca\xfe\xba\xbe", b"\xbe\xba\xfe\xca"}
191
+
192
+
193
+ def macho_kind(path):
194
+ """'thin', 'fat' or None, from the magic bytes: PyInstaller ships
195
+ extension-less Mach-O files. Java .class files share the fat magic,
196
+ but a real universal binary has only a few architectures."""
197
+ with open(path, "rb") as f:
198
+ header = f.read(8)
199
+ if len(header) < 8:
200
+ return None
201
+ if header[:4] in THIN_MAGICS:
202
+ return "thin"
203
+ if header[:4] in FAT_MAGICS:
204
+ byteorder = "big" if header[:4] == b"\xca\xfe\xba\xbe" else "little"
205
+ return "fat" if int.from_bytes(header[4:8], byteorder) < 30 else None
206
+ return None
207
+
208
+
209
+ def collect(root):
210
+ """Relative path -> 'dir', 'symlink' or 'file' (symlinked dirs are not descended)."""
211
+ entries = {}
212
+ for current, dirnames, filenames in os.walk(root):
213
+ for name in dirnames + filenames:
214
+ path = os.path.join(current, name)
215
+ if os.path.islink(path):
216
+ kind = "symlink"
217
+ elif os.path.isdir(path):
218
+ kind = "dir"
219
+ else:
220
+ kind = "file"
221
+ entries[os.path.relpath(path, root)] = kind
222
+ return entries
223
+
224
+
225
+ arm = collect(ARM_APP)
226
+ x86 = collect(X86_APP)
227
+ os.makedirs(OUT_APP)
228
+ single_arch = []
229
+ fused = 0
230
+
231
+ for rel in sorted(set(arm) | set(x86)):
232
+ arm_kind, x86_kind = arm.get(rel), x86.get(rel)
233
+ arm_path = os.path.join(ARM_APP, rel)
234
+ x86_path = os.path.join(X86_APP, rel)
235
+ out_path = os.path.join(OUT_APP, rel)
236
+
237
+ if "dir" in (arm_kind, x86_kind):
238
+ os.makedirs(out_path, exist_ok=True)
239
+ continue
240
+ os.makedirs(os.path.dirname(out_path), exist_ok=True)
241
+
242
+ if "symlink" in (arm_kind, x86_kind):
243
+ if arm_kind == x86_kind and os.readlink(arm_path) != os.readlink(x86_path):
244
+ print(f"WARNING: symlink target differs, using arm64: {rel}")
245
+ os.symlink(os.readlink(arm_path if arm_kind == "symlink" else x86_path), out_path)
246
+ continue
247
+
248
+ if arm_kind is None or x86_kind is None:
249
+ present = arm_path if arm_kind else x86_path
250
+ if macho_kind(present):
251
+ single_arch.append(rel)
252
+ else:
253
+ print(f"WARNING: data file present in one build only: {rel}")
254
+ shutil.copy2(present, out_path)
255
+ continue
256
+
257
+ if filecmp.cmp(arm_path, x86_path, shallow=False):
258
+ shutil.copy2(arm_path, out_path)
259
+ continue
260
+
261
+ arm_macho, x86_macho = macho_kind(arm_path), macho_kind(x86_path)
262
+ if arm_macho == x86_macho == "thin":
263
+ subprocess.run(["lipo", "-create", arm_path, x86_path, "-output", out_path], check=True)
264
+ shutil.copymode(arm_path, out_path)
265
+ fused += 1
266
+ elif "fat" in (arm_macho, x86_macho):
267
+ # Already universal; the two copies differ only in their signature.
268
+ shutil.copy2(arm_path if arm_macho == "fat" else x86_path, out_path)
269
+ elif arm_macho or x86_macho:
270
+ single_arch.append(rel)
271
+ else:
272
+ print(f"WARNING: data file differs, using arm64: {rel}")
273
+ shutil.copy2(arm_path, out_path)
274
+
275
+ if single_arch:
276
+ sys.exit(
277
+ "ERROR: these Mach-O binaries exist for one architecture only, so the "
278
+ "app would crash on the other:\n" + "\n".join(f" - {p}" for p in single_arch)
279
+ )
280
+
281
+ not_universal = []
282
+ checked = 0
283
+ for rel, kind in sorted(collect(OUT_APP).items()):
284
+ path = os.path.join(OUT_APP, rel)
285
+ if kind != "file" or not macho_kind(path):
286
+ continue
287
+ checked += 1
288
+ archs = subprocess.run(["lipo", "-archs", path], capture_output=True, text=True).stdout.split()
289
+ if not {"arm64", "x86_64"} <= set(archs):
290
+ not_universal.append(f" - {rel}: {' '.join(archs) or 'unreadable'}")
291
+
292
+ print(f"Fused {fused} binaries, checked {checked} Mach-O files")
293
+ if not_universal:
294
+ sys.exit("ERROR: these binaries are not universal2:\n" + "\n".join(not_universal))
295
+ print(f"Universal2 app written to {OUT_APP}")
296
+
297
+ # The merge leaves the bundle's seal stale, so the app is always re-signed:
298
+ # with the Developer ID when the certificate secrets are set, otherwise
299
+ # ad-hoc (so macOS reports "unverified developer", not "damaged").
300
+ - name: Sign the app
301
+ id: sign
302
+ env:
303
+ CERTIFICATE_BASE64: ${{ secrets.APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_BASE64 }}
304
+ CERTIFICATE_PASSWORD: ${{ secrets.APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_PASSWORD }}
305
+ KEYCHAIN_PASSWORD: ${{ secrets.APPLE_KEYCHAIN_PASSWORD }}
306
+ run: |
307
+ set -euo pipefail
308
+ APP="dist/universal2/SYNCmoss.app"
309
+
310
+ # The hardened runtime needs these for the numba/llvmlite JIT (writable
311
+ # + executable memory) and for loading the bundled Python extensions.
312
+ ENTITLEMENTS="${RUNNER_TEMP}/entitlements.plist"
313
+ cat > "${ENTITLEMENTS}" <<'EOF'
314
+ <?xml version="1.0" encoding="UTF-8"?>
315
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
316
+ <plist version="1.0">
317
+ <dict>
318
+ <key>com.apple.security.cs.allow-jit</key>
319
+ <true/>
320
+ <key>com.apple.security.cs.allow-unsigned-executable-memory</key>
321
+ <true/>
322
+ <key>com.apple.security.cs.disable-library-validation</key>
323
+ <true/>
324
+ </dict>
325
+ </plist>
326
+ EOF
327
+
328
+ # Extended attributes would invalidate the signature.
329
+ xattr -cr "${APP}"
330
+
331
+ if [ -n "${CERTIFICATE_BASE64}" ] && [ -n "${CERTIFICATE_PASSWORD}" ]; then
332
+ KEYCHAIN_PATH="${RUNNER_TEMP}/signing.keychain-db"
333
+ CERTIFICATE_PATH="${RUNNER_TEMP}/certificate.p12"
334
+ if [ -z "${KEYCHAIN_PASSWORD}" ]; then
335
+ KEYCHAIN_PASSWORD="$(openssl rand -base64 24)"
336
+ echo "::add-mask::${KEYCHAIN_PASSWORD}"
337
+ fi
338
+
339
+ echo "Creating a temporary keychain and importing the certificate"
340
+ echo "${CERTIFICATE_BASE64}" | base64 --decode -o "${CERTIFICATE_PATH}"
341
+ security create-keychain -p "${KEYCHAIN_PASSWORD}" "${KEYCHAIN_PATH}"
342
+ security set-keychain-settings -lut 21600 "${KEYCHAIN_PATH}"
343
+ security unlock-keychain -p "${KEYCHAIN_PASSWORD}" "${KEYCHAIN_PATH}"
344
+ security import "${CERTIFICATE_PATH}" -P "${CERTIFICATE_PASSWORD}" -A -t cert -f pkcs12 -k "${KEYCHAIN_PATH}"
345
+ rm -f "${CERTIFICATE_PATH}"
346
+ security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "${KEYCHAIN_PASSWORD}" "${KEYCHAIN_PATH}" > /dev/null
347
+ security list-keychains -d user -s "${KEYCHAIN_PATH}"
348
+
349
+ IDENTITY="$(security find-identity -v -p codesigning "${KEYCHAIN_PATH}" | awk '/"Developer ID Application/ && !found { print $2; found = 1 }')"
350
+ if [ -z "${IDENTITY}" ]; then
351
+ echo "::error::the certificate holds no 'Developer ID Application' identity"
352
+ security find-identity -v -p codesigning "${KEYCHAIN_PATH}"
353
+ exit 1
354
+ fi
355
+
356
+ echo "Signing with the Developer ID (hardened runtime)"
357
+ codesign --force --deep --timestamp --options runtime \
358
+ --entitlements "${ENTITLEMENTS}" \
359
+ --keychain "${KEYCHAIN_PATH}" \
360
+ --sign "${IDENTITY}" \
361
+ "${APP}"
362
+ security delete-keychain "${KEYCHAIN_PATH}"
363
+ echo "developer_id=true" >> "${GITHUB_OUTPUT}"
364
+ else
365
+ echo "::warning::Apple certificate secrets are not set: the macOS DMG is ad-hoc signed and not notarized"
366
+ codesign --force --deep --sign - "${APP}"
367
+ echo "developer_id=false" >> "${GITHUB_OUTPUT}"
368
+ fi
369
+
370
+ codesign --verify --deep --strict --verbose=2 "${APP}"
371
+ echo "Universal executable architectures: $(lipo -archs "${APP}/Contents/MacOS/SYNCmoss")"
372
+
373
+ # On a copy, so files the test writes never reach the DMG. For a Developer
374
+ # ID build this runs under the hardened runtime, i.e. it also checks that
375
+ # the entitlements above are enough for numba.
376
+ - name: Smoke-test the signed universal2 app
377
+ run: |
378
+ set -euo pipefail
379
+ mkdir -p "${RUNNER_TEMP}/smoke"
380
+ ditto dist/universal2/SYNCmoss.app "${RUNNER_TEMP}/smoke/SYNCmoss.app"
381
+ "${RUNNER_TEMP}/smoke/SYNCmoss.app/Contents/MacOS/SYNCmoss" --test
382
+
383
+ # The drag-and-drop installer window, built as crispy's
384
+ # package/create-dmg.sh does: a writable image holding the app, a link to
385
+ # /Applications, the background picture (bundle/dmg_background.pdf) and
386
+ # the volume icon; Finder lays the window out (positions match the
387
+ # picture); then it is converted to the compressed read-only DMG.
388
+ - name: Create the DMG
389
+ run: |
390
+ set -euo pipefail
391
+ APP="dist/universal2/SYNCmoss.app"
392
+ STAGING="${RUNNER_TEMP}/dmg"
393
+ RW_DMG="${RUNNER_TEMP}/SYNCmoss-rw.dmg"
394
+ rm -rf "${STAGING}" "${RW_DMG}"
395
+
396
+ mkdir -p "${STAGING}/.background" "${STAGING}/.fseventsd"
397
+ ditto "${APP}" "${STAGING}/SYNCmoss.app"
398
+ ln -s /Applications "${STAGING}/Applications"
399
+ cp bundle/dmg_background.pdf "${STAGING}/.background/background.pdf"
400
+ # The volume shows the app's own icon.
401
+ ICON_FILE="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIconFile' "${APP}/Contents/Info.plist")"
402
+ case "${ICON_FILE}" in *.icns) ;; *) ICON_FILE="${ICON_FILE}.icns" ;; esac
403
+ cp "${APP}/Contents/Resources/${ICON_FILE}" "${STAGING}/.VolumeIcon.icns"
404
+ touch "${STAGING}/.fseventsd/no_log"
405
+
406
+ # Room for the .DS_Store Finder writes. hdiutil is occasionally
407
+ # "Resource busy" on hosted runners: retry.
408
+ SIZE_MB=$(( $(du -sm "${STAGING}" | cut -f1) + 50 ))
409
+ for attempt in 1 2 3; do
410
+ if hdiutil create -format UDRW -fs APFS -volname SYNCmoss -size "${SIZE_MB}m" \
411
+ -srcfolder "${STAGING}" -ov "${RW_DMG}"; then
412
+ break
413
+ fi
414
+ if [ "${attempt}" -eq 3 ]; then
415
+ exit 1
416
+ fi
417
+ echo "hdiutil create failed, retrying in 10 s"
418
+ sleep 10
419
+ done
420
+
421
+ ATTACH="$(hdiutil attach -readwrite -noverify -noautoopen "${RW_DMG}" | grep '^/dev/')"
422
+ DEVICE="$(echo "${ATTACH}" | head -n 1 | awk '{print $1}')"
423
+ MOUNT_POINT="$(echo "${ATTACH}" | awk -F'\t' '$3 != "" {print $3}' | tail -n 1 | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
424
+ echo "Attached ${DEVICE} at ${MOUNT_POINT}"
425
+ if [ ! -d "${MOUNT_POINT}" ]; then
426
+ echo "::error::could not find where the image was mounted"
427
+ echo "${ATTACH}"
428
+ exit 1
429
+ fi
430
+ SetFile -a C "${MOUNT_POINT}"
431
+ SetFile -a V "${MOUNT_POINT}/.background"
432
+
433
+ LAYOUT="${RUNNER_TEMP}/dmg-layout.applescript"
434
+ cat > "${LAYOUT}" <<'EOF'
435
+ on run argv
436
+ tell application "Finder"
437
+ tell disk (item 1 of argv)
438
+ open
439
+ set current view of container window to icon view
440
+ set toolbar visible of container window to false
441
+ set statusbar visible of container window to false
442
+ set bounds of container window to {200, 120, 700, 420}
443
+ set viewOptions to the icon view options of container window
444
+ set arrangement of viewOptions to not arranged
445
+ set icon size of viewOptions to 96
446
+ set background picture of viewOptions to file ".background:background.pdf"
447
+ set position of item "SYNCmoss.app" to {114, 124}
448
+ set position of item "Applications" to {386, 124}
449
+ close
450
+ open
451
+ delay 1
452
+ update without registering applications
453
+ delay 2
454
+ close
455
+ end tell
456
+ end tell
457
+ end run
458
+ EOF
459
+ # Finder scripting can time out on a busy runner: retry.
460
+ for attempt in 1 2 3; do
461
+ if osascript "${LAYOUT}" "$(basename "${MOUNT_POINT}")"; then
462
+ break
463
+ fi
464
+ if [ "${attempt}" -eq 3 ]; then
465
+ echo "::error::Finder could not lay out the DMG window"
466
+ exit 1
467
+ fi
468
+ echo "Finder layout failed, retrying in 5 s"
469
+ sleep 5
470
+ done
471
+ # Finder saves the layout in .DS_Store when the window closes.
472
+ for _ in $(seq 1 30); do
473
+ [ -f "${MOUNT_POINT}/.DS_Store" ] && break
474
+ sleep 1
475
+ done
476
+ if [ ! -f "${MOUNT_POINT}/.DS_Store" ]; then
477
+ echo "::error::Finder did not save the DMG window layout (.DS_Store)"
478
+ exit 1
479
+ fi
480
+
481
+ sync
482
+ for attempt in 1 2 3 4 5; do
483
+ if hdiutil detach "${DEVICE}"; then
484
+ break
485
+ fi
486
+ if [ "${attempt}" -eq 5 ]; then
487
+ hdiutil detach "${DEVICE}" -force
488
+ break
489
+ fi
490
+ echo "volume still busy, retrying in 5 s"
491
+ sleep 5
492
+ done
493
+ hdiutil convert "${RW_DMG}" -format UDZO -imagekey zlib-level=9 -ov -o dist/SYNCmoss.dmg
494
+ rm -f "${RW_DMG}"
495
+
496
+ - name: Notarize and staple the DMG
497
+ id: notarize
498
+ if: steps.sign.outputs.developer_id == 'true'
499
+ env:
500
+ APPLE_ID: ${{ secrets.APPLE_ID }}
501
+ APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
502
+ APPLICATION_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APPLICATION_SPECIFIC_PASSWORD }}
503
+ run: |
504
+ set -euo pipefail
505
+ if [ -z "${APPLE_ID}" ] || [ -z "${APPLE_TEAM_ID}" ] || [ -z "${APPLICATION_SPECIFIC_PASSWORD}" ]; then
506
+ echo "::warning::APPLE_ID / APPLE_TEAM_ID / APPLE_APPLICATION_SPECIFIC_PASSWORD are not set: the DMG is signed but not notarized, so Gatekeeper will still block it"
507
+ exit 0
508
+ fi
509
+ DMG="dist/SYNCmoss.dmg"
510
+
511
+ echo "Submitting ${DMG} for notarization"
512
+ RESULT="$(xcrun notarytool submit "${DMG}" \
513
+ --apple-id "${APPLE_ID}" --team-id "${APPLE_TEAM_ID}" --password "${APPLICATION_SPECIFIC_PASSWORD}" \
514
+ --wait --timeout 1h --output-format json)" || true
515
+ echo "${RESULT}"
516
+ SUBMISSION_ID="$(python -c 'import json, sys; print(json.loads(sys.argv[1]).get("id", ""))' "${RESULT}" 2> /dev/null || true)"
517
+ STATUS="$(python -c 'import json, sys; print(json.loads(sys.argv[1]).get("status", ""))' "${RESULT}" 2> /dev/null || true)"
518
+ if [ "${STATUS}" != "Accepted" ]; then
519
+ echo "::error::notarization did not succeed (status: '${STATUS:-none}')"
520
+ if [ -n "${SUBMISSION_ID}" ]; then
521
+ xcrun notarytool log "${SUBMISSION_ID}" \
522
+ --apple-id "${APPLE_ID}" --team-id "${APPLE_TEAM_ID}" --password "${APPLICATION_SPECIFIC_PASSWORD}" || true
523
+ fi
524
+ exit 1
525
+ fi
526
+
527
+ xcrun stapler staple "${DMG}"
528
+ xcrun stapler validate "${DMG}"
529
+ echo "notarized=true" >> "${GITHUB_OUTPUT}"
530
+
531
+ - name: Name the DMG
532
+ env:
533
+ NOTARIZED: ${{ steps.notarize.outputs.notarized }}
534
+ run: |
535
+ if [ "${NOTARIZED}" = "true" ]; then
536
+ mv dist/SYNCmoss.dmg dist/SYNCmoss-macOS-universal2-signed.dmg
537
+ else
538
+ mv dist/SYNCmoss.dmg dist/SYNCmoss-macOS-universal2.dmg
539
+ fi
540
+ ls -lh dist/*.dmg
541
+
542
+ - name: Upload the universal2 DMG
543
+ uses: actions/upload-artifact@v5
544
+ with:
545
+ name: macos-universal2
546
+ path: dist/SYNCmoss-macOS-universal2*.dmg
547
+ if-no-files-found: error
548
+ retention-days: 5
@@ -10,5 +10,11 @@ Third-party components are used under their respective licenses:
10
10
  - NumPy: BSD 3-Clause
11
11
  - SciPy: BSD 3-Clause
12
12
  - Matplotlib: BSD-compatible
13
+ - Numba: BSD 2-Clause
14
+ - llvmlite: BSD 2-Clause (contains LLVM: Apache 2.0 with LLVM Exceptions)
15
+ - Bliss (optional, not included; used only if installed separately):
16
+ LGPL v3.0
13
17
 
14
- For complete third-party license details, see NOTICE.txt.
18
+ For complete third-party license details, see NOTICE.txt. The Windows and
19
+ macOS bundles also carry the full license text of every third-party package
20
+ they contain, in a "licenses" folder.
@@ -0,0 +1,120 @@
1
+ SYNCmoss - Third-Party Software Notices and Information
2
+ =========================================================
3
+
4
+ This project is licensed under the MIT License. See LICENSE for details.
5
+ It uses the following third-party libraries, each distributed under
6
+ their own license terms.
7
+
8
+ The Windows and macOS bundles contain the full license text of every
9
+ third-party package they include -- the libraries below, their own
10
+ dependencies and the Python runtime -- in a "licenses" folder:
11
+ - Windows: _internal/licenses/
12
+ - macOS: SYNCmoss.app/Contents/Resources/licenses/
13
+ In the program, Supp -> License shows this notice and all of those texts.
14
+
15
+ PySide6 (Qt for Python)
16
+ -----------------------
17
+ Licensed under the GNU Lesser General Public License (LGPL) v3.0
18
+ Copyright (C) The Qt Company Ltd.
19
+ https://www.qt.io/licensing/
20
+
21
+ LGPL v3.0 COMPLIANCE NOTICE:
22
+ This software dynamically links to PySide6. Under the terms of the
23
+ LGPL v3.0, you may:
24
+ - Use this MIT-licensed software without your own code becoming LGPL.
25
+ - Replace the PySide6 library with any compatible version.
26
+ - Obtain PySide6 source code at:
27
+ https://code.qt.io/cgit/pyside/pyside-setup.git/
28
+ - Read the full LGPL v3.0 license, and the GNU GPL v3.0 it builds on,
29
+ in the bundles' licenses/LGPL-3.0.txt and licenses/GPL-3.0.txt, or at:
30
+ https://www.gnu.org/licenses/lgpl-3.0.html
31
+ (The PySide6 wheels themselves carry only a Qt commercial-license notice,
32
+ which is why the two texts are shipped separately.) Qt itself contains
33
+ third-party components (FreeType, HarfBuzz, libpng, zlib, ...) under their
34
+ own licenses; The Qt Company lists them at
35
+ https://doc.qt.io/qt-6/licenses-used-in-qt.html
36
+
37
+ NumPy
38
+ -----
39
+ Licensed under the BSD 3-Clause License
40
+ Copyright (c) 2005-2025, NumPy Developers
41
+ https://numpy.org/doc/stable/license.html
42
+
43
+ SciPy
44
+ -----
45
+ Licensed under the BSD 3-Clause License
46
+ Copyright (c) 2001-2002 Enthought, Inc. 2003, SciPy Developers
47
+ https://scipy.org/scipylib/license.html
48
+
49
+ Matplotlib
50
+ ----------
51
+ Licensed under the Matplotlib License (BSD-compatible)
52
+ Copyright (c) 2012- Matplotlib Development Team
53
+ Copyright (c) 2002-2011 John D. Hunter
54
+ https://matplotlib.org/stable/users/project/license.html
55
+
56
+ Numba
57
+ -----
58
+ Licensed under the BSD 2-Clause License
59
+ Copyright (c) 2012, Anaconda, Inc.
60
+ https://github.com/numba/numba/blob/main/LICENSE
61
+
62
+ llvmlite
63
+ --------
64
+ Licensed under the BSD 2-Clause License
65
+ Copyright (c) 2014, Anaconda, Inc.
66
+ https://github.com/numba/llvmlite/blob/main/LICENSE
67
+ It contains LLVM, licensed under the Apache License v2.0 with LLVM
68
+ Exceptions (https://llvm.org/LICENSE.txt).
69
+
70
+ Python
71
+ ------
72
+ Licensed under the Python Software Foundation License
73
+ Copyright (c) Python Software Foundation; All Rights Reserved
74
+ https://docs.python.org/3/license.html
75
+ The bundles contain the Python runtime; its LICENSE.txt is included as
76
+ licenses/Python/LICENSE.txt. The C libraries built into the runtime
77
+ (OpenSSL, expat, libffi, zlib, libmpdec, ...) and their licenses are
78
+ listed at the address above.
79
+
80
+ Microsoft Visual C++ Runtime (Windows bundle only)
81
+ --------------------------------------------------
82
+ Copyright (c) Microsoft Corporation
83
+ The Windows bundle contains Microsoft's C and C++ run-time libraries
84
+ (VCRUNTIME140*.dll, MSVCP140*.dll, VCOMP140.dll and, where present,
85
+ ucrtbase.dll with the api-ms-win-*.dll forwarders). They are "Microsoft
86
+ Distributable Code", which Microsoft allows to be redistributed unmodified
87
+ with the programs that use them, provided that whoever uses or passes on
88
+ the Windows bundle does not:
89
+ - alter any copyright, trademark or patent notice in Microsoft's
90
+ Distributable Code;
91
+ - use Microsoft's trademarks in their programs' names or in a way that
92
+ suggests the programs come from or are endorsed by Microsoft;
93
+ - distribute Microsoft's Distributable Code to run on a platform other
94
+ than Microsoft operating systems, run-time technologies or application
95
+ platforms; or
96
+ - include Microsoft Distributable Code in malicious, deceptive or
97
+ unlawful programs.
98
+ These conditions apply only to Microsoft's files, not to SYNCmoss or to
99
+ the other components. The full terms are in Microsoft's documentation
100
+ (included in its developer tools and on microsoft.com).
101
+
102
+ Bliss (optional -- NOT included)
103
+ --------------------------------
104
+ Licensed under the GNU Lesser General Public License (LGPL) v3.0
105
+ Copyright (c) Beamline Control Unit, ESRF
106
+ https://gitlab.esrf.fr/bliss/bliss
107
+ SYNCmoss neither contains nor distributes Bliss, and the Windows and macOS
108
+ bundles never include it. Only when Bliss has been installed separately
109
+ (Linux beamline installations) does SYNCmoss import it at run time, to read
110
+ spectra online from a Bliss channel.
111
+
112
+ PyInstaller (bundling only)
113
+ ---------------------------
114
+ Licensed under the GPL 2.0 with a special bootloader exception that
115
+ permits distributing non-GPL applications bundled with PyInstaller.
116
+ Copyright (c) 2010-2023, PyInstaller Development Team
117
+ https://pyinstaller.org/en/stable/license.html
118
+ The Windows and macOS bundles are created with PyInstaller. The
119
+ bootloader exception explicitly allows this use without requiring
120
+ the application itself to be GPL-licensed.