setiastrosuitepro 1.6.7__py3-none-any.whl
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.
Potentially problematic release.
This version of setiastrosuitepro might be problematic. Click here for more details.
- setiastro/__init__.py +2 -0
- setiastro/data/SASP_data.fits +0 -0
- setiastro/data/catalogs/List_of_Galaxies_with_Distances_Gly.csv +488 -0
- setiastro/data/catalogs/astrobin_filters.csv +890 -0
- setiastro/data/catalogs/astrobin_filters_page1_local.csv +51 -0
- setiastro/data/catalogs/cali2.csv +63 -0
- setiastro/data/catalogs/cali2color.csv +65 -0
- setiastro/data/catalogs/celestial_catalog - original.csv +16471 -0
- setiastro/data/catalogs/celestial_catalog.csv +24031 -0
- setiastro/data/catalogs/detected_stars.csv +24784 -0
- setiastro/data/catalogs/fits_header_data.csv +46 -0
- setiastro/data/catalogs/test.csv +8 -0
- setiastro/data/catalogs/updated_celestial_catalog.csv +16471 -0
- setiastro/images/Astro_Spikes.png +0 -0
- setiastro/images/Background_startup.jpg +0 -0
- setiastro/images/HRDiagram.png +0 -0
- setiastro/images/LExtract.png +0 -0
- setiastro/images/LInsert.png +0 -0
- setiastro/images/Oxygenation-atm-2.svg.png +0 -0
- setiastro/images/RGB080604.png +0 -0
- setiastro/images/abeicon.png +0 -0
- setiastro/images/aberration.png +0 -0
- setiastro/images/acv_icon.png +0 -0
- setiastro/images/andromedatry.png +0 -0
- setiastro/images/andromedatry_satellited.png +0 -0
- setiastro/images/annotated.png +0 -0
- setiastro/images/aperture.png +0 -0
- setiastro/images/astrosuite.ico +0 -0
- setiastro/images/astrosuite.png +0 -0
- setiastro/images/astrosuitepro.icns +0 -0
- setiastro/images/astrosuitepro.ico +0 -0
- setiastro/images/astrosuitepro.png +0 -0
- setiastro/images/background.png +0 -0
- setiastro/images/background2.png +0 -0
- setiastro/images/benchmark.png +0 -0
- setiastro/images/big_moon_stabilizer_timeline.png +0 -0
- setiastro/images/big_moon_stabilizer_timeline_clean.png +0 -0
- setiastro/images/blaster.png +0 -0
- setiastro/images/blink.png +0 -0
- setiastro/images/clahe.png +0 -0
- setiastro/images/collage.png +0 -0
- setiastro/images/colorwheel.png +0 -0
- setiastro/images/contsub.png +0 -0
- setiastro/images/convo.png +0 -0
- setiastro/images/copyslot.png +0 -0
- setiastro/images/cosmic.png +0 -0
- setiastro/images/cosmicsat.png +0 -0
- setiastro/images/crop1.png +0 -0
- setiastro/images/cropicon.png +0 -0
- setiastro/images/curves.png +0 -0
- setiastro/images/cvs.png +0 -0
- setiastro/images/debayer.png +0 -0
- setiastro/images/denoise_cnn_custom.png +0 -0
- setiastro/images/denoise_cnn_graph.png +0 -0
- setiastro/images/disk.png +0 -0
- setiastro/images/dse.png +0 -0
- setiastro/images/exoicon.png +0 -0
- setiastro/images/eye.png +0 -0
- setiastro/images/first_quarter.png +0 -0
- setiastro/images/fliphorizontal.png +0 -0
- setiastro/images/flipvertical.png +0 -0
- setiastro/images/font.png +0 -0
- setiastro/images/freqsep.png +0 -0
- setiastro/images/full_moon.png +0 -0
- setiastro/images/functionbundle.png +0 -0
- setiastro/images/graxpert.png +0 -0
- setiastro/images/green.png +0 -0
- setiastro/images/gridicon.png +0 -0
- setiastro/images/halo.png +0 -0
- setiastro/images/hdr.png +0 -0
- setiastro/images/histogram.png +0 -0
- setiastro/images/hubble.png +0 -0
- setiastro/images/imagecombine.png +0 -0
- setiastro/images/invert.png +0 -0
- setiastro/images/isophote.png +0 -0
- setiastro/images/isophote_demo_figure.png +0 -0
- setiastro/images/isophote_demo_image.png +0 -0
- setiastro/images/isophote_demo_model.png +0 -0
- setiastro/images/isophote_demo_residual.png +0 -0
- setiastro/images/jwstpupil.png +0 -0
- setiastro/images/last_quarter.png +0 -0
- setiastro/images/linearfit.png +0 -0
- setiastro/images/livestacking.png +0 -0
- setiastro/images/mask.png +0 -0
- setiastro/images/maskapply.png +0 -0
- setiastro/images/maskcreate.png +0 -0
- setiastro/images/maskremove.png +0 -0
- setiastro/images/morpho.png +0 -0
- setiastro/images/mosaic.png +0 -0
- setiastro/images/multiscale_decomp.png +0 -0
- setiastro/images/nbtorgb.png +0 -0
- setiastro/images/neutral.png +0 -0
- setiastro/images/new_moon.png +0 -0
- setiastro/images/nuke.png +0 -0
- setiastro/images/openfile.png +0 -0
- setiastro/images/pedestal.png +0 -0
- setiastro/images/pen.png +0 -0
- setiastro/images/pixelmath.png +0 -0
- setiastro/images/platesolve.png +0 -0
- setiastro/images/ppp.png +0 -0
- setiastro/images/pro.png +0 -0
- setiastro/images/project.png +0 -0
- setiastro/images/psf.png +0 -0
- setiastro/images/redo.png +0 -0
- setiastro/images/redoicon.png +0 -0
- setiastro/images/rescale.png +0 -0
- setiastro/images/rgbalign.png +0 -0
- setiastro/images/rgbcombo.png +0 -0
- setiastro/images/rgbextract.png +0 -0
- setiastro/images/rotate180.png +0 -0
- setiastro/images/rotatearbitrary.png +0 -0
- setiastro/images/rotateclockwise.png +0 -0
- setiastro/images/rotatecounterclockwise.png +0 -0
- setiastro/images/satellite.png +0 -0
- setiastro/images/script.png +0 -0
- setiastro/images/selectivecolor.png +0 -0
- setiastro/images/simbad.png +0 -0
- setiastro/images/slot0.png +0 -0
- setiastro/images/slot1.png +0 -0
- setiastro/images/slot2.png +0 -0
- setiastro/images/slot3.png +0 -0
- setiastro/images/slot4.png +0 -0
- setiastro/images/slot5.png +0 -0
- setiastro/images/slot6.png +0 -0
- setiastro/images/slot7.png +0 -0
- setiastro/images/slot8.png +0 -0
- setiastro/images/slot9.png +0 -0
- setiastro/images/spcc.png +0 -0
- setiastro/images/spin_precession_vs_lunar_distance.png +0 -0
- setiastro/images/spinner.gif +0 -0
- setiastro/images/stacking.png +0 -0
- setiastro/images/staradd.png +0 -0
- setiastro/images/staralign.png +0 -0
- setiastro/images/starnet.png +0 -0
- setiastro/images/starregistration.png +0 -0
- setiastro/images/starspike.png +0 -0
- setiastro/images/starstretch.png +0 -0
- setiastro/images/statstretch.png +0 -0
- setiastro/images/supernova.png +0 -0
- setiastro/images/uhs.png +0 -0
- setiastro/images/undoicon.png +0 -0
- setiastro/images/upscale.png +0 -0
- setiastro/images/viewbundle.png +0 -0
- setiastro/images/waning_crescent_1.png +0 -0
- setiastro/images/waning_crescent_2.png +0 -0
- setiastro/images/waning_crescent_3.png +0 -0
- setiastro/images/waning_crescent_4.png +0 -0
- setiastro/images/waning_crescent_5.png +0 -0
- setiastro/images/waning_gibbous_1.png +0 -0
- setiastro/images/waning_gibbous_2.png +0 -0
- setiastro/images/waning_gibbous_3.png +0 -0
- setiastro/images/waning_gibbous_4.png +0 -0
- setiastro/images/waning_gibbous_5.png +0 -0
- setiastro/images/waxing_crescent_1.png +0 -0
- setiastro/images/waxing_crescent_2.png +0 -0
- setiastro/images/waxing_crescent_3.png +0 -0
- setiastro/images/waxing_crescent_4.png +0 -0
- setiastro/images/waxing_crescent_5.png +0 -0
- setiastro/images/waxing_gibbous_1.png +0 -0
- setiastro/images/waxing_gibbous_2.png +0 -0
- setiastro/images/waxing_gibbous_3.png +0 -0
- setiastro/images/waxing_gibbous_4.png +0 -0
- setiastro/images/waxing_gibbous_5.png +0 -0
- setiastro/images/whitebalance.png +0 -0
- setiastro/images/wimi_icon_256x256.png +0 -0
- setiastro/images/wimilogo.png +0 -0
- setiastro/images/wims.png +0 -0
- setiastro/images/wrench_icon.png +0 -0
- setiastro/images/xisfliberator.png +0 -0
- setiastro/qml/ResourceMonitor.qml +128 -0
- setiastro/saspro/__init__.py +20 -0
- setiastro/saspro/__main__.py +964 -0
- setiastro/saspro/_generated/__init__.py +7 -0
- setiastro/saspro/_generated/build_info.py +3 -0
- setiastro/saspro/abe.py +1379 -0
- setiastro/saspro/abe_preset.py +196 -0
- setiastro/saspro/aberration_ai.py +910 -0
- setiastro/saspro/aberration_ai_preset.py +224 -0
- setiastro/saspro/accel_installer.py +218 -0
- setiastro/saspro/accel_workers.py +30 -0
- setiastro/saspro/acv_exporter.py +379 -0
- setiastro/saspro/add_stars.py +627 -0
- setiastro/saspro/astrobin_exporter.py +1010 -0
- setiastro/saspro/astrospike.py +153 -0
- setiastro/saspro/astrospike_python.py +1841 -0
- setiastro/saspro/autostretch.py +198 -0
- setiastro/saspro/backgroundneutral.py +639 -0
- setiastro/saspro/batch_convert.py +328 -0
- setiastro/saspro/batch_renamer.py +522 -0
- setiastro/saspro/blemish_blaster.py +494 -0
- setiastro/saspro/blink_comparator_pro.py +3149 -0
- setiastro/saspro/bundles.py +61 -0
- setiastro/saspro/bundles_dock.py +114 -0
- setiastro/saspro/cheat_sheet.py +213 -0
- setiastro/saspro/clahe.py +371 -0
- setiastro/saspro/comet_stacking.py +1442 -0
- setiastro/saspro/common_tr.py +107 -0
- setiastro/saspro/config.py +38 -0
- setiastro/saspro/config_bootstrap.py +40 -0
- setiastro/saspro/config_manager.py +316 -0
- setiastro/saspro/continuum_subtract.py +1620 -0
- setiastro/saspro/convo.py +1403 -0
- setiastro/saspro/convo_preset.py +414 -0
- setiastro/saspro/copyastro.py +190 -0
- setiastro/saspro/cosmicclarity.py +1593 -0
- setiastro/saspro/cosmicclarity_preset.py +407 -0
- setiastro/saspro/crop_dialog_pro.py +1005 -0
- setiastro/saspro/crop_preset.py +189 -0
- setiastro/saspro/curve_editor_pro.py +2608 -0
- setiastro/saspro/curves_preset.py +375 -0
- setiastro/saspro/debayer.py +673 -0
- setiastro/saspro/debug_utils.py +29 -0
- setiastro/saspro/dnd_mime.py +35 -0
- setiastro/saspro/doc_manager.py +2727 -0
- setiastro/saspro/exoplanet_detector.py +2258 -0
- setiastro/saspro/file_utils.py +284 -0
- setiastro/saspro/fitsmodifier.py +748 -0
- setiastro/saspro/fix_bom.py +32 -0
- setiastro/saspro/free_torch_memory.py +48 -0
- setiastro/saspro/frequency_separation.py +1352 -0
- setiastro/saspro/function_bundle.py +1596 -0
- setiastro/saspro/generate_translations.py +3092 -0
- setiastro/saspro/ghs_dialog_pro.py +728 -0
- setiastro/saspro/ghs_preset.py +284 -0
- setiastro/saspro/graxpert.py +638 -0
- setiastro/saspro/graxpert_preset.py +287 -0
- setiastro/saspro/gui/__init__.py +0 -0
- setiastro/saspro/gui/main_window.py +8928 -0
- setiastro/saspro/gui/mixins/__init__.py +33 -0
- setiastro/saspro/gui/mixins/dock_mixin.py +375 -0
- setiastro/saspro/gui/mixins/file_mixin.py +450 -0
- setiastro/saspro/gui/mixins/geometry_mixin.py +503 -0
- setiastro/saspro/gui/mixins/header_mixin.py +441 -0
- setiastro/saspro/gui/mixins/mask_mixin.py +421 -0
- setiastro/saspro/gui/mixins/menu_mixin.py +391 -0
- setiastro/saspro/gui/mixins/theme_mixin.py +367 -0
- setiastro/saspro/gui/mixins/toolbar_mixin.py +1824 -0
- setiastro/saspro/gui/mixins/update_mixin.py +323 -0
- setiastro/saspro/gui/mixins/view_mixin.py +477 -0
- setiastro/saspro/gui/statistics_dialog.py +47 -0
- setiastro/saspro/halobgon.py +492 -0
- setiastro/saspro/header_viewer.py +448 -0
- setiastro/saspro/headless_utils.py +88 -0
- setiastro/saspro/histogram.py +760 -0
- setiastro/saspro/history_explorer.py +941 -0
- setiastro/saspro/i18n.py +168 -0
- setiastro/saspro/image_combine.py +421 -0
- setiastro/saspro/image_peeker_pro.py +1608 -0
- setiastro/saspro/imageops/__init__.py +37 -0
- setiastro/saspro/imageops/mdi_snap.py +292 -0
- setiastro/saspro/imageops/scnr.py +36 -0
- setiastro/saspro/imageops/starbasedwhitebalance.py +210 -0
- setiastro/saspro/imageops/stretch.py +236 -0
- setiastro/saspro/isophote.py +1186 -0
- setiastro/saspro/layers.py +208 -0
- setiastro/saspro/layers_dock.py +714 -0
- setiastro/saspro/lazy_imports.py +193 -0
- setiastro/saspro/legacy/__init__.py +2 -0
- setiastro/saspro/legacy/image_manager.py +2360 -0
- setiastro/saspro/legacy/numba_utils.py +3676 -0
- setiastro/saspro/legacy/xisf.py +1213 -0
- setiastro/saspro/linear_fit.py +537 -0
- setiastro/saspro/live_stacking.py +1854 -0
- setiastro/saspro/log_bus.py +5 -0
- setiastro/saspro/logging_config.py +460 -0
- setiastro/saspro/luminancerecombine.py +510 -0
- setiastro/saspro/main_helpers.py +201 -0
- setiastro/saspro/mask_creation.py +1090 -0
- setiastro/saspro/masks_core.py +56 -0
- setiastro/saspro/mdi_widgets.py +353 -0
- setiastro/saspro/memory_utils.py +666 -0
- setiastro/saspro/metadata_patcher.py +75 -0
- setiastro/saspro/mfdeconv.py +3909 -0
- setiastro/saspro/mfdeconv_earlystop.py +71 -0
- setiastro/saspro/mfdeconvcudnn.py +3312 -0
- setiastro/saspro/mfdeconvsport.py +2459 -0
- setiastro/saspro/minorbodycatalog.py +567 -0
- setiastro/saspro/morphology.py +411 -0
- setiastro/saspro/multiscale_decomp.py +1751 -0
- setiastro/saspro/nbtorgb_stars.py +541 -0
- setiastro/saspro/numba_utils.py +3145 -0
- setiastro/saspro/numba_warmup.py +141 -0
- setiastro/saspro/ops/__init__.py +9 -0
- setiastro/saspro/ops/command_help_dialog.py +623 -0
- setiastro/saspro/ops/command_runner.py +217 -0
- setiastro/saspro/ops/commands.py +1594 -0
- setiastro/saspro/ops/script_editor.py +1105 -0
- setiastro/saspro/ops/scripts.py +1476 -0
- setiastro/saspro/ops/settings.py +637 -0
- setiastro/saspro/parallel_utils.py +554 -0
- setiastro/saspro/pedestal.py +121 -0
- setiastro/saspro/perfect_palette_picker.py +1105 -0
- setiastro/saspro/pipeline.py +110 -0
- setiastro/saspro/pixelmath.py +1604 -0
- setiastro/saspro/plate_solver.py +2480 -0
- setiastro/saspro/project_io.py +797 -0
- setiastro/saspro/psf_utils.py +136 -0
- setiastro/saspro/psf_viewer.py +631 -0
- setiastro/saspro/pyi_rthook_astroquery.py +95 -0
- setiastro/saspro/remove_green.py +331 -0
- setiastro/saspro/remove_stars.py +1599 -0
- setiastro/saspro/remove_stars_preset.py +446 -0
- setiastro/saspro/resources.py +570 -0
- setiastro/saspro/rgb_combination.py +208 -0
- setiastro/saspro/rgb_extract.py +19 -0
- setiastro/saspro/rgbalign.py +727 -0
- setiastro/saspro/runtime_imports.py +7 -0
- setiastro/saspro/runtime_torch.py +754 -0
- setiastro/saspro/save_options.py +73 -0
- setiastro/saspro/selective_color.py +1614 -0
- setiastro/saspro/sfcc.py +1530 -0
- setiastro/saspro/shortcuts.py +3125 -0
- setiastro/saspro/signature_insert.py +1106 -0
- setiastro/saspro/stacking_suite.py +19069 -0
- setiastro/saspro/star_alignment.py +7383 -0
- setiastro/saspro/star_alignment_preset.py +329 -0
- setiastro/saspro/star_metrics.py +49 -0
- setiastro/saspro/star_spikes.py +769 -0
- setiastro/saspro/star_stretch.py +542 -0
- setiastro/saspro/stat_stretch.py +554 -0
- setiastro/saspro/status_log_dock.py +78 -0
- setiastro/saspro/subwindow.py +3523 -0
- setiastro/saspro/supernovaasteroidhunter.py +1719 -0
- setiastro/saspro/swap_manager.py +134 -0
- setiastro/saspro/torch_backend.py +89 -0
- setiastro/saspro/torch_rejection.py +434 -0
- setiastro/saspro/translations/all_source_strings.json +4726 -0
- setiastro/saspro/translations/ar_translations.py +4096 -0
- setiastro/saspro/translations/de_translations.py +3728 -0
- setiastro/saspro/translations/es_translations.py +4169 -0
- setiastro/saspro/translations/fr_translations.py +4090 -0
- setiastro/saspro/translations/hi_translations.py +3803 -0
- setiastro/saspro/translations/integrate_translations.py +271 -0
- setiastro/saspro/translations/it_translations.py +4728 -0
- setiastro/saspro/translations/ja_translations.py +3834 -0
- setiastro/saspro/translations/pt_translations.py +3847 -0
- setiastro/saspro/translations/ru_translations.py +3082 -0
- setiastro/saspro/translations/saspro_ar.qm +0 -0
- setiastro/saspro/translations/saspro_ar.ts +16019 -0
- setiastro/saspro/translations/saspro_de.qm +0 -0
- setiastro/saspro/translations/saspro_de.ts +14548 -0
- setiastro/saspro/translations/saspro_es.qm +0 -0
- setiastro/saspro/translations/saspro_es.ts +16202 -0
- setiastro/saspro/translations/saspro_fr.qm +0 -0
- setiastro/saspro/translations/saspro_fr.ts +15870 -0
- setiastro/saspro/translations/saspro_hi.qm +0 -0
- setiastro/saspro/translations/saspro_hi.ts +14855 -0
- setiastro/saspro/translations/saspro_it.qm +0 -0
- setiastro/saspro/translations/saspro_it.ts +19046 -0
- setiastro/saspro/translations/saspro_ja.qm +0 -0
- setiastro/saspro/translations/saspro_ja.ts +14980 -0
- setiastro/saspro/translations/saspro_pt.qm +0 -0
- setiastro/saspro/translations/saspro_pt.ts +15024 -0
- setiastro/saspro/translations/saspro_ru.qm +0 -0
- setiastro/saspro/translations/saspro_ru.ts +11835 -0
- setiastro/saspro/translations/saspro_sw.qm +0 -0
- setiastro/saspro/translations/saspro_sw.ts +15237 -0
- setiastro/saspro/translations/saspro_uk.qm +0 -0
- setiastro/saspro/translations/saspro_uk.ts +15248 -0
- setiastro/saspro/translations/saspro_zh.qm +0 -0
- setiastro/saspro/translations/saspro_zh.ts +15289 -0
- setiastro/saspro/translations/sw_translations.py +3897 -0
- setiastro/saspro/translations/uk_translations.py +3929 -0
- setiastro/saspro/translations/zh_translations.py +3910 -0
- setiastro/saspro/versioning.py +77 -0
- setiastro/saspro/view_bundle.py +1558 -0
- setiastro/saspro/wavescale_hdr.py +648 -0
- setiastro/saspro/wavescale_hdr_preset.py +101 -0
- setiastro/saspro/wavescalede.py +683 -0
- setiastro/saspro/wavescalede_preset.py +230 -0
- setiastro/saspro/wcs_update.py +374 -0
- setiastro/saspro/whitebalance.py +540 -0
- setiastro/saspro/widgets/__init__.py +48 -0
- setiastro/saspro/widgets/common_utilities.py +306 -0
- setiastro/saspro/widgets/graphics_views.py +122 -0
- setiastro/saspro/widgets/image_utils.py +518 -0
- setiastro/saspro/widgets/minigame/game.js +991 -0
- setiastro/saspro/widgets/minigame/index.html +53 -0
- setiastro/saspro/widgets/minigame/style.css +241 -0
- setiastro/saspro/widgets/preview_dialogs.py +280 -0
- setiastro/saspro/widgets/resource_monitor.py +313 -0
- setiastro/saspro/widgets/spinboxes.py +290 -0
- setiastro/saspro/widgets/themed_buttons.py +13 -0
- setiastro/saspro/widgets/wavelet_utils.py +331 -0
- setiastro/saspro/wimi.py +7367 -0
- setiastro/saspro/wims.py +588 -0
- setiastro/saspro/window_shelf.py +185 -0
- setiastro/saspro/xisf.py +1213 -0
- setiastrosuitepro-1.6.7.dist-info/METADATA +279 -0
- setiastrosuitepro-1.6.7.dist-info/RECORD +394 -0
- setiastrosuitepro-1.6.7.dist-info/WHEEL +4 -0
- setiastrosuitepro-1.6.7.dist-info/entry_points.txt +6 -0
- setiastrosuitepro-1.6.7.dist-info/licenses/LICENSE +674 -0
- setiastrosuitepro-1.6.7.dist-info/licenses/license.txt +2580 -0
|
@@ -0,0 +1,754 @@
|
|
|
1
|
+
# pro/runtime_torch.py (hardened against shadowing / broken wheels)
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
import subprocess
|
|
6
|
+
import platform
|
|
7
|
+
import shutil
|
|
8
|
+
import json
|
|
9
|
+
import time
|
|
10
|
+
import errno
|
|
11
|
+
import importlib
|
|
12
|
+
import re
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from contextlib import contextmanager
|
|
15
|
+
|
|
16
|
+
import platform as _plat
|
|
17
|
+
from pathlib import Path as _Path
|
|
18
|
+
|
|
19
|
+
def _maybe_find_torch_shm_manager(torch_mod) -> str | None:
|
|
20
|
+
# Only Linux wheels include/use this helper binary.
|
|
21
|
+
if _plat.system() != "Linux":
|
|
22
|
+
return None
|
|
23
|
+
try:
|
|
24
|
+
base = _Path(getattr(torch_mod, "__file__", "")).parent
|
|
25
|
+
p = base / "bin" / "torch_shm_manager"
|
|
26
|
+
return str(p) if p.exists() else None
|
|
27
|
+
except Exception:
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
31
|
+
# Paths & runtime selection
|
|
32
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
33
|
+
def _venv_pyver(venv_python: Path) -> tuple[int, int] | None:
|
|
34
|
+
"""Return (major, minor) for the venv interpreter, or None if unknown."""
|
|
35
|
+
try:
|
|
36
|
+
out = subprocess.check_output(
|
|
37
|
+
[str(venv_python), "-c", "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')"],
|
|
38
|
+
text=True,
|
|
39
|
+
).strip()
|
|
40
|
+
maj, min_ = out.split(".")
|
|
41
|
+
return int(maj), int(min_)
|
|
42
|
+
except Exception:
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
def _tag_for_pyver(maj: int, min_: int) -> str:
|
|
46
|
+
return f"py{maj}{min_}"
|
|
47
|
+
|
|
48
|
+
def _runtime_base_dir() -> Path:
|
|
49
|
+
"""
|
|
50
|
+
Base folder that may contain multiple versioned runtimes (py310, py311, py312...).
|
|
51
|
+
Overridable via SASPRO_RUNTIME_DIR (which points to the parent "runtime" dir).
|
|
52
|
+
"""
|
|
53
|
+
env_override = os.getenv("SASPRO_RUNTIME_DIR")
|
|
54
|
+
if env_override:
|
|
55
|
+
base = Path(env_override).expanduser().resolve()
|
|
56
|
+
else:
|
|
57
|
+
sysname = platform.system()
|
|
58
|
+
if sysname == "Windows":
|
|
59
|
+
base = Path(os.getenv("LOCALAPPDATA", Path.home() / "AppData" / "Local"))
|
|
60
|
+
elif sysname == "Darwin":
|
|
61
|
+
base = Path.home() / "Library" / "Application Support"
|
|
62
|
+
else:
|
|
63
|
+
base = Path(os.getenv("XDG_DATA_HOME", Path.home() / ".local" / "share"))
|
|
64
|
+
base = base / "SASpro" / "runtime"
|
|
65
|
+
return base
|
|
66
|
+
|
|
67
|
+
def _current_tag() -> str:
|
|
68
|
+
return f"py{sys.version_info.major}{sys.version_info.minor}"
|
|
69
|
+
|
|
70
|
+
def _discover_existing_runtime_dir() -> Path | None:
|
|
71
|
+
"""
|
|
72
|
+
Return the newest existing runtime dir that already has a venv python,
|
|
73
|
+
using the venv interpreter's REAL version instead of just the folder name.
|
|
74
|
+
"""
|
|
75
|
+
base = _runtime_base_dir()
|
|
76
|
+
if not base.exists():
|
|
77
|
+
return None
|
|
78
|
+
candidates: list[tuple[int, int, Path]] = []
|
|
79
|
+
for p in base.glob("py*"):
|
|
80
|
+
vpy = p / "venv" / ("Scripts/python.exe" if platform.system() == "Windows" else "bin/python")
|
|
81
|
+
if not vpy.exists():
|
|
82
|
+
continue
|
|
83
|
+
ver = _venv_pyver(vpy)
|
|
84
|
+
if ver:
|
|
85
|
+
candidates.append((ver[0], ver[1], p))
|
|
86
|
+
if not candidates:
|
|
87
|
+
return None
|
|
88
|
+
candidates.sort() # pick the highest Python (major, minor)
|
|
89
|
+
return candidates[-1][2]
|
|
90
|
+
|
|
91
|
+
def _user_runtime_dir() -> Path:
|
|
92
|
+
"""
|
|
93
|
+
Use an existing runtime if we find one; otherwise select a directory for the
|
|
94
|
+
current interpreter version (py310/py311/py312...).
|
|
95
|
+
"""
|
|
96
|
+
existing = _discover_existing_runtime_dir()
|
|
97
|
+
return existing or (_runtime_base_dir() / _current_tag())
|
|
98
|
+
|
|
99
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
100
|
+
# Shadowing & sanity checks
|
|
101
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
104
|
+
# Shadowing & sanity checks
|
|
105
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
def _is_compiled_torch_dir(d: Path) -> bool:
|
|
108
|
+
"""True if 'torch' directory contains the compiled extension files."""
|
|
109
|
+
return any(d.glob("_C.*.pyd")) or any(d.glob("_C.*.so")) or any(d.glob("_C.cpython*"))
|
|
110
|
+
|
|
111
|
+
def _looks_like_source_tree_torch(d: Path) -> bool:
|
|
112
|
+
"""
|
|
113
|
+
True if this is a PyTorch repo / editable install dir (has torch/_C/__init__.py).
|
|
114
|
+
These can *never* satisfy torch._C at runtime.
|
|
115
|
+
"""
|
|
116
|
+
return (d / "_C" / "__init__.py").exists()
|
|
117
|
+
|
|
118
|
+
def _ban_shadow_torch_paths(status_cb=print) -> None:
|
|
119
|
+
"""
|
|
120
|
+
Remove (not just demote) any sys.path entries that would cause a source-tree
|
|
121
|
+
import of torch to win over the wheel. Also handles CWD ('') and editable installs.
|
|
122
|
+
"""
|
|
123
|
+
keep: list[str] = []
|
|
124
|
+
banned: list[str] = []
|
|
125
|
+
|
|
126
|
+
for entry in list(sys.path):
|
|
127
|
+
try:
|
|
128
|
+
base = Path(entry) if entry else Path.cwd()
|
|
129
|
+
td = base / "torch"
|
|
130
|
+
if td.is_dir():
|
|
131
|
+
# (a) repo/editable: has torch/_C/__init__.py → ban outright
|
|
132
|
+
if _looks_like_source_tree_torch(td):
|
|
133
|
+
banned.append(entry or "<cwd>")
|
|
134
|
+
continue
|
|
135
|
+
# (b) any 'torch' dir without compiled _C.* → ban (cannot work at runtime)
|
|
136
|
+
if not _is_compiled_torch_dir(td):
|
|
137
|
+
banned.append(entry or "<cwd>")
|
|
138
|
+
continue
|
|
139
|
+
except Exception:
|
|
140
|
+
# if we can't inspect, keep it
|
|
141
|
+
pass
|
|
142
|
+
keep.append(entry)
|
|
143
|
+
|
|
144
|
+
if banned:
|
|
145
|
+
sys.path[:] = keep
|
|
146
|
+
try:
|
|
147
|
+
status_cb("Removed shadowing torch paths: " + ", ".join(banned))
|
|
148
|
+
except Exception:
|
|
149
|
+
pass
|
|
150
|
+
|
|
151
|
+
_demote_shadow_torch_paths = _ban_shadow_torch_paths
|
|
152
|
+
|
|
153
|
+
def _purge_bad_torch_from_sysmodules(status_cb=print) -> None:
|
|
154
|
+
"""
|
|
155
|
+
If 'torch' is already imported from a shadow location, drop it so we can
|
|
156
|
+
re-import from the wheel after cleaning sys.path.
|
|
157
|
+
"""
|
|
158
|
+
try:
|
|
159
|
+
import importlib
|
|
160
|
+
if "torch" in sys.modules:
|
|
161
|
+
mod = sys.modules["torch"]
|
|
162
|
+
tf = getattr(mod, "__file__", "") or ""
|
|
163
|
+
if tf and (("site-packages" not in tf) and ("dist-packages" not in tf)):
|
|
164
|
+
# definitely a shadow import
|
|
165
|
+
for k in list(sys.modules.keys()):
|
|
166
|
+
if k == "torch" or k.startswith("torch."):
|
|
167
|
+
sys.modules.pop(k, None)
|
|
168
|
+
status_cb(f"Purged shadowed torch import: {tf}")
|
|
169
|
+
# Always ensure we don't carry a stale extension handle
|
|
170
|
+
sys.modules.pop("torch._C", None)
|
|
171
|
+
importlib.invalidate_caches()
|
|
172
|
+
except Exception:
|
|
173
|
+
pass
|
|
174
|
+
|
|
175
|
+
def _torch_sanity_check(status_cb=print):
|
|
176
|
+
try:
|
|
177
|
+
import torch
|
|
178
|
+
import importlib
|
|
179
|
+
tf = getattr(torch, "__file__", "") or ""
|
|
180
|
+
pkg_dir = Path(tf).parent if tf else None
|
|
181
|
+
|
|
182
|
+
# must come from site/dist packages
|
|
183
|
+
if ("site-packages" not in tf) and ("dist-packages" not in tf):
|
|
184
|
+
raise RuntimeError(f"Shadow import: torch.__file__ = {tf}")
|
|
185
|
+
|
|
186
|
+
# compiled extension must exist, and 'torch/_C/__init__.py' must NOT
|
|
187
|
+
if not _is_compiled_torch_dir(pkg_dir):
|
|
188
|
+
raise RuntimeError(f"Wheel missing torch._C in {pkg_dir}")
|
|
189
|
+
if (pkg_dir / "_C" / "__init__.py").exists():
|
|
190
|
+
raise RuntimeError(f"Found package folder torch/_C at {pkg_dir/'_C'}, this indicates a source tree.")
|
|
191
|
+
|
|
192
|
+
importlib.import_module("torch._C") # force extension load
|
|
193
|
+
|
|
194
|
+
x = torch.ones(1); y = x + 1
|
|
195
|
+
if int(y.item()) != 2:
|
|
196
|
+
raise RuntimeError("Unexpected tensor arithmetic result from torch sanity op.")
|
|
197
|
+
except Exception as e:
|
|
198
|
+
raise RuntimeError(f"PyTorch C-extension check failed: {e}") from e
|
|
199
|
+
|
|
200
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
201
|
+
# OS / permissions helpers
|
|
202
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
203
|
+
|
|
204
|
+
def _pip_run(venv_python: Path, args: list[str], status_cb=print) -> subprocess.CompletedProcess:
|
|
205
|
+
env = os.environ.copy()
|
|
206
|
+
env.pop("PYTHONPATH", None)
|
|
207
|
+
env.pop("PYTHONHOME", None)
|
|
208
|
+
return subprocess.run([str(venv_python), "-m", "pip", *args],
|
|
209
|
+
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, env=env)
|
|
210
|
+
|
|
211
|
+
def _pip_ok(venv_python: Path, args: list[str], status_cb=print) -> bool:
|
|
212
|
+
r = _pip_run(venv_python, args, status_cb=status_cb)
|
|
213
|
+
if r.returncode != 0:
|
|
214
|
+
tail = (r.stdout or "").strip()
|
|
215
|
+
try: status_cb(tail[-4000:])
|
|
216
|
+
except Exception as e:
|
|
217
|
+
import logging
|
|
218
|
+
logging.debug(f"Exception suppressed: {type(e).__name__}: {e}")
|
|
219
|
+
return r.returncode == 0
|
|
220
|
+
|
|
221
|
+
def _ensure_numpy(venv_python: Path, status_cb=print) -> None:
|
|
222
|
+
"""
|
|
223
|
+
Torch wheels may not pull NumPy; ensure NumPy is present in the SAME venv.
|
|
224
|
+
Safe to call repeatedly.
|
|
225
|
+
"""
|
|
226
|
+
def _numpy_present() -> bool:
|
|
227
|
+
code = "import importlib.util; print('OK' if importlib.util.find_spec('numpy') else 'MISS')"
|
|
228
|
+
try:
|
|
229
|
+
out = subprocess.check_output([str(venv_python), "-c", code], text=True).strip()
|
|
230
|
+
return (out == "OK")
|
|
231
|
+
except Exception:
|
|
232
|
+
return False
|
|
233
|
+
|
|
234
|
+
if _numpy_present():
|
|
235
|
+
return
|
|
236
|
+
|
|
237
|
+
# Keep tools fresh, then install a compatible NumPy (Torch 2.x is fine with NumPy 1.26–2.x)
|
|
238
|
+
_pip_ok(venv_python, ["install", "--upgrade", "pip", "setuptools", "wheel"], status_cb=status_cb)
|
|
239
|
+
|
|
240
|
+
# Prefer latest available in [1.26, 3.0)
|
|
241
|
+
if not _pip_ok(venv_python, ["install", "--prefer-binary", "--no-cache-dir", "numpy>=1.26,<3"], status_cb=status_cb):
|
|
242
|
+
# Final fallback to a broadly available pin
|
|
243
|
+
_pip_ok(venv_python, ["install", "--prefer-binary", "--no-cache-dir", "numpy==1.26.*"], status_cb=status_cb)
|
|
244
|
+
|
|
245
|
+
# Post-install verification
|
|
246
|
+
if not _numpy_present():
|
|
247
|
+
raise RuntimeError("Failed to install NumPy into the SASpro runtime venv.")
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _is_access_denied(exc: BaseException) -> bool:
|
|
251
|
+
if not isinstance(exc, OSError):
|
|
252
|
+
return False
|
|
253
|
+
if getattr(exc, "errno", None) == errno.EACCES:
|
|
254
|
+
return True
|
|
255
|
+
return getattr(exc, "winerror", None) == 5 # ERROR_ACCESS_DENIED
|
|
256
|
+
|
|
257
|
+
def _access_denied_msg(base_path: Path) -> str:
|
|
258
|
+
return (
|
|
259
|
+
"Access denied while preparing the SASpro runtime at:\n"
|
|
260
|
+
f" {base_path}\n\n"
|
|
261
|
+
"Possible causes:\n"
|
|
262
|
+
" • A corporate policy blocks writing to %LOCALAPPDATA%.\n"
|
|
263
|
+
" • Security software is sandboxing the app.\n\n"
|
|
264
|
+
"Fixes:\n"
|
|
265
|
+
" 1) Run SASpro once as Administrator (right-click → Run as administrator), or\n"
|
|
266
|
+
" 2) Set an alternate writable folder via environment variable SASPRO_RUNTIME_DIR\n"
|
|
267
|
+
" (e.g. C:\\Users\\<you>\\SASproRuntime) and relaunch."
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
271
|
+
# Venv creation & site discovery
|
|
272
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
273
|
+
|
|
274
|
+
def _venv_paths(rt: Path):
|
|
275
|
+
return {
|
|
276
|
+
"venv": rt / "venv",
|
|
277
|
+
"python": (rt / "venv" / "Scripts" / "python.exe") if platform.system() == "Windows" else (rt / "venv" / "bin" / "python"),
|
|
278
|
+
"marker": rt / "torch_installed.json",
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
def _site_packages(venv_python: Path) -> Path:
|
|
282
|
+
code = "import site, sys; print([p for p in site.getsitepackages() if 'site-packages' in p][-1])"
|
|
283
|
+
out = subprocess.check_output([str(venv_python), "-c", code], text=True).strip()
|
|
284
|
+
return Path(out)
|
|
285
|
+
|
|
286
|
+
def _ensure_venv(rt: Path, status_cb=print) -> Path:
|
|
287
|
+
p = _venv_paths(rt)
|
|
288
|
+
if not p["python"].exists():
|
|
289
|
+
try:
|
|
290
|
+
status_cb(f"Setting up SASpro runtime venv at: {p['venv']}")
|
|
291
|
+
p["venv"].mkdir(parents=True, exist_ok=True)
|
|
292
|
+
|
|
293
|
+
# choose the system python that will back this venv
|
|
294
|
+
py_cmd = _find_system_python_cmd() if getattr(sys, "frozen", False) else [sys.executable]
|
|
295
|
+
# detect its version to ensure the folder tag matches
|
|
296
|
+
out = subprocess.check_output(py_cmd + ["-c", "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')"], text=True).strip()
|
|
297
|
+
maj, min_ = map(int, out.split("."))
|
|
298
|
+
desired_tag = _tag_for_pyver(maj, min_)
|
|
299
|
+
if rt.name != desired_tag:
|
|
300
|
+
rt = _runtime_base_dir() / desired_tag
|
|
301
|
+
p = _venv_paths(rt)
|
|
302
|
+
status_cb(f"Adjusted runtime folder to match Python {maj}.{min_}: {rt}")
|
|
303
|
+
p["venv"].mkdir(parents=True, exist_ok=True)
|
|
304
|
+
|
|
305
|
+
env = os.environ.copy(); env.pop("PYTHONHOME", None); env.pop("PYTHONPATH", None)
|
|
306
|
+
subprocess.check_call(py_cmd + ["-m", "venv", str(p["venv"])], env=env)
|
|
307
|
+
subprocess.check_call([str(p["python"]), "-m", "ensurepip", "--upgrade"], env=env)
|
|
308
|
+
subprocess.check_call([str(p["python"]), "-m", "pip", "install", "--upgrade", "pip", "wheel", "setuptools"], env=env)
|
|
309
|
+
except subprocess.CalledProcessError:
|
|
310
|
+
try:
|
|
311
|
+
if p["venv"].exists():
|
|
312
|
+
shutil.rmtree(p["venv"], ignore_errors=True)
|
|
313
|
+
finally:
|
|
314
|
+
raise
|
|
315
|
+
except Exception as e:
|
|
316
|
+
if _is_access_denied(e):
|
|
317
|
+
raise OSError(_access_denied_msg(rt)) from e
|
|
318
|
+
raise
|
|
319
|
+
else:
|
|
320
|
+
# venv already exists — verify its interpreter version matches the folder tag
|
|
321
|
+
ver = _venv_pyver(p["python"])
|
|
322
|
+
if ver and rt.name != _tag_for_pyver(*ver):
|
|
323
|
+
status_cb(f"Runtime folder/version mismatch ({rt.name} vs Python {ver[0]}.{ver[1]}). Rebuilding.")
|
|
324
|
+
shutil.rmtree(p["venv"], ignore_errors=True)
|
|
325
|
+
# recreate at the correct tag
|
|
326
|
+
corrected = _runtime_base_dir() / _tag_for_pyver(*ver)
|
|
327
|
+
return _ensure_venv(corrected, status_cb=status_cb)
|
|
328
|
+
|
|
329
|
+
return p["python"]
|
|
330
|
+
|
|
331
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
332
|
+
# Install locking & version ladder
|
|
333
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
334
|
+
|
|
335
|
+
@contextmanager
|
|
336
|
+
def _install_lock(rt: Path, timeout_s: int = 600):
|
|
337
|
+
"""
|
|
338
|
+
Prevent concurrent partial installs into the same runtime.
|
|
339
|
+
"""
|
|
340
|
+
lock = rt / ".install.lock"
|
|
341
|
+
rt.mkdir(parents=True, exist_ok=True)
|
|
342
|
+
start = time.time()
|
|
343
|
+
while True:
|
|
344
|
+
try:
|
|
345
|
+
fd = os.open(str(lock), os.O_CREAT | os.O_EXCL | os.O_WRONLY)
|
|
346
|
+
os.close(fd)
|
|
347
|
+
break
|
|
348
|
+
except FileExistsError:
|
|
349
|
+
if time.time() - start > timeout_s:
|
|
350
|
+
raise RuntimeError(f"Another install is running (lock: {lock})")
|
|
351
|
+
time.sleep(0.5)
|
|
352
|
+
try:
|
|
353
|
+
yield
|
|
354
|
+
finally:
|
|
355
|
+
try:
|
|
356
|
+
lock.unlink()
|
|
357
|
+
except Exception:
|
|
358
|
+
pass
|
|
359
|
+
|
|
360
|
+
# coarse but practical ladder by Python minor
|
|
361
|
+
_TORCH_VERSION_LADDER: dict[tuple[int, int], list[str]] = {
|
|
362
|
+
(3, 12): ["2.4.*", "2.3.*", "2.2.*"],
|
|
363
|
+
(3, 11): ["2.4.*", "2.3.*", "2.2.*", "2.1.*"],
|
|
364
|
+
(3, 10): ["2.4.*", "2.3.*", "2.2.*", "2.1.*", "1.13.*"],
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
368
|
+
# Torch installation with robust fallbacks
|
|
369
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
370
|
+
|
|
371
|
+
def _check_cuda_in_venv(venv_python: Path, status_cb=print) -> tuple[bool, str | None, str | None]:
|
|
372
|
+
"""
|
|
373
|
+
Run a small script *inside the runtime venv* to see if CUDA is usable.
|
|
374
|
+
|
|
375
|
+
Returns (ok, cuda_tag, error_msg)
|
|
376
|
+
• ok – True if torch imports and torch.cuda.is_available() and a small
|
|
377
|
+
matmul on device='cuda' succeeds.
|
|
378
|
+
• cuda_tag – value of torch.version.cuda (if available)
|
|
379
|
+
• error_msg – text from any exception or stderr, for logging.
|
|
380
|
+
"""
|
|
381
|
+
code = r"""
|
|
382
|
+
import json
|
|
383
|
+
import sys
|
|
384
|
+
try:
|
|
385
|
+
import torch
|
|
386
|
+
info = {
|
|
387
|
+
"cuda_tag": getattr(getattr(torch, "version", None), "cuda", None),
|
|
388
|
+
"has_cuda": bool(getattr(torch, "cuda", None) and torch.cuda.is_available()),
|
|
389
|
+
"err": None,
|
|
390
|
+
}
|
|
391
|
+
if info["has_cuda"]:
|
|
392
|
+
# force some real GPU work
|
|
393
|
+
x = torch.rand((256, 256), device="cuda", dtype=torch.float32)
|
|
394
|
+
y = torch.rand((256, 256), device="cuda", dtype=torch.float32)
|
|
395
|
+
_ = (x @ y).sum().item()
|
|
396
|
+
print(json.dumps(info))
|
|
397
|
+
except Exception as e:
|
|
398
|
+
print(json.dumps({"cuda_tag": None, "has_cuda": False, "err": str(e)}))
|
|
399
|
+
sys.exit(1)
|
|
400
|
+
"""
|
|
401
|
+
r = subprocess.run(
|
|
402
|
+
[str(venv_python), "-c", code],
|
|
403
|
+
stdout=subprocess.PIPE,
|
|
404
|
+
stderr=subprocess.STDOUT,
|
|
405
|
+
text=True,
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
out = (r.stdout or "").strip()
|
|
409
|
+
# take last line in case pip noise gets mixed in
|
|
410
|
+
last = out.splitlines()[-1] if out else ""
|
|
411
|
+
try:
|
|
412
|
+
data = json.loads(last) if last else {}
|
|
413
|
+
except Exception as e:
|
|
414
|
+
msg = f"Failed to parse CUDA check output: {e}\nRaw output:\n{out}"
|
|
415
|
+
try:
|
|
416
|
+
status_cb(msg)
|
|
417
|
+
except Exception:
|
|
418
|
+
pass
|
|
419
|
+
return False, None, msg
|
|
420
|
+
|
|
421
|
+
ok = bool(data.get("has_cuda"))
|
|
422
|
+
tag = data.get("cuda_tag")
|
|
423
|
+
err = data.get("err")
|
|
424
|
+
return ok, tag, err
|
|
425
|
+
|
|
426
|
+
def _check_xpu_in_venv(venv_python: Path, status_cb=print) -> tuple[bool, str | None]:
|
|
427
|
+
code = r"""
|
|
428
|
+
import json
|
|
429
|
+
import sys
|
|
430
|
+
try:
|
|
431
|
+
import torch
|
|
432
|
+
has_xpu = hasattr(torch, "xpu") and torch.xpu.is_available()
|
|
433
|
+
if has_xpu:
|
|
434
|
+
x = torch.rand((128, 128), device="xpu")
|
|
435
|
+
y = torch.rand((128, 128), device="xpu")
|
|
436
|
+
_ = (x @ y).sum().item()
|
|
437
|
+
print(json.dumps({"has_xpu": bool(has_xpu)}))
|
|
438
|
+
except Exception as e:
|
|
439
|
+
print(json.dumps({"has_xpu": False, "err": str(e)}))
|
|
440
|
+
sys.exit(1)
|
|
441
|
+
"""
|
|
442
|
+
r = subprocess.run(
|
|
443
|
+
[str(venv_python), "-c", code],
|
|
444
|
+
stdout=subprocess.PIPE,
|
|
445
|
+
stderr=subprocess.STDOUT,
|
|
446
|
+
text=True,
|
|
447
|
+
)
|
|
448
|
+
out = (r.stdout or "").strip()
|
|
449
|
+
last = out.splitlines()[-1] if out else ""
|
|
450
|
+
try:
|
|
451
|
+
data = json.loads(last) if last else {}
|
|
452
|
+
except Exception as e:
|
|
453
|
+
msg = f"Failed to parse XPU check output: {e}\nRaw output:\n{out}"
|
|
454
|
+
try: status_cb(msg)
|
|
455
|
+
except Exception as e:
|
|
456
|
+
import logging
|
|
457
|
+
logging.debug(f"Exception suppressed: {type(e).__name__}: {e}")
|
|
458
|
+
return False, msg
|
|
459
|
+
return bool(data.get("has_xpu")), data.get("err")
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def _install_torch(venv_python: Path, prefer_cuda: bool, prefer_xpu: bool, status_cb=print):
|
|
463
|
+
"""
|
|
464
|
+
Install torch into the per-user venv with best-effort backend detection:
|
|
465
|
+
• macOS arm64 → PyPI (MPS)
|
|
466
|
+
• Win/Linux + (prefer_cuda True) → try CUDA indices in order: cu124, cu121, cu118
|
|
467
|
+
• else → PyPI (CPU), with Linux fallback to official CPU index
|
|
468
|
+
Uses a version ladder when "no matching distribution" occurs.
|
|
469
|
+
"""
|
|
470
|
+
import platform as _plat
|
|
471
|
+
INTEL_XPU_INDEX = "https://pytorch-extension.intel.com/release-whl/stable/xpu/us/"
|
|
472
|
+
|
|
473
|
+
def _pip(*args, env=None) -> subprocess.CompletedProcess:
|
|
474
|
+
e = (os.environ.copy() if env is None else env)
|
|
475
|
+
e.pop("PYTHONPATH", None); e.pop("PYTHONHOME", None)
|
|
476
|
+
return subprocess.run([str(venv_python), "-m", "pip", *args],
|
|
477
|
+
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, env=e)
|
|
478
|
+
|
|
479
|
+
def _pip_ok(cmd: list[str]) -> bool:
|
|
480
|
+
r = _pip(*cmd)
|
|
481
|
+
if r.returncode != 0:
|
|
482
|
+
# surface tail of pip log for the UI
|
|
483
|
+
tail = (r.stdout or "").strip()
|
|
484
|
+
status_cb(tail[-4000:])
|
|
485
|
+
return r.returncode == 0
|
|
486
|
+
|
|
487
|
+
def _pyver() -> tuple[int, int]:
|
|
488
|
+
code = "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')"
|
|
489
|
+
out = subprocess.check_output([str(venv_python), "-c", code], text=True).strip()
|
|
490
|
+
major, minor = out.split(".")
|
|
491
|
+
return int(major), int(minor)
|
|
492
|
+
|
|
493
|
+
sysname = _plat.system()
|
|
494
|
+
machine = _plat.machine().lower()
|
|
495
|
+
py_major, py_minor = _pyver()
|
|
496
|
+
|
|
497
|
+
if sysname == "Darwin" and ("arm64" in machine or "aarch64" in machine):
|
|
498
|
+
if py_minor >= 13:
|
|
499
|
+
raise RuntimeError(
|
|
500
|
+
f"PyTorch wheels are not available for macOS arm64 on Python {py_major}.{py_minor}. "
|
|
501
|
+
"Please install Python 3.12 (e.g. `brew install python@3.12`) so SAS Pro can create "
|
|
502
|
+
"its runtime with 3.12 and install the MPS-enabled torch wheel."
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
ladder = _TORCH_VERSION_LADDER.get((py_major, py_minor), ["2.4.*", "2.3.*", "2.2.*"])
|
|
506
|
+
|
|
507
|
+
status_cb(f"Runtime Python: {py_major}.{py_minor}")
|
|
508
|
+
|
|
509
|
+
# Keep venv tools fresh
|
|
510
|
+
_pip_ok(["install", "--upgrade", "pip", "setuptools", "wheel"])
|
|
511
|
+
|
|
512
|
+
def _try_series(index_url: str | None, versions: list[str]) -> bool:
|
|
513
|
+
base = ["install", "--prefer-binary", "--no-cache-dir"]
|
|
514
|
+
if index_url:
|
|
515
|
+
base += ["--index-url", index_url]
|
|
516
|
+
# latest for that index first
|
|
517
|
+
if _pip_ok(base + ["torch"]):
|
|
518
|
+
return True
|
|
519
|
+
# walk the ladder
|
|
520
|
+
for v in versions:
|
|
521
|
+
if _pip_ok(base + [f"torch=={v}"]):
|
|
522
|
+
return True
|
|
523
|
+
return False
|
|
524
|
+
|
|
525
|
+
# macOS Apple Silicon → MPS wheels on PyPI
|
|
526
|
+
if sysname == "Darwin" and ("arm64" in machine or "aarch64" in machine):
|
|
527
|
+
status_cb("Installing PyTorch (macOS arm64, MPS)…")
|
|
528
|
+
if not _try_series(None, ladder):
|
|
529
|
+
raise RuntimeError("Failed to find a matching PyTorch wheel for macOS arm64.")
|
|
530
|
+
return
|
|
531
|
+
|
|
532
|
+
# Windows/Linux – CUDA first if requested, then CPU
|
|
533
|
+
try_cuda = prefer_cuda and sysname in ("Windows", "Linux")
|
|
534
|
+
cuda_indices = [
|
|
535
|
+
("cu129", "https://download.pytorch.org/whl/cu129"),
|
|
536
|
+
("cu128", "https://download.pytorch.org/whl/cu128"),
|
|
537
|
+
("cu124", "https://download.pytorch.org/whl/cu124"),
|
|
538
|
+
("cu121", "https://download.pytorch.org/whl/cu121"),
|
|
539
|
+
("cu118", "https://download.pytorch.org/whl/cu118"),
|
|
540
|
+
]
|
|
541
|
+
|
|
542
|
+
if try_cuda:
|
|
543
|
+
for tag, url in cuda_indices:
|
|
544
|
+
status_cb(f"Trying PyTorch CUDA wheels: {tag} …")
|
|
545
|
+
if _try_series(url, ladder):
|
|
546
|
+
# Verify the wheel just installed in the *runtime venv*, not the GUI env.
|
|
547
|
+
ok, cuda_tag, err = _check_cuda_in_venv(venv_python, status_cb=status_cb)
|
|
548
|
+
if not ok:
|
|
549
|
+
status_cb(
|
|
550
|
+
f"Installed from {tag} but CUDA is not available in the runtime venv "
|
|
551
|
+
f"(torch.version.cuda={cuda_tag!r}, err={err!r}). "
|
|
552
|
+
"Uninstalling and trying next…"
|
|
553
|
+
)
|
|
554
|
+
_pip_ok(["uninstall", "-y", "torch", "torchvision", "torchaudio"])
|
|
555
|
+
continue
|
|
556
|
+
|
|
557
|
+
status_cb(f"Installed PyTorch CUDA ({tag}; torch.version.cuda={cuda_tag}).")
|
|
558
|
+
return
|
|
559
|
+
|
|
560
|
+
status_cb(f"No matching CUDA {tag} wheel for this Python/OS. Trying next…")
|
|
561
|
+
|
|
562
|
+
status_cb("Falling back to CPU wheels (no matching CUDA wheel).")
|
|
563
|
+
try_xpu = prefer_xpu and sysname in ("Windows", "Linux")
|
|
564
|
+
if try_xpu:
|
|
565
|
+
status_cb("Trying PyTorch Intel XPU wheels…")
|
|
566
|
+
if _try_series(INTEL_XPU_INDEX, ladder):
|
|
567
|
+
ok, err = _check_xpu_in_venv(venv_python, status_cb=status_cb)
|
|
568
|
+
if ok:
|
|
569
|
+
status_cb("Installed PyTorch Intel XPU (torch.xpu available).")
|
|
570
|
+
return
|
|
571
|
+
else:
|
|
572
|
+
status_cb(f"XPU runtime test failed in venv: {err!r}. Uninstalling and falling back…")
|
|
573
|
+
_pip_ok(["uninstall", "-y", "torch", "torchvision", "torchaudio"])
|
|
574
|
+
else:
|
|
575
|
+
status_cb("No matching Intel XPU wheel for this Python/OS.")
|
|
576
|
+
# CPU path
|
|
577
|
+
status_cb("Installing PyTorch (CPU)…")
|
|
578
|
+
if _try_series(None, ladder):
|
|
579
|
+
return
|
|
580
|
+
if sysname == "Linux":
|
|
581
|
+
status_cb("Retry with official CPU index…")
|
|
582
|
+
if _try_series("https://download.pytorch.org/whl/cpu", ladder):
|
|
583
|
+
return
|
|
584
|
+
raise RuntimeError("Failed to install any compatible PyTorch wheel (CPU or CUDA).")
|
|
585
|
+
|
|
586
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
587
|
+
# Public entry points
|
|
588
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
589
|
+
|
|
590
|
+
def import_torch(prefer_cuda: bool = True, prefer_xpu: bool = False, status_cb=print):
|
|
591
|
+
"""
|
|
592
|
+
Ensure a per-user venv exists with torch installed; return the imported module.
|
|
593
|
+
Hardened against shadow imports, broken wheels, concurrent installs, and partial markers.
|
|
594
|
+
"""
|
|
595
|
+
# Before any attempt, demote shadowing paths (CWD / random folders)
|
|
596
|
+
_ban_shadow_torch_paths(status_cb=status_cb)
|
|
597
|
+
_purge_bad_torch_from_sysmodules(status_cb=status_cb)
|
|
598
|
+
|
|
599
|
+
# Fast path: if torch already importable and sane, use it
|
|
600
|
+
try:
|
|
601
|
+
import torch # noqa
|
|
602
|
+
_torch_sanity_check(status_cb=status_cb)
|
|
603
|
+
return torch
|
|
604
|
+
except Exception:
|
|
605
|
+
pass
|
|
606
|
+
|
|
607
|
+
rt = _user_runtime_dir()
|
|
608
|
+
vp = _ensure_venv(rt, status_cb=status_cb)
|
|
609
|
+
site = _site_packages(vp)
|
|
610
|
+
marker = rt / "torch_installed.json"
|
|
611
|
+
|
|
612
|
+
try:
|
|
613
|
+
_ensure_numpy(vp, status_cb=status_cb)
|
|
614
|
+
except Exception:
|
|
615
|
+
# Non-fatal; we'll try again if torch complains at runtime
|
|
616
|
+
pass
|
|
617
|
+
|
|
618
|
+
# If no marker, perform install under a lock
|
|
619
|
+
if not marker.exists():
|
|
620
|
+
try:
|
|
621
|
+
with _install_lock(rt):
|
|
622
|
+
# Re-check inside lock in case another process finished
|
|
623
|
+
if not marker.exists():
|
|
624
|
+
_install_torch(vp, prefer_cuda=prefer_cuda, prefer_xpu=prefer_xpu, status_cb=status_cb)
|
|
625
|
+
except Exception as e:
|
|
626
|
+
if _is_access_denied(e):
|
|
627
|
+
raise OSError(_access_denied_msg(rt)) from e
|
|
628
|
+
raise
|
|
629
|
+
|
|
630
|
+
# Ensure the venv site is first on sys.path, then demote shadowers again
|
|
631
|
+
if str(site) not in sys.path:
|
|
632
|
+
sys.path.insert(0, str(site))
|
|
633
|
+
_demote_shadow_torch_paths(status_cb=status_cb)
|
|
634
|
+
|
|
635
|
+
# Import + sanity. If broken, force a clean repair (all OSes).
|
|
636
|
+
def _force_repair():
|
|
637
|
+
try:
|
|
638
|
+
status_cb("Detected broken/shadowed Torch import → attempting clean repair…")
|
|
639
|
+
except Exception:
|
|
640
|
+
pass
|
|
641
|
+
subprocess.run([str(vp), "-m", "pip", "uninstall", "-y", "torch"], check=False)
|
|
642
|
+
subprocess.run([str(vp), "-m", "pip", "cache", "purge"], check=False)
|
|
643
|
+
with _install_lock(rt):
|
|
644
|
+
_install_torch(vp, prefer_cuda=prefer_cuda, prefer_xpu=prefer_xpu, status_cb=status_cb)
|
|
645
|
+
importlib.invalidate_caches()
|
|
646
|
+
_demote_shadow_torch_paths(status_cb=status_cb)
|
|
647
|
+
|
|
648
|
+
try:
|
|
649
|
+
_ensure_numpy(vp, status_cb=status_cb)
|
|
650
|
+
except Exception:
|
|
651
|
+
pass
|
|
652
|
+
|
|
653
|
+
try:
|
|
654
|
+
import torch # noqa
|
|
655
|
+
_torch_sanity_check(status_cb=status_cb)
|
|
656
|
+
# write/update marker only when sane
|
|
657
|
+
if not marker.exists():
|
|
658
|
+
pyver = f"{sys.version_info.major}.{sys.version_info.minor}"
|
|
659
|
+
marker.write_text(json.dumps({"installed": True, "python": pyver, "when": int(time.time())}), encoding="utf-8")
|
|
660
|
+
return torch
|
|
661
|
+
except Exception:
|
|
662
|
+
_force_repair()
|
|
663
|
+
_purge_bad_torch_from_sysmodules(status_cb=status_cb)
|
|
664
|
+
_ban_shadow_torch_paths(status_cb=status_cb)
|
|
665
|
+
import torch # retry
|
|
666
|
+
_torch_sanity_check(status_cb=status_cb)
|
|
667
|
+
if not marker.exists():
|
|
668
|
+
pyver = f"{sys.version_info.major}.{sys.version_info.minor}"
|
|
669
|
+
marker.write_text(json.dumps({"installed": True, "python": pyver, "when": int(time.time())}), encoding="utf-8")
|
|
670
|
+
return torch
|
|
671
|
+
|
|
672
|
+
def _find_system_python_cmd() -> list[str]:
|
|
673
|
+
import platform as _plat
|
|
674
|
+
if _plat.system() == "Darwin":
|
|
675
|
+
# Prefer versions that have PyTorch wheels on arm64.
|
|
676
|
+
candidates = [
|
|
677
|
+
"/opt/homebrew/bin/python3.12",
|
|
678
|
+
"/usr/local/bin/python3.12",
|
|
679
|
+
"/usr/bin/python3.12",
|
|
680
|
+
"/opt/homebrew/bin/python3.11",
|
|
681
|
+
"/usr/local/bin/python3.11",
|
|
682
|
+
"/usr/bin/python3.11",
|
|
683
|
+
"/opt/homebrew/bin/python3.10",
|
|
684
|
+
"/usr/local/bin/python3.10",
|
|
685
|
+
"/usr/bin/python3.10",
|
|
686
|
+
# finally, unversioned fallbacks (may be 3.13 — last resort)
|
|
687
|
+
"/opt/homebrew/bin/python3",
|
|
688
|
+
"/usr/local/bin/python3",
|
|
689
|
+
"/usr/bin/python3",
|
|
690
|
+
]
|
|
691
|
+
for exe in candidates:
|
|
692
|
+
if shutil.which(exe) or os.path.exists(exe):
|
|
693
|
+
try:
|
|
694
|
+
r = subprocess.run([exe, "-c", "import sys; print(sys.version)"],
|
|
695
|
+
stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True)
|
|
696
|
+
if r.returncode == 0:
|
|
697
|
+
return [exe]
|
|
698
|
+
except Exception:
|
|
699
|
+
pass
|
|
700
|
+
if _plat.system() == "Windows":
|
|
701
|
+
for args in (["py","-3.12"], ["py","-3.11"], ["py","-3.10"], ["py","-3"], ["python3"], ["python"]):
|
|
702
|
+
try:
|
|
703
|
+
r = subprocess.run(args + ["-c","import sys; print(sys.version)"],
|
|
704
|
+
stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True)
|
|
705
|
+
if r.returncode == 0:
|
|
706
|
+
return args
|
|
707
|
+
except Exception:
|
|
708
|
+
pass
|
|
709
|
+
else:
|
|
710
|
+
for exe in ("python3.12","python3.11","python3.10","python3"):
|
|
711
|
+
p = shutil.which(exe)
|
|
712
|
+
if p:
|
|
713
|
+
try:
|
|
714
|
+
r = subprocess.run([p,"-c","import sys; print(sys.version)"],
|
|
715
|
+
stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True)
|
|
716
|
+
if r.returncode == 0:
|
|
717
|
+
return [p]
|
|
718
|
+
except Exception:
|
|
719
|
+
pass
|
|
720
|
+
p = shutil.which("python")
|
|
721
|
+
if p:
|
|
722
|
+
return [p]
|
|
723
|
+
raise RuntimeError(
|
|
724
|
+
"Could not find a system Python to create the runtime environment.\n"
|
|
725
|
+
"Install Python 3.10+ or set SASPRO_RUNTIME_DIR to a writable path."
|
|
726
|
+
)
|
|
727
|
+
|
|
728
|
+
def add_runtime_to_sys_path(status_cb=print) -> None:
|
|
729
|
+
"""
|
|
730
|
+
Warm up sys.path so a fresh launch can see the runtime immediately.
|
|
731
|
+
"""
|
|
732
|
+
rt = _user_runtime_dir()
|
|
733
|
+
p = _venv_paths(rt)
|
|
734
|
+
vpy = p["python"]
|
|
735
|
+
if not vpy.exists():
|
|
736
|
+
return
|
|
737
|
+
try:
|
|
738
|
+
site = _site_packages(vpy)
|
|
739
|
+
sp = str(site)
|
|
740
|
+
if sp not in sys.path:
|
|
741
|
+
sys.path.insert(0, sp)
|
|
742
|
+
try:
|
|
743
|
+
status_cb(f"Added runtime site-packages to sys.path: {sp}")
|
|
744
|
+
except Exception:
|
|
745
|
+
pass
|
|
746
|
+
# also consider sibling dirs:
|
|
747
|
+
for c in (site, site.parent / "site-packages", site.parent / "dist-packages"):
|
|
748
|
+
sc = str(c)
|
|
749
|
+
if c.exists() and sc not in sys.path:
|
|
750
|
+
sys.path.insert(0, sc)
|
|
751
|
+
# After adding, demote any accidental shadowing paths
|
|
752
|
+
_demote_shadow_torch_paths(status_cb=status_cb)
|
|
753
|
+
except Exception:
|
|
754
|
+
return
|