syncmoss 0.3.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.3.0 → syncmoss-0.4.1}/.github/workflows/_Tests.yml +6 -3
- {syncmoss-0.3.0 → syncmoss-0.4.1}/.gitignore +8 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/PKG-INFO +1 -1
- {syncmoss-0.3.0 → syncmoss-0.4.1}/pyproject.toml +15 -1
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Calibration.py +10 -4
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library_window.py +1 -1
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/constants.py +20 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/fitting_io.py +4 -2
- syncmoss-0.4.1/syncmoss/instrumental_io.py +2001 -0
- syncmoss-0.4.1/syncmoss/instrumental_window.py +89 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/main.py +1 -1
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/model_io.py +1 -1
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/models.py +27 -5
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/models_positions.py +10 -2
- syncmoss-0.4.1/syncmoss/parameters/Calibration.dat +513 -0
- syncmoss-0.4.1/syncmoss/parameters/INSexp.txt +1 -0
- syncmoss-0.4.1/syncmoss/parameters/INSint.txt +1 -0
- syncmoss-0.4.1/syncmoss/parameters/INSth.txt +1 -0
- syncmoss-0.4.1/syncmoss/parameters/calibr.png +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/parameters/help.md +23 -0
- syncmoss-0.4.1/syncmoss/sms_theory.py +2264 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/spectrum_io.py +37 -11
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/spectrum_plotter.py +52 -0
- syncmoss-0.4.1/syncmoss/supp_menu.py +684 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/syncmoss_main.py +244 -53
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss.egg-info/PKG-INFO +1 -1
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss.egg-info/SOURCES.txt +13 -0
- syncmoss-0.4.1/syncmoss.egg-info/scm_file_list.json +172 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/tests/conftest.py +72 -0
- syncmoss-0.4.1/tests/syncmoss_test.py +628 -0
- syncmoss-0.4.1/tests/test_calibration_path.py +95 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/tests/test_instrumental_metadata.py +62 -0
- syncmoss-0.4.1/tests/test_instrumental_method_choice.py +155 -0
- syncmoss-0.4.1/tests/test_instrumental_recentre.py +136 -0
- syncmoss-0.4.1/tests/test_instrumental_window.py +121 -0
- syncmoss-0.4.1/tests/test_integral_points.py +104 -0
- syncmoss-0.4.1/tests/test_library_dir.py +239 -0
- syncmoss-0.4.1/tests/test_search_guards.py +129 -0
- syncmoss-0.4.1/tests/test_sms_theory.py +519 -0
- syncmoss-0.4.1/tests/test_theory_search.py +152 -0
- syncmoss-0.3.0/.github/workflows/_MacOS.yml +0 -329
- syncmoss-0.3.0/syncmoss/instrumental_io.py +0 -800
- syncmoss-0.3.0/syncmoss/parameters/Calibration.dat +0 -513
- syncmoss-0.3.0/syncmoss/parameters/INSexp.txt +0 -1
- syncmoss-0.3.0/syncmoss/parameters/INSint.txt +0 -1
- syncmoss-0.3.0/syncmoss/parameters/calibr.png +0 -0
- syncmoss-0.3.0/syncmoss/supp_menu.py +0 -295
- syncmoss-0.3.0/tests/syncmoss_test.py +0 -342
- {syncmoss-0.3.0 → syncmoss-0.4.1}/.github/workflows/Tests.yml +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/.github/workflows/_PyPI.yml +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/.github/workflows/_TagRelease.yml +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/.github/workflows/_UploadRelease.yml +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/.github/workflows/_Version.yml +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/.github/workflows/_Wheels.yml +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/.github/workflows/_WindowsExe.yml +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/.github/workflows/release.yml +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/.vscode/settings.json +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/COPYING.txt +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/LICENSE +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/Lib_test/test.mdl +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/NOTICE.txt +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/README.md +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/bundle/MacOS.spec +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/bundle/Windows.spec +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/bundle/bundle.py +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/bundle/macos_bundle.py +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/docs/thick_models.md +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/docs/thick_vs_thin.md +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/requirements.txt +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/setup.cfg +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Hamiltonian_helper.py +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Aegirine.mdl +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Akaganeite.mdl +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Almandine.mdl +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Antitaenite.mdl +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Arsenopyrite.mdl +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Bornite.mdl +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Bridgmanite (Fe,Al-bearing) @100 GPa.mdl +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Bridgmanite (Fe,Al-bearing).mdl +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Celadonite.mdl +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Cementite.mdl +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Chalcopyrite.mdl +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Clinopyroxene (augite).mdl +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Copiapite.mdl +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Cubanite.mdl +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Epidote.mdl +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Fayalite (Fe-olivine).mdl +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Ferrihydrite (2-line).mdl +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Ferropericlase.mdl +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Goethite.mdl +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Green rust (fougerite, carbonate).mdl +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Haegg carbide.mdl +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Hedenbergite.mdl +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Hematite.mdl +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Heterosite (FePO4).mdl +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Ilmenite.mdl +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Iron phosphide.mdl +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Isocubanite.mdl +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Jarosite.mdl +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Kamacite.mdl +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Lepidocrocite.mdl +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Maghemite.mdl +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Magnetite.mdl +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Marcasite.mdl +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Melanterite.mdl +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Orthopyroxene (enstatite-ferrosilite).mdl +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Prussian blue (ferric ferrocyanide).mdl +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Pyrite.mdl +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Pyrrhotite.mdl +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Rhomboclase.mdl +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Roemerite.mdl +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Siderite.mdl +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Szomolnokite.mdl +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Taenite.mdl +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Talnakhite.mdl +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Tetrataenite.mdl +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Triphylite (LiFePO4).mdl +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Troilite.mdl +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Vivianite.mdl +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Voltaite.mdl +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/alpha-Fe (metallic iron, bcc).mdl +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library_io.py +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/icons/CheckBox.png +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/icons/CheckBox_.png +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/icons/CheckBox_L.png +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/icons/CheckBox_L2.png +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/icons/DU.png +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/icons/Switch.png +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/icons/UD.png +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/icons/icon_r.ico +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/legacy.py +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/minimi_lib.py +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/models_description_window.py +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/parameters/ABSorber3.txt +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/parameters/Be.txt +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/parameters/GCMS.txt +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/parameters/INS_APS.txt +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/parameters/KB.txt +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/parameters/NFS.txt +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/parameters/models_description.md +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/parameters_table.py +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/results_table.py +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/support_math.py +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/test_graf.txt +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/test_param.txt +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/theme_dark.json +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/theme_light.json +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss.egg-info/dependency_links.txt +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss.egg-info/entry_points.txt +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss.egg-info/requires.txt +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss.egg-info/top_level.txt +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/tests/data/Distr_corr.mdl +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/tests/data/Distr_distr.mdl +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/tests/data/alpha_fe_sms_000.mca +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/tests/data/calibration_000_golden.json +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/tests/data/generate_cal_fixtures.py +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/tests/data/synthetic_alpha_fe_cms_linear.mca +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/tests/golden_models.json +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/tests/test_calibration.py +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/tests/test_calibration_helpers.py +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/tests/test_distr_composition.py +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/tests/test_distr_expansion.py +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/tests/test_distr_mdl.py +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/tests/test_distr_targets.py +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/tests/test_expression_validation.py +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/tests/test_golden_models.py +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/tests/test_gui_smoke.py +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/tests/test_hamiltonian_texture.py +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/tests/test_legacy.py +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/tests/test_library_io.py +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/tests/test_model_append.py +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/tests/test_model_io.py +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/tests/test_model_only_mode.py +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/tests/test_nbaseline_results.py +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/tests/test_parameter_links.py +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/tests/test_polarization.py +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/tests/test_recon_composition.py +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/tests/test_recon_mdl.py +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/tests/test_recon_results.py +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/tests/test_reference_shift.py +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/tests/test_sdw_grid.py +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/tests/test_support_math.py +0 -0
- {syncmoss-0.3.0 → syncmoss-0.4.1}/tests/test_texture_s1.py +0 -0
- {syncmoss-0.3.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
|
|
@@ -74,7 +74,7 @@ jobs:
|
|
|
74
74
|
run: |
|
|
75
75
|
python -m pip install --upgrade pip
|
|
76
76
|
pip install wheels/*.whl
|
|
77
|
-
pip install pytest
|
|
77
|
+
pip install pytest pytest-xdist
|
|
78
78
|
|
|
79
79
|
- name: Install runtime dependencies + pytest (source checkout)
|
|
80
80
|
if: inputs.install == 'source'
|
|
@@ -82,7 +82,7 @@ jobs:
|
|
|
82
82
|
run: |
|
|
83
83
|
python -m pip install --upgrade pip
|
|
84
84
|
pip install -r requirements.txt
|
|
85
|
-
pip install pytest
|
|
85
|
+
pip install pytest pytest-xdist
|
|
86
86
|
|
|
87
87
|
- name: Run the test-suite
|
|
88
88
|
shell: bash
|
|
@@ -103,4 +103,7 @@ jobs:
|
|
|
103
103
|
if [ "$INSTALL" = "wheel" ]; then cd "$(mktemp -d)"; fi
|
|
104
104
|
|
|
105
105
|
python -c "import syncmoss.syncmoss_main as m; print('syncmoss from:', m.__file__)"
|
|
106
|
-
|
|
106
|
+
# -n 0 forces a SERIAL run: pyproject sets -n auto for local use,
|
|
107
|
+
# which oversubscribes a small CI runner. xdist is installed above
|
|
108
|
+
# only so that this flag parses.
|
|
109
|
+
python -m pytest "$GITHUB_WORKSPACE/tests" -n 0 "${args[@]}"
|
|
@@ -162,3 +162,11 @@ minimi_optimization_notes.txt
|
|
|
162
162
|
# suite still covers the model end-to-end via the SCDW golden case, plus its
|
|
163
163
|
# param-count and locked-angle checks (test_model_io / test_thick_gui).
|
|
164
164
|
tests/test_sdw_grid.py
|
|
165
|
+
|
|
166
|
+
# SMS instrumental-function study against the ESRF (temperature, angle) dataset
|
|
167
|
+
# on N:\SL_studies: analysis scripts, cached intermediate fits and the figures
|
|
168
|
+
# they produce. Local-only -- it needs a network share that is not part of the
|
|
169
|
+
# repo, and its outputs are large. The conclusions that matter are tracked in
|
|
170
|
+
# docs/sms_instrumental_function.md; the calibration it produced is in
|
|
171
|
+
# sms_theory.FEBO3_BS_CALIBRATION and instrumental_io.ESRF_STANDARD_LINE_WIDTH.
|
|
172
|
+
sms_study/
|
|
@@ -73,7 +73,21 @@ local_scheme = "no-local-version"
|
|
|
73
73
|
|
|
74
74
|
[tool.pytest.ini_options]
|
|
75
75
|
testpaths = ["tests"]
|
|
76
|
-
|
|
76
|
+
# -n 6: almost every test here spends its time in ONE process doing physics
|
|
77
|
+
# (a fit, a calibration, an instrumental-function search), so a serial run
|
|
78
|
+
# leaves most of the machine idle -- measured 351 s serial against 116 s on 8
|
|
79
|
+
# workers for the calibration + instrumental set.
|
|
80
|
+
#
|
|
81
|
+
# A FIXED 6, not "auto". Each worker builds its own QApplication, PhysicsApp
|
|
82
|
+
# and numba-compiled model, so the suite is limited by MEMORY, not by cores:
|
|
83
|
+
# "auto" resolves to one worker per core (22 here) and dies during start-up
|
|
84
|
+
# with an execnet EOFError once the machine runs out. The same ceiling bit the
|
|
85
|
+
# 83-spectrum study, where 10 parallel fits OOM'd and 6 did not.
|
|
86
|
+
#
|
|
87
|
+
# CI overrides this with -n 0 (see .github/workflows/_Tests.yml): the runners
|
|
88
|
+
# are small, where even 6 oversubscribes. Use -n 0 locally too when debugging,
|
|
89
|
+
# since xdist swallows live output.
|
|
90
|
+
addopts = "-q --basetemp=./.pytest_tmp -n 6"
|
|
77
91
|
markers = [
|
|
78
92
|
"gui: builds the PySide6 GUI; runs head-less via QT_QPA_PLATFORM=offscreen (set in tests/conftest.py)",
|
|
79
93
|
"slow: slow tests (numba compilation / fitting)",
|
|
@@ -102,6 +102,7 @@ import numpy as np
|
|
|
102
102
|
|
|
103
103
|
import syncmoss.minimi_lib as mi
|
|
104
104
|
import syncmoss.models as m5
|
|
105
|
+
import syncmoss.sms_theory as smst
|
|
105
106
|
from syncmoss.constants import (number_of_baseline_parameters, ALPHA_FE_FIELD,
|
|
106
107
|
TESLA_PER_MMS, LINE_RATIO_25, LINE_RATIO_34,
|
|
107
108
|
NAT_WIDTH)
|
|
@@ -532,11 +533,16 @@ def Calibration(dir_path, Cal_file, pool, VVV, INS, JN, x0, MulCo, Vel_start=1,
|
|
|
532
533
|
return m5.TI(x, p, model, JN, pool, x0, MulCo, INS, [], [], Norm=Norm)
|
|
533
534
|
|
|
534
535
|
# The multi-line instrumental function displaces the apparent line
|
|
535
|
-
# positions by
|
|
536
|
-
# the
|
|
536
|
+
# positions by its first moment (for the legacy sum of unit-area
|
|
537
|
+
# Gaussians: the intensity-weighted sum of the line shifts); the folded
|
|
538
|
+
# velocity axis is corrected by this at the very end. The theoretical
|
|
539
|
+
# SMS shapes compute the same moment in sms_theory.ins_centroid.
|
|
537
540
|
INS_shift = 0
|
|
538
|
-
|
|
539
|
-
|
|
541
|
+
if smst.ins_kind(INS) == smst.KIND_GAUSS:
|
|
542
|
+
for i in range(0, int(len(INS) / 3)):
|
|
543
|
+
INS_shift += INS[i * 3 + 1] * INS[i * 3 + 2] ** 2
|
|
544
|
+
else:
|
|
545
|
+
INS_shift = smst.ins_centroid(INS)
|
|
540
546
|
else:
|
|
541
547
|
raise ValueError(f"unknown experimental method VVV={VVV!r} (expected 1 or 3)")
|
|
542
548
|
|
|
@@ -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
|
|
@@ -56,6 +56,26 @@ TESLA_PER_MMS = 1 / (2 * LINE_SHIFT_16) # 3.1097641 T per mm/s o
|
|
|
56
56
|
LINE_RATIO_25 = LINE_SHIFT_25 / LINE_SHIFT_16 # 0.5789752
|
|
57
57
|
LINE_RATIO_34 = LINE_SHIFT_34 / LINE_SHIFT_16 # 0.1579504
|
|
58
58
|
|
|
59
|
+
# --- scattering-side 57Fe quantities (sms_theory.py) -----------------------
|
|
60
|
+
# Everything below is DERIVED from the primitives above plus two new inputs,
|
|
61
|
+
# ALPHA_IC and the universal hc; no 57Fe literal is repeated.
|
|
62
|
+
# ALPHA_IC -- internal conversion coefficient of the 14.4 keV level.
|
|
63
|
+
# HC_EV_A -- h*c = 1239.8419843320026 eV nm (exact in SI since 2019).
|
|
64
|
+
ALPHA_IC = 8.56
|
|
65
|
+
HC_EV_A = 12398.419843320026 # eV * Angstrom
|
|
66
|
+
|
|
67
|
+
MMPS_TO_NEV = E0 / c * 10**9 # 48.0733 neV per (mm/s): E = E0 v/c
|
|
68
|
+
GAMMA0_NEV = NAT_WIDTH * MMPS_TO_NEV # 4.7112 neV, the natural width in neV
|
|
69
|
+
MU_N_NEV_PER_T = mun / (E0_J / E0) * 10**9 # 31.4331 neV/T
|
|
70
|
+
LAMBDA_A = HC_EV_A / E0 # 0.8603 A
|
|
71
|
+
K_VEC_A = 2 * 3.141592653589793 / LAMBDA_A # 7.3035 1/A
|
|
72
|
+
R_E_A = 2.8179403262 * 10**-5 # classical electron radius, A
|
|
73
|
+
# Maximum resonant cross-section from the level scheme:
|
|
74
|
+
# sigma_0 = 2 pi lambdabar^2 (2 I_e + 1)/(2 I_g + 1) / (1 + alpha_ic)
|
|
75
|
+
# equals `sigma` above (2.464e-22 m^2) to 0.02 %; sms_theory.selftest() checks it.
|
|
76
|
+
SIGMA0_A2 = (2 * 3.141592653589793 * (LAMBDA_A / (2 * 3.141592653589793))**2
|
|
77
|
+
* (2 * 1.5 + 1) / (2 * 0.5 + 1) / (1 + ALPHA_IC)) # 0.024644 A^2
|
|
78
|
+
|
|
59
79
|
# Linear polarization degree of the SMS (synchrotron) beam, 0..1. Only a default:
|
|
60
80
|
# it is GUI-editable via Supp -> "Set polarization" and travels to the models as
|
|
61
81
|
# the `pol` / `sms_pol` argument. 0.98 is a realistic synchrotron beam.
|
|
@@ -343,7 +343,8 @@ def fit_single_spectrum(app, spectrum_file, pool, background=None, sequence_para
|
|
|
343
343
|
# Load and concatenate all spectra
|
|
344
344
|
A_list, B_list = [], []
|
|
345
345
|
for i, spec_file in enumerate(spectrum_files):
|
|
346
|
-
A_temp, B_temp = load_spectrum(
|
|
346
|
+
A_temp, B_temp = load_spectrum(
|
|
347
|
+
app, spec_file, calibration_path=app.calibration_path)
|
|
347
348
|
A_temp, B_temp = A_temp[0], B_temp[0]
|
|
348
349
|
A_list.append(A_temp)
|
|
349
350
|
B_list.append(B_temp)
|
|
@@ -357,7 +358,8 @@ def fit_single_spectrum(app, spectrum_file, pool, background=None, sequence_para
|
|
|
357
358
|
|
|
358
359
|
else:
|
|
359
360
|
# Single spectrum fitting mode
|
|
360
|
-
A, B = load_spectrum(app, spectrum_file
|
|
361
|
+
A, B = load_spectrum(app, spectrum_file,
|
|
362
|
+
calibration_path=app.calibration_path)
|
|
361
363
|
A, B = A[0], B[0] # Unpack from list
|
|
362
364
|
A_list, B_list = [A], [B]
|
|
363
365
|
|