syncmoss 0.4.0__tar.gz → 0.4.1__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.
- syncmoss-0.4.1/.github/workflows/_MacOS.yml +451 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/PKG-INFO +1 -1
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library_window.py +1 -1
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/instrumental_io.py +21 -22
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/main.py +1 -1
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/model_io.py +1 -1
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/sms_theory.py +6 -1
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/supp_menu.py +5 -4
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/syncmoss_main.py +58 -15
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss.egg-info/PKG-INFO +1 -1
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss.egg-info/SOURCES.txt +1 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss.egg-info/scm_file_list.json +1 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/syncmoss_test.py +272 -3
- {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/test_instrumental_recentre.py +21 -5
- syncmoss-0.4.1/tests/test_library_dir.py +239 -0
- syncmoss-0.4.0/.github/workflows/_MacOS.yml +0 -329
- {syncmoss-0.4.0 → syncmoss-0.4.1}/.github/workflows/Tests.yml +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/.github/workflows/_PyPI.yml +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/.github/workflows/_TagRelease.yml +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/.github/workflows/_Tests.yml +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/.github/workflows/_UploadRelease.yml +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/.github/workflows/_Version.yml +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/.github/workflows/_Wheels.yml +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/.github/workflows/_WindowsExe.yml +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/.github/workflows/release.yml +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/.gitignore +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/.vscode/settings.json +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/COPYING.txt +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/LICENSE +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/Lib_test/test.mdl +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/NOTICE.txt +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/README.md +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/bundle/MacOS.spec +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/bundle/Windows.spec +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/bundle/bundle.py +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/bundle/macos_bundle.py +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/docs/thick_models.md +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/docs/thick_vs_thin.md +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/pyproject.toml +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/requirements.txt +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/setup.cfg +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Calibration.py +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Hamiltonian_helper.py +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Aegirine.mdl +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Akaganeite.mdl +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Almandine.mdl +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Antitaenite.mdl +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Arsenopyrite.mdl +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Bornite.mdl +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Bridgmanite (Fe,Al-bearing) @100 GPa.mdl +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Bridgmanite (Fe,Al-bearing).mdl +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Celadonite.mdl +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Cementite.mdl +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Chalcopyrite.mdl +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Clinopyroxene (augite).mdl +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Copiapite.mdl +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Cubanite.mdl +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Epidote.mdl +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Fayalite (Fe-olivine).mdl +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Ferrihydrite (2-line).mdl +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Ferropericlase.mdl +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Goethite.mdl +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Green rust (fougerite, carbonate).mdl +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Haegg carbide.mdl +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Hedenbergite.mdl +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Hematite.mdl +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Heterosite (FePO4).mdl +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Ilmenite.mdl +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Iron phosphide.mdl +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Isocubanite.mdl +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Jarosite.mdl +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Kamacite.mdl +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Lepidocrocite.mdl +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Maghemite.mdl +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Magnetite.mdl +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Marcasite.mdl +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Melanterite.mdl +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Orthopyroxene (enstatite-ferrosilite).mdl +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Prussian blue (ferric ferrocyanide).mdl +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Pyrite.mdl +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Pyrrhotite.mdl +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Rhomboclase.mdl +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Roemerite.mdl +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Siderite.mdl +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Szomolnokite.mdl +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Taenite.mdl +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Talnakhite.mdl +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Tetrataenite.mdl +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Triphylite (LiFePO4).mdl +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Troilite.mdl +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Vivianite.mdl +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Voltaite.mdl +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/alpha-Fe (metallic iron, bcc).mdl +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library_io.py +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/constants.py +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/fitting_io.py +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/icons/CheckBox.png +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/icons/CheckBox_.png +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/icons/CheckBox_L.png +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/icons/CheckBox_L2.png +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/icons/DU.png +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/icons/Switch.png +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/icons/UD.png +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/icons/icon_r.ico +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/instrumental_window.py +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/legacy.py +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/minimi_lib.py +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/models.py +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/models_description_window.py +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/models_positions.py +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/parameters/ABSorber3.txt +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/parameters/Be.txt +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/parameters/Calibration.dat +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/parameters/GCMS.txt +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/parameters/INS_APS.txt +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/parameters/INSexp.txt +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/parameters/INSint.txt +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/parameters/INSth.txt +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/parameters/KB.txt +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/parameters/NFS.txt +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/parameters/calibr.png +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/parameters/help.md +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/parameters/models_description.md +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/parameters_table.py +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/results_table.py +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/spectrum_io.py +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/spectrum_plotter.py +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/support_math.py +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/test_graf.txt +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/test_param.txt +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/theme_dark.json +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/theme_light.json +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss.egg-info/dependency_links.txt +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss.egg-info/entry_points.txt +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss.egg-info/requires.txt +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss.egg-info/top_level.txt +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/conftest.py +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/data/Distr_corr.mdl +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/data/Distr_distr.mdl +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/data/alpha_fe_sms_000.mca +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/data/calibration_000_golden.json +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/data/generate_cal_fixtures.py +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/data/synthetic_alpha_fe_cms_linear.mca +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/golden_models.json +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/test_calibration.py +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/test_calibration_helpers.py +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/test_calibration_path.py +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/test_distr_composition.py +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/test_distr_expansion.py +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/test_distr_mdl.py +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/test_distr_targets.py +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/test_expression_validation.py +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/test_golden_models.py +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/test_gui_smoke.py +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/test_hamiltonian_texture.py +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/test_instrumental_metadata.py +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/test_instrumental_method_choice.py +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/test_instrumental_window.py +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/test_integral_points.py +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/test_legacy.py +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/test_library_io.py +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/test_model_append.py +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/test_model_io.py +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/test_model_only_mode.py +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/test_nbaseline_results.py +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/test_parameter_links.py +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/test_polarization.py +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/test_recon_composition.py +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/test_recon_mdl.py +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/test_recon_results.py +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/test_reference_shift.py +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/test_sdw_grid.py +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/test_search_guards.py +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/test_sms_theory.py +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/test_support_math.py +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/test_texture_s1.py +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/test_theory_search.py +0 -0
- {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/test_thick_gui.py +0 -0
|
@@ -0,0 +1,451 @@
|
|
|
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 DMG, notarize, staple.
|
|
7
|
+
#
|
|
8
|
+
# Signing and notarization run only when the Apple secrets are set (repository
|
|
9
|
+
# Settings -> Secrets and variables -> Actions; release.yml forwards them).
|
|
10
|
+
# Without them the job still ships a universal2 DMG, ad-hoc signed and not
|
|
11
|
+
# notarized, named SYNCmoss-macOS-universal2.dmg. A notarized one is named
|
|
12
|
+
# SYNCmoss-macOS-universal2-signed.dmg; _TagRelease.yml / _UploadRelease.yml
|
|
13
|
+
# tell the two apart by that name.
|
|
14
|
+
#
|
|
15
|
+
# APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_BASE64
|
|
16
|
+
# the "Developer ID Application" certificate WITH its private key,
|
|
17
|
+
# exported from Keychain Access as .p12, then: base64 -i cert.p12 | pbcopy
|
|
18
|
+
# APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_PASSWORD password of that .p12
|
|
19
|
+
# APPLE_ID Apple Account e-mail of the developer
|
|
20
|
+
# APPLE_TEAM_ID 10-character Team ID (developer.apple.com -> Membership)
|
|
21
|
+
# APPLE_APPLICATION_SPECIFIC_PASSWORD app-specific password (account.apple.com -> Sign-In and Security)
|
|
22
|
+
# APPLE_KEYCHAIN_PASSWORD optional: password of the throwaway CI keychain
|
|
23
|
+
# (a random one is used when unset)
|
|
24
|
+
|
|
25
|
+
on:
|
|
26
|
+
workflow_call:
|
|
27
|
+
secrets:
|
|
28
|
+
APPLE_ID:
|
|
29
|
+
required: false
|
|
30
|
+
APPLE_TEAM_ID:
|
|
31
|
+
required: false
|
|
32
|
+
APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_PASSWORD:
|
|
33
|
+
required: false
|
|
34
|
+
APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_BASE64:
|
|
35
|
+
required: false
|
|
36
|
+
APPLE_KEYCHAIN_PASSWORD:
|
|
37
|
+
required: false
|
|
38
|
+
APPLE_APPLICATION_SPECIFIC_PASSWORD:
|
|
39
|
+
required: false
|
|
40
|
+
|
|
41
|
+
jobs:
|
|
42
|
+
build:
|
|
43
|
+
name: Build macOS app (${{ matrix.arch }})
|
|
44
|
+
runs-on: ${{ matrix.runner }}
|
|
45
|
+
timeout-minutes: 60
|
|
46
|
+
strategy:
|
|
47
|
+
fail-fast: false
|
|
48
|
+
matrix:
|
|
49
|
+
include:
|
|
50
|
+
- runner: macos-15
|
|
51
|
+
arch: arm64
|
|
52
|
+
- runner: macos-15-intel
|
|
53
|
+
arch: x86_64
|
|
54
|
+
env:
|
|
55
|
+
MACOSX_DEPLOYMENT_TARGET: "11"
|
|
56
|
+
CFLAGS: "-mmacosx-version-min=11"
|
|
57
|
+
LDFLAGS: "-mmacosx-version-min=11"
|
|
58
|
+
steps:
|
|
59
|
+
- uses: actions/checkout@v6
|
|
60
|
+
|
|
61
|
+
- uses: actions/setup-python@v6
|
|
62
|
+
with:
|
|
63
|
+
python-version: "3.13"
|
|
64
|
+
cache: "pip"
|
|
65
|
+
|
|
66
|
+
- name: Check the runner architecture
|
|
67
|
+
run: |
|
|
68
|
+
MACHINE="$(python -c 'import platform; print(platform.machine())')"
|
|
69
|
+
echo "Python runs as ${MACHINE}"
|
|
70
|
+
if [ "${MACHINE}" != "${{ matrix.arch }}" ]; then
|
|
71
|
+
echo "::error::expected a ${{ matrix.arch }} runner, got ${MACHINE}"
|
|
72
|
+
exit 1
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
- name: Download wheel artifact
|
|
76
|
+
uses: actions/download-artifact@v6
|
|
77
|
+
with:
|
|
78
|
+
name: wheels
|
|
79
|
+
path: wheels
|
|
80
|
+
|
|
81
|
+
# numba 0.62.x (llvmlite 0.45.x) is the last release with macOS x86_64
|
|
82
|
+
# wheels. The pin applies to BOTH architectures: the universal2 app holds
|
|
83
|
+
# one copy of the pure-Python code, so the two builds must resolve the
|
|
84
|
+
# same versions (checked again before the merge). --only-binary stops pip
|
|
85
|
+
# from quietly building an sdist on one architecture only.
|
|
86
|
+
- name: Install dependencies
|
|
87
|
+
run: |
|
|
88
|
+
python -m pip install --upgrade pip setuptools wheel
|
|
89
|
+
python -m pip install --only-binary=:all: "numba==0.62.1" -r requirements.txt pyinstaller wheels/*.whl
|
|
90
|
+
python -m pip list --format=freeze
|
|
91
|
+
|
|
92
|
+
- name: Build the app
|
|
93
|
+
run: python bundle/macos_bundle.py --dist-dir bundle/MacOS/${{ matrix.arch }} --work-dir bundle/build/MacOS/${{ matrix.arch }}
|
|
94
|
+
|
|
95
|
+
# tar keeps the symlinks (Qt frameworks) and executable bits that
|
|
96
|
+
# upload-artifact would drop.
|
|
97
|
+
- name: Archive the app bundle
|
|
98
|
+
run: |
|
|
99
|
+
set -euo pipefail
|
|
100
|
+
DIST="bundle/MacOS/${{ matrix.arch }}"
|
|
101
|
+
APP_PATH=""
|
|
102
|
+
for candidate in "${DIST}/SYNCmoss.app" "${DIST}/SYNCmoss/SYNCmoss.app"; do
|
|
103
|
+
if [ -d "${candidate}" ]; then
|
|
104
|
+
APP_PATH="${candidate}"
|
|
105
|
+
break
|
|
106
|
+
fi
|
|
107
|
+
done
|
|
108
|
+
if [ -z "${APP_PATH}" ]; then
|
|
109
|
+
echo "::error::SYNCmoss.app not found in the ${{ matrix.arch }} build output"
|
|
110
|
+
find "${DIST}" -maxdepth 3 -print
|
|
111
|
+
exit 1
|
|
112
|
+
fi
|
|
113
|
+
echo "Executable architectures: $(lipo -archs "${APP_PATH}/Contents/MacOS/SYNCmoss")"
|
|
114
|
+
mkdir -p macos-artifact
|
|
115
|
+
tar -cpzf macos-artifact/SYNCmoss.app.tar.gz -C "$(dirname "${APP_PATH}")" SYNCmoss.app
|
|
116
|
+
python -m pip list --format=freeze > macos-artifact/pip-freeze.txt
|
|
117
|
+
echo "APP_PATH=${APP_PATH}" >> "${GITHUB_ENV}"
|
|
118
|
+
|
|
119
|
+
- name: Upload the app bundle
|
|
120
|
+
uses: actions/upload-artifact@v5
|
|
121
|
+
with:
|
|
122
|
+
name: macos-app-${{ matrix.arch }}
|
|
123
|
+
path: macos-artifact/
|
|
124
|
+
if-no-files-found: error
|
|
125
|
+
retention-days: 1
|
|
126
|
+
|
|
127
|
+
# Runs each architecture natively (the x86_64 slice is never exercised
|
|
128
|
+
# later). After archiving, so nothing the test writes reaches the artifact.
|
|
129
|
+
- name: Smoke-test the frozen app
|
|
130
|
+
run: |
|
|
131
|
+
"${APP_PATH}/Contents/MacOS/SYNCmoss" --test
|
|
132
|
+
|
|
133
|
+
universal2:
|
|
134
|
+
name: Merge, sign and notarize the universal2 app
|
|
135
|
+
needs: build
|
|
136
|
+
runs-on: macos-15
|
|
137
|
+
timeout-minutes: 90
|
|
138
|
+
steps:
|
|
139
|
+
- uses: actions/setup-python@v6
|
|
140
|
+
with:
|
|
141
|
+
python-version: "3.13"
|
|
142
|
+
|
|
143
|
+
- name: Download the arm64 app
|
|
144
|
+
uses: actions/download-artifact@v6
|
|
145
|
+
with:
|
|
146
|
+
name: macos-app-arm64
|
|
147
|
+
path: bundles/arm64
|
|
148
|
+
|
|
149
|
+
- name: Download the x86_64 app
|
|
150
|
+
uses: actions/download-artifact@v6
|
|
151
|
+
with:
|
|
152
|
+
name: macos-app-x86_64
|
|
153
|
+
path: bundles/x86_64
|
|
154
|
+
|
|
155
|
+
- name: Check both builds used the same packages
|
|
156
|
+
run: |
|
|
157
|
+
if ! diff -u bundles/arm64/pip-freeze.txt bundles/x86_64/pip-freeze.txt; then
|
|
158
|
+
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"
|
|
159
|
+
exit 1
|
|
160
|
+
fi
|
|
161
|
+
|
|
162
|
+
- name: Extract the app bundles
|
|
163
|
+
run: |
|
|
164
|
+
set -euo pipefail
|
|
165
|
+
mkdir -p arm64 x86_64
|
|
166
|
+
tar -xpf bundles/arm64/SYNCmoss.app.tar.gz -C arm64
|
|
167
|
+
tar -xpf bundles/x86_64/SYNCmoss.app.tar.gz -C x86_64
|
|
168
|
+
|
|
169
|
+
# Adapted from crispy's package/merge-universal.py: walk both bundles,
|
|
170
|
+
# fuse every pair of thin Mach-O files with lipo, copy everything else
|
|
171
|
+
# (identical files, already-universal binaries, data files, symlinks),
|
|
172
|
+
# then require every Mach-O file of the result to carry both slices.
|
|
173
|
+
- name: Merge into a universal2 app
|
|
174
|
+
shell: python
|
|
175
|
+
run: |
|
|
176
|
+
import filecmp
|
|
177
|
+
import os
|
|
178
|
+
import shutil
|
|
179
|
+
import subprocess
|
|
180
|
+
import sys
|
|
181
|
+
|
|
182
|
+
ARM_APP = "arm64/SYNCmoss.app"
|
|
183
|
+
X86_APP = "x86_64/SYNCmoss.app"
|
|
184
|
+
OUT_APP = "dist/universal2/SYNCmoss.app"
|
|
185
|
+
|
|
186
|
+
THIN_MAGICS = {b"\xfe\xed\xfa\xce", b"\xfe\xed\xfa\xcf", b"\xce\xfa\xed\xfe", b"\xcf\xfa\xed\xfe"}
|
|
187
|
+
FAT_MAGICS = {b"\xca\xfe\xba\xbe", b"\xbe\xba\xfe\xca"}
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def macho_kind(path):
|
|
191
|
+
"""'thin', 'fat' or None, from the magic bytes: PyInstaller ships
|
|
192
|
+
extension-less Mach-O files. Java .class files share the fat magic,
|
|
193
|
+
but a real universal binary has only a few architectures."""
|
|
194
|
+
with open(path, "rb") as f:
|
|
195
|
+
header = f.read(8)
|
|
196
|
+
if len(header) < 8:
|
|
197
|
+
return None
|
|
198
|
+
if header[:4] in THIN_MAGICS:
|
|
199
|
+
return "thin"
|
|
200
|
+
if header[:4] in FAT_MAGICS:
|
|
201
|
+
byteorder = "big" if header[:4] == b"\xca\xfe\xba\xbe" else "little"
|
|
202
|
+
return "fat" if int.from_bytes(header[4:8], byteorder) < 30 else None
|
|
203
|
+
return None
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def collect(root):
|
|
207
|
+
"""Relative path -> 'dir', 'symlink' or 'file' (symlinked dirs are not descended)."""
|
|
208
|
+
entries = {}
|
|
209
|
+
for current, dirnames, filenames in os.walk(root):
|
|
210
|
+
for name in dirnames + filenames:
|
|
211
|
+
path = os.path.join(current, name)
|
|
212
|
+
if os.path.islink(path):
|
|
213
|
+
kind = "symlink"
|
|
214
|
+
elif os.path.isdir(path):
|
|
215
|
+
kind = "dir"
|
|
216
|
+
else:
|
|
217
|
+
kind = "file"
|
|
218
|
+
entries[os.path.relpath(path, root)] = kind
|
|
219
|
+
return entries
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
arm = collect(ARM_APP)
|
|
223
|
+
x86 = collect(X86_APP)
|
|
224
|
+
os.makedirs(OUT_APP)
|
|
225
|
+
single_arch = []
|
|
226
|
+
fused = 0
|
|
227
|
+
|
|
228
|
+
for rel in sorted(set(arm) | set(x86)):
|
|
229
|
+
arm_kind, x86_kind = arm.get(rel), x86.get(rel)
|
|
230
|
+
arm_path = os.path.join(ARM_APP, rel)
|
|
231
|
+
x86_path = os.path.join(X86_APP, rel)
|
|
232
|
+
out_path = os.path.join(OUT_APP, rel)
|
|
233
|
+
|
|
234
|
+
if "dir" in (arm_kind, x86_kind):
|
|
235
|
+
os.makedirs(out_path, exist_ok=True)
|
|
236
|
+
continue
|
|
237
|
+
os.makedirs(os.path.dirname(out_path), exist_ok=True)
|
|
238
|
+
|
|
239
|
+
if "symlink" in (arm_kind, x86_kind):
|
|
240
|
+
if arm_kind == x86_kind and os.readlink(arm_path) != os.readlink(x86_path):
|
|
241
|
+
print(f"WARNING: symlink target differs, using arm64: {rel}")
|
|
242
|
+
os.symlink(os.readlink(arm_path if arm_kind == "symlink" else x86_path), out_path)
|
|
243
|
+
continue
|
|
244
|
+
|
|
245
|
+
if arm_kind is None or x86_kind is None:
|
|
246
|
+
present = arm_path if arm_kind else x86_path
|
|
247
|
+
if macho_kind(present):
|
|
248
|
+
single_arch.append(rel)
|
|
249
|
+
else:
|
|
250
|
+
print(f"WARNING: data file present in one build only: {rel}")
|
|
251
|
+
shutil.copy2(present, out_path)
|
|
252
|
+
continue
|
|
253
|
+
|
|
254
|
+
if filecmp.cmp(arm_path, x86_path, shallow=False):
|
|
255
|
+
shutil.copy2(arm_path, out_path)
|
|
256
|
+
continue
|
|
257
|
+
|
|
258
|
+
arm_macho, x86_macho = macho_kind(arm_path), macho_kind(x86_path)
|
|
259
|
+
if arm_macho == x86_macho == "thin":
|
|
260
|
+
subprocess.run(["lipo", "-create", arm_path, x86_path, "-output", out_path], check=True)
|
|
261
|
+
shutil.copymode(arm_path, out_path)
|
|
262
|
+
fused += 1
|
|
263
|
+
elif "fat" in (arm_macho, x86_macho):
|
|
264
|
+
# Already universal; the two copies differ only in their signature.
|
|
265
|
+
shutil.copy2(arm_path if arm_macho == "fat" else x86_path, out_path)
|
|
266
|
+
elif arm_macho or x86_macho:
|
|
267
|
+
single_arch.append(rel)
|
|
268
|
+
else:
|
|
269
|
+
print(f"WARNING: data file differs, using arm64: {rel}")
|
|
270
|
+
shutil.copy2(arm_path, out_path)
|
|
271
|
+
|
|
272
|
+
if single_arch:
|
|
273
|
+
sys.exit(
|
|
274
|
+
"ERROR: these Mach-O binaries exist for one architecture only, so the "
|
|
275
|
+
"app would crash on the other:\n" + "\n".join(f" - {p}" for p in single_arch)
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
not_universal = []
|
|
279
|
+
checked = 0
|
|
280
|
+
for rel, kind in sorted(collect(OUT_APP).items()):
|
|
281
|
+
path = os.path.join(OUT_APP, rel)
|
|
282
|
+
if kind != "file" or not macho_kind(path):
|
|
283
|
+
continue
|
|
284
|
+
checked += 1
|
|
285
|
+
archs = subprocess.run(["lipo", "-archs", path], capture_output=True, text=True).stdout.split()
|
|
286
|
+
if not {"arm64", "x86_64"} <= set(archs):
|
|
287
|
+
not_universal.append(f" - {rel}: {' '.join(archs) or 'unreadable'}")
|
|
288
|
+
|
|
289
|
+
print(f"Fused {fused} binaries, checked {checked} Mach-O files")
|
|
290
|
+
if not_universal:
|
|
291
|
+
sys.exit("ERROR: these binaries are not universal2:\n" + "\n".join(not_universal))
|
|
292
|
+
print(f"Universal2 app written to {OUT_APP}")
|
|
293
|
+
|
|
294
|
+
# The merge leaves the bundle's seal stale, so the app is always re-signed:
|
|
295
|
+
# with the Developer ID when the certificate secrets are set, otherwise
|
|
296
|
+
# ad-hoc (so macOS reports "unverified developer", not "damaged").
|
|
297
|
+
- name: Sign the app
|
|
298
|
+
id: sign
|
|
299
|
+
env:
|
|
300
|
+
CERTIFICATE_BASE64: ${{ secrets.APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_BASE64 }}
|
|
301
|
+
CERTIFICATE_PASSWORD: ${{ secrets.APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_PASSWORD }}
|
|
302
|
+
KEYCHAIN_PASSWORD: ${{ secrets.APPLE_KEYCHAIN_PASSWORD }}
|
|
303
|
+
run: |
|
|
304
|
+
set -euo pipefail
|
|
305
|
+
APP="dist/universal2/SYNCmoss.app"
|
|
306
|
+
|
|
307
|
+
# The hardened runtime needs these for the numba/llvmlite JIT (writable
|
|
308
|
+
# + executable memory) and for loading the bundled Python extensions.
|
|
309
|
+
ENTITLEMENTS="${RUNNER_TEMP}/entitlements.plist"
|
|
310
|
+
cat > "${ENTITLEMENTS}" <<'EOF'
|
|
311
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
312
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
313
|
+
<plist version="1.0">
|
|
314
|
+
<dict>
|
|
315
|
+
<key>com.apple.security.cs.allow-jit</key>
|
|
316
|
+
<true/>
|
|
317
|
+
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
|
318
|
+
<true/>
|
|
319
|
+
<key>com.apple.security.cs.disable-library-validation</key>
|
|
320
|
+
<true/>
|
|
321
|
+
</dict>
|
|
322
|
+
</plist>
|
|
323
|
+
EOF
|
|
324
|
+
|
|
325
|
+
# Extended attributes would invalidate the signature.
|
|
326
|
+
xattr -cr "${APP}"
|
|
327
|
+
|
|
328
|
+
if [ -n "${CERTIFICATE_BASE64}" ] && [ -n "${CERTIFICATE_PASSWORD}" ]; then
|
|
329
|
+
KEYCHAIN_PATH="${RUNNER_TEMP}/signing.keychain-db"
|
|
330
|
+
CERTIFICATE_PATH="${RUNNER_TEMP}/certificate.p12"
|
|
331
|
+
if [ -z "${KEYCHAIN_PASSWORD}" ]; then
|
|
332
|
+
KEYCHAIN_PASSWORD="$(openssl rand -base64 24)"
|
|
333
|
+
echo "::add-mask::${KEYCHAIN_PASSWORD}"
|
|
334
|
+
fi
|
|
335
|
+
|
|
336
|
+
echo "Creating a temporary keychain and importing the certificate"
|
|
337
|
+
echo "${CERTIFICATE_BASE64}" | base64 --decode -o "${CERTIFICATE_PATH}"
|
|
338
|
+
security create-keychain -p "${KEYCHAIN_PASSWORD}" "${KEYCHAIN_PATH}"
|
|
339
|
+
security set-keychain-settings -lut 21600 "${KEYCHAIN_PATH}"
|
|
340
|
+
security unlock-keychain -p "${KEYCHAIN_PASSWORD}" "${KEYCHAIN_PATH}"
|
|
341
|
+
security import "${CERTIFICATE_PATH}" -P "${CERTIFICATE_PASSWORD}" -A -t cert -f pkcs12 -k "${KEYCHAIN_PATH}"
|
|
342
|
+
rm -f "${CERTIFICATE_PATH}"
|
|
343
|
+
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "${KEYCHAIN_PASSWORD}" "${KEYCHAIN_PATH}" > /dev/null
|
|
344
|
+
security list-keychains -d user -s "${KEYCHAIN_PATH}"
|
|
345
|
+
|
|
346
|
+
IDENTITY="$(security find-identity -v -p codesigning "${KEYCHAIN_PATH}" | awk '/"Developer ID Application/ && !found { print $2; found = 1 }')"
|
|
347
|
+
if [ -z "${IDENTITY}" ]; then
|
|
348
|
+
echo "::error::the certificate holds no 'Developer ID Application' identity"
|
|
349
|
+
security find-identity -v -p codesigning "${KEYCHAIN_PATH}"
|
|
350
|
+
exit 1
|
|
351
|
+
fi
|
|
352
|
+
|
|
353
|
+
echo "Signing with the Developer ID (hardened runtime)"
|
|
354
|
+
codesign --force --deep --timestamp --options runtime \
|
|
355
|
+
--entitlements "${ENTITLEMENTS}" \
|
|
356
|
+
--keychain "${KEYCHAIN_PATH}" \
|
|
357
|
+
--sign "${IDENTITY}" \
|
|
358
|
+
"${APP}"
|
|
359
|
+
security delete-keychain "${KEYCHAIN_PATH}"
|
|
360
|
+
echo "developer_id=true" >> "${GITHUB_OUTPUT}"
|
|
361
|
+
else
|
|
362
|
+
echo "::warning::Apple certificate secrets are not set: the macOS DMG is ad-hoc signed and not notarized"
|
|
363
|
+
codesign --force --deep --sign - "${APP}"
|
|
364
|
+
echo "developer_id=false" >> "${GITHUB_OUTPUT}"
|
|
365
|
+
fi
|
|
366
|
+
|
|
367
|
+
codesign --verify --deep --strict --verbose=2 "${APP}"
|
|
368
|
+
echo "Universal executable architectures: $(lipo -archs "${APP}/Contents/MacOS/SYNCmoss")"
|
|
369
|
+
|
|
370
|
+
# On a copy, so files the test writes never reach the DMG. For a Developer
|
|
371
|
+
# ID build this runs under the hardened runtime, i.e. it also checks that
|
|
372
|
+
# the entitlements above are enough for numba.
|
|
373
|
+
- name: Smoke-test the signed universal2 app
|
|
374
|
+
run: |
|
|
375
|
+
set -euo pipefail
|
|
376
|
+
mkdir -p "${RUNNER_TEMP}/smoke"
|
|
377
|
+
ditto dist/universal2/SYNCmoss.app "${RUNNER_TEMP}/smoke/SYNCmoss.app"
|
|
378
|
+
"${RUNNER_TEMP}/smoke/SYNCmoss.app/Contents/MacOS/SYNCmoss" --test
|
|
379
|
+
|
|
380
|
+
- name: Create the DMG
|
|
381
|
+
run: |
|
|
382
|
+
set -euo pipefail
|
|
383
|
+
STAGING="${RUNNER_TEMP}/dmg"
|
|
384
|
+
mkdir -p "${STAGING}"
|
|
385
|
+
ditto dist/universal2/SYNCmoss.app "${STAGING}/SYNCmoss.app"
|
|
386
|
+
ln -s /Applications "${STAGING}/Applications"
|
|
387
|
+
# hdiutil is occasionally "Resource busy" on hosted runners: retry.
|
|
388
|
+
for attempt in 1 2 3; do
|
|
389
|
+
if hdiutil create -volname SYNCmoss -srcfolder "${STAGING}" -ov -format UDZO -imagekey zlib-level=9 dist/SYNCmoss.dmg; then
|
|
390
|
+
break
|
|
391
|
+
fi
|
|
392
|
+
if [ "${attempt}" -eq 3 ]; then
|
|
393
|
+
exit 1
|
|
394
|
+
fi
|
|
395
|
+
echo "hdiutil failed, retrying in 10 s"
|
|
396
|
+
sleep 10
|
|
397
|
+
done
|
|
398
|
+
|
|
399
|
+
- name: Notarize and staple the DMG
|
|
400
|
+
id: notarize
|
|
401
|
+
if: steps.sign.outputs.developer_id == 'true'
|
|
402
|
+
env:
|
|
403
|
+
APPLE_ID: ${{ secrets.APPLE_ID }}
|
|
404
|
+
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
|
405
|
+
APPLICATION_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APPLICATION_SPECIFIC_PASSWORD }}
|
|
406
|
+
run: |
|
|
407
|
+
set -euo pipefail
|
|
408
|
+
if [ -z "${APPLE_ID}" ] || [ -z "${APPLE_TEAM_ID}" ] || [ -z "${APPLICATION_SPECIFIC_PASSWORD}" ]; then
|
|
409
|
+
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"
|
|
410
|
+
exit 0
|
|
411
|
+
fi
|
|
412
|
+
DMG="dist/SYNCmoss.dmg"
|
|
413
|
+
|
|
414
|
+
echo "Submitting ${DMG} for notarization"
|
|
415
|
+
RESULT="$(xcrun notarytool submit "${DMG}" \
|
|
416
|
+
--apple-id "${APPLE_ID}" --team-id "${APPLE_TEAM_ID}" --password "${APPLICATION_SPECIFIC_PASSWORD}" \
|
|
417
|
+
--wait --timeout 1h --output-format json)" || true
|
|
418
|
+
echo "${RESULT}"
|
|
419
|
+
SUBMISSION_ID="$(python -c 'import json, sys; print(json.loads(sys.argv[1]).get("id", ""))' "${RESULT}" 2> /dev/null || true)"
|
|
420
|
+
STATUS="$(python -c 'import json, sys; print(json.loads(sys.argv[1]).get("status", ""))' "${RESULT}" 2> /dev/null || true)"
|
|
421
|
+
if [ "${STATUS}" != "Accepted" ]; then
|
|
422
|
+
echo "::error::notarization did not succeed (status: '${STATUS:-none}')"
|
|
423
|
+
if [ -n "${SUBMISSION_ID}" ]; then
|
|
424
|
+
xcrun notarytool log "${SUBMISSION_ID}" \
|
|
425
|
+
--apple-id "${APPLE_ID}" --team-id "${APPLE_TEAM_ID}" --password "${APPLICATION_SPECIFIC_PASSWORD}" || true
|
|
426
|
+
fi
|
|
427
|
+
exit 1
|
|
428
|
+
fi
|
|
429
|
+
|
|
430
|
+
xcrun stapler staple "${DMG}"
|
|
431
|
+
xcrun stapler validate "${DMG}"
|
|
432
|
+
echo "notarized=true" >> "${GITHUB_OUTPUT}"
|
|
433
|
+
|
|
434
|
+
- name: Name the DMG
|
|
435
|
+
env:
|
|
436
|
+
NOTARIZED: ${{ steps.notarize.outputs.notarized }}
|
|
437
|
+
run: |
|
|
438
|
+
if [ "${NOTARIZED}" = "true" ]; then
|
|
439
|
+
mv dist/SYNCmoss.dmg dist/SYNCmoss-macOS-universal2-signed.dmg
|
|
440
|
+
else
|
|
441
|
+
mv dist/SYNCmoss.dmg dist/SYNCmoss-macOS-universal2.dmg
|
|
442
|
+
fi
|
|
443
|
+
ls -lh dist/*.dmg
|
|
444
|
+
|
|
445
|
+
- name: Upload the universal2 DMG
|
|
446
|
+
uses: actions/upload-artifact@v5
|
|
447
|
+
with:
|
|
448
|
+
name: macos-universal2
|
|
449
|
+
path: dist/SYNCmoss-macOS-universal2*.dmg
|
|
450
|
+
if-no-files-found: error
|
|
451
|
+
retention-days: 5
|
|
@@ -119,7 +119,7 @@ def save_to_library_via_dialog(main_window):
|
|
|
119
119
|
|
|
120
120
|
def open_library_model_dialog(main_window, parent_widget, insert_row, model_options=None):
|
|
121
121
|
"""Open internal Library browser with filters and detailed preview."""
|
|
122
|
-
library_dir =
|
|
122
|
+
library_dir = main_window.library_dir
|
|
123
123
|
if not os.path.isdir(library_dir):
|
|
124
124
|
main_window.set_status(f"Library folder not found: {library_dir}", "red")
|
|
125
125
|
return
|
|
@@ -1258,40 +1258,39 @@ THEORY_FREE_FIELDS = ('theta_urad', 'B_s', 'dEQ', 'shift',
|
|
|
1258
1258
|
# over (0.19-2.4 T across the published temperature series), i.e. T ~ 75.93 C by
|
|
1259
1259
|
# the calibrated law.
|
|
1260
1260
|
#
|
|
1261
|
-
# dEQ = -0.
|
|
1262
|
-
#
|
|
1263
|
-
#
|
|
1264
|
-
# this parametrisation, 6 % away.
|
|
1261
|
+
# dEQ = -0.4216 +- 0.0038 mm/s (2026-09-29 global fit of the 83-spectrum ESRF
|
|
1262
|
+
# series: 123 parameters, shape chi2 1.317, and the independent intensity
|
|
1263
|
+
# cross-check agreeing to 0.91-1.05 across all nine temperatures).
|
|
1265
1264
|
#
|
|
1266
|
-
#
|
|
1267
|
-
#
|
|
1268
|
-
#
|
|
1269
|
-
#
|
|
1270
|
-
#
|
|
1271
|
-
#
|
|
1272
|
-
# because it was measured to improve anything (see sms_study/test_flm_refine.py,
|
|
1273
|
-
# where the data prefer 0.55-0.65).
|
|
1265
|
+
# QUOTE THE ERROR AS +-0.03, NOT +-0.004. dEQ tracks how much freedom the model
|
|
1266
|
+
# is given rather than converging on a value: -0.3228, then -0.4056, then
|
|
1267
|
+
# -0.3900, now -0.4216, each time the source model changed. The +-0.0038 is the
|
|
1268
|
+
# covariance of one fit; the spread ACROSS defensible fits is ten times that. So
|
|
1269
|
+
# the laboratory value of Lyubutin et al. (2022), 2q = -0.3815 mm/s in this
|
|
1270
|
+
# parametrisation, is consistent with this one -- do not report a disagreement.
|
|
1274
1271
|
#
|
|
1275
|
-
#
|
|
1276
|
-
#
|
|
1272
|
+
# f_LM is the opposite and IS well determined: 0.7718, 0.7661, 0.7714, 0.7711
|
|
1273
|
+
# across those same four models. It is pinned by the width of the saturated core
|
|
1274
|
+
# (see the dynamical-saturation argument in sms_theory), which the model gets
|
|
1275
|
+
# right, while dEQ trades against whatever is absorbing line asymmetry.
|
|
1277
1276
|
#
|
|
1278
1277
|
# mosaic_urad starts at ZERO: the angle deviation is off by default (see
|
|
1279
1278
|
# THEORY_PASSES), and setting_urad = 3 urad is applied regardless, so the source
|
|
1280
|
-
# is never completely unsmeared.
|
|
1281
|
-
#
|
|
1279
|
+
# is never completely unsmeared. Released only at the rocking minimum, where the
|
|
1280
|
+
# 83-spectrum fit puts it at 20-28 urad (it is a property of one measurement's
|
|
1281
|
+
# setting, not a constant -- so this is a start, not a value).
|
|
1282
1282
|
#
|
|
1283
1283
|
# theta and B_s are the OPERATING POINT: spectrum 008 of the ESRF series
|
|
1284
1284
|
# (T = 75.825 C, the angle the source is actually used at), refitted under these
|
|
1285
1285
|
# very conventions -- setting_urad = 3, n_angle = 11 -- at chi2 = 0.97. Starting
|
|
1286
1286
|
# a search at a guessed point instead is what the B_s scan exists to repair.
|
|
1287
1287
|
#
|
|
1288
|
-
# dEQ, gauss_fwhm and f_LM come from the GLOBAL
|
|
1289
|
-
#
|
|
1290
|
-
#
|
|
1291
|
-
|
|
1292
|
-
THEORY_START = {'theta_urad': 87.3, 'B_s': 0.7898, 'dEQ': -0.3900,
|
|
1288
|
+
# dEQ, gauss_fwhm and f_LM come from the GLOBAL fit, not from 008: they are
|
|
1289
|
+
# properties of the crystal and the instrument, and one spectrum determines dEQ
|
|
1290
|
+
# only to about +-0.04 (008 alone prefers -0.24).
|
|
1291
|
+
THEORY_START = {'theta_urad': 87.3, 'B_s': 0.7898, 'dEQ': -0.4216,
|
|
1293
1292
|
'shift': -0.2882, 'mosaic_urad': 0.0, 'dBs_rel_fwhm': 0.0,
|
|
1294
|
-
'gauss_fwhm': 0.1303, 'f_LM': 0.
|
|
1293
|
+
'gauss_fwhm': 0.1303, 'f_LM': 0.7711}
|
|
1295
1294
|
|
|
1296
1295
|
# Levenberg-Marquardt budget for the SCHEDULE passes. The final polish keeps
|
|
1297
1296
|
# the full budget; the intermediate passes do not need it and each iteration is
|
|
@@ -570,7 +570,7 @@ def save_model_to_library(main_window, title, comment=None, metadata=None, notif
|
|
|
570
570
|
main_window.set_status("Library title is empty", "orange")
|
|
571
571
|
return False
|
|
572
572
|
|
|
573
|
-
library_dir =
|
|
573
|
+
library_dir = main_window.library_dir
|
|
574
574
|
os.makedirs(library_dir, exist_ok=True)
|
|
575
575
|
final_title, version_idx = compute_versioned_title_if_needed(library_dir, title)
|
|
576
576
|
file_path = os.path.join(library_dir, f"{final_title}.mdl")
|
|
@@ -1225,7 +1225,12 @@ PHYS_FIELDS = ('theta_urad', 'B_s', 'dEQ', 'shift', 'thickness_um',
|
|
|
1225
1225
|
'gauss_fwhm', 'n_gauss', 'dT_mK')
|
|
1226
1226
|
|
|
1227
1227
|
PHYS_DEFAULTS = {
|
|
1228
|
-
|
|
1228
|
+
# dEQ and f_LM must match instrumental_io.THEORY_START, which is where they
|
|
1229
|
+
# are documented. They were allowed to drift apart once (this dict kept
|
|
1230
|
+
# -0.3228 for three revisions after the fitted value had moved) and nothing
|
|
1231
|
+
# caught it, because every production call passes dEQ explicitly -- so the
|
|
1232
|
+
# stale number only ever surfaces in a new caller that does not.
|
|
1233
|
+
'theta_urad': 70.0, 'B_s': 0.50, 'dEQ': -0.4216, 'shift': 0.0,
|
|
1229
1234
|
'thickness_um': DEFAULT_THICKNESS_UM, 'mosaic_urad': DEFAULT_MOSAIC_URAD,
|
|
1230
1235
|
'setting_urad': DEFAULT_SETTING_URAD, 'phi_m_deg': 0.0,
|
|
1231
1236
|
'f_LM': DEFAULT_F_LM, 'dBs_rel_fwhm': 0.0, 'n_angle': 11.0,
|
|
@@ -580,13 +580,14 @@ def open_hamiltonian_helper(main_window):
|
|
|
580
580
|
|
|
581
581
|
def export_library_pressed(main_window):
|
|
582
582
|
"""Export internal Library folder to a selected destination."""
|
|
583
|
+
# The Library dialogs open in the Library folder itself, not the work folder.
|
|
583
584
|
destination = QFileDialog.getExistingDirectory(
|
|
584
|
-
main_window, "Select destination folder", main_window.
|
|
585
|
+
main_window, "Select destination folder", main_window.library_dir)
|
|
585
586
|
if not destination:
|
|
586
587
|
main_window.set_status("Export Library canceled", "orange")
|
|
587
588
|
return
|
|
588
589
|
try:
|
|
589
|
-
library_dir =
|
|
590
|
+
library_dir = main_window.library_dir
|
|
590
591
|
target = export_library(library_dir, destination)
|
|
591
592
|
main_window.set_status(f"Library exported to: {target}", "green")
|
|
592
593
|
except Exception as e:
|
|
@@ -596,12 +597,12 @@ def export_library_pressed(main_window):
|
|
|
596
597
|
def import_library_pressed(main_window):
|
|
597
598
|
"""Import .mdl files from selected folder into internal Library folder."""
|
|
598
599
|
source = QFileDialog.getExistingDirectory(
|
|
599
|
-
main_window, "Select source folder", main_window.
|
|
600
|
+
main_window, "Select source folder", main_window.library_dir)
|
|
600
601
|
if not source:
|
|
601
602
|
main_window.set_status("Import Library canceled", "orange")
|
|
602
603
|
return
|
|
603
604
|
try:
|
|
604
|
-
library_dir =
|
|
605
|
+
library_dir = main_window.library_dir
|
|
605
606
|
result = import_library(source, library_dir)
|
|
606
607
|
if isinstance(result, dict):
|
|
607
608
|
copied = int(result.get('copied', 0))
|