railmux 0.4.0.dev28__tar.gz → 0.4.0.dev30__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.
- {railmux-0.4.0.dev28/src/railmux.egg-info → railmux-0.4.0.dev30}/PKG-INFO +1 -1
- railmux-0.4.0.dev30/src/railmux/__init__.py +1 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux/windows_install_log.py +27 -9
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux/windows_msys2.py +317 -14
- railmux-0.4.0.dev30/src/railmux/windows_paths.py +73 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30/src/railmux.egg-info}/PKG-INFO +1 -1
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux.egg-info/SOURCES.txt +2 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_windows_install_log.py +55 -2
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_windows_msys2.py +331 -3
- railmux-0.4.0.dev30/tests/test_windows_paths.py +83 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tools/windows_archive_smoke.py +37 -1
- railmux-0.4.0.dev28/src/railmux/__init__.py +0 -1
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/LICENSE +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/MANIFEST.in +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/README.md +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/pyproject.toml +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/setup.cfg +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux/__main__.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux/atomic_file.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux/background_index.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux/cli.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux/codex_index.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux/config.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux/config_cli.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux/diagnostics.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux/discovery.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux/display_transport.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux/entrypoint.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux/fast_display_client.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux/fast_display_history.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux/fast_display_input.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux/fast_display_protocol.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux/fast_display_server.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux/favorites.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux/fuzzy.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux/help_workspace.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux/launcher.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux/legacy_sessions.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux/local_clipboard.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux/local_open.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux/models.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux/modes.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux/mouse_manager.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux/orphan_marker.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux/pane_surface.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux/path_codec.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux/provider_paths.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux/remote_config.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux/renames.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux/restart_state.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux/runtime_config.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux/scroll_agent.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux/scroll_manager.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux/selection_isolation.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux/self_update.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux/session_cache.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux/session_index.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux/session_lease.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux/setting_contracts.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux/settings.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux/ssh_args.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux/ssh_compat.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux/ssh_display_diagnostics.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux/ssh_doctor.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux/system_deps.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux/terminal_status.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux/tmux_binding_manager.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux/tmux_ctl.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux/tmux_health.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux/tmux_server.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux/tool_panes.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux/transcript.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux/ui/__init__.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux/ui/_widgets.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux/ui/app.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux/ui/keymap.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux/ui/modals.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux/ui/projects_pane.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux/ui/running_pane.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux/ui/sessions_pane.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux/ui/sidebar.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux/ui/statusbar.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux/ui/workspace.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux/windows_attach_relay.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux/windows_bootstrap.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux/windows_pacman.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux/windows_tmux_lifecycle.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux/windows_ui_transition.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux.egg-info/dependency_links.txt +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux.egg-info/entry_points.txt +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux.egg-info/requires.txt +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/src/railmux.egg-info/top_level.txt +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_agent_workspace.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_atomic_file.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_background_index.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_cli.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_codex_delete.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_codex_index.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_codex_lineage.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_codex_transcript.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_config.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_config_cli.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_denested_transport_benchmark.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_diagnostics.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_discovery.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_display_transport.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_fast_full_window.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_focus_highlight.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_fullscreen_binding.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_fuzzy.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_help_agent.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_help_workspace.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_history_cleanup.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_keymap.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_launcher.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_layout_profile.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_legacy_sessions.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_local_clipboard.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_local_open.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_modals.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_mode_selection.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_modes.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_mouse_manager.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_new_project.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_options.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_orphan_recovery.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_pane_surface.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_path_codec.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_projects_pane.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_provider_paths.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_release_notes.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_release_workflow.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_remote_config.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_renames.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_restart_state.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_running_pane.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_running_sort.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_runtime_config.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_scroll_agent.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_scroll_manager.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_selection_isolation.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_self_update.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_session_cache.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_session_index.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_session_lease.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_sessions_pane.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_setting_contracts.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_settings.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_sidebar.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_soft_quit.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_ssh_args.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_ssh_compat.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_ssh_display_benchmark.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_ssh_display_diagnostics.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_ssh_doctor.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_status.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_statusbar.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_system_deps.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_terminal_size.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_terminal_status.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_termux_local_touch.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_tmux_binding_manager.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_tmux_ctl.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_tmux_health.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_tmux_integration.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_tmux_server.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_tmux_status.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_tool_panes.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_transcript.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_utf8_state.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_web_demo_recorder.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_wheel_smoke.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_widgets.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_windows_attach_relay.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_windows_bootstrap.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_windows_pacman.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_windows_tmux_lifecycle.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_windows_ui_transition.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_windows_wrapper_contract.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tests/test_yolo_prompt.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tools/denested_transport_benchmark.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tools/release_notes.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev30}/tools/wheel_smoke.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.0.dev30"
|
|
@@ -22,6 +22,7 @@ from railmux.terminal_status import (
|
|
|
22
22
|
STYLE_WARNING,
|
|
23
23
|
styled,
|
|
24
24
|
)
|
|
25
|
+
from railmux.windows_paths import managed_windows_data_root
|
|
25
26
|
|
|
26
27
|
|
|
27
28
|
_LOG_KEEP_COUNT = 5
|
|
@@ -60,6 +61,7 @@ _MSYS2_RESTART_MARKER = (
|
|
|
60
61
|
"to complete this update all msys2 processes including this terminal "
|
|
61
62
|
"will be closed"
|
|
62
63
|
)
|
|
64
|
+
_ANSI_ESCAPE_RE = re.compile(r"\x1b\[[0-?]*[ -/]*[@-~]")
|
|
63
65
|
_EXTRACTION_PERCENT_RE = re.compile(r"(?:^|\s)([0-9]{1,3})%\s")
|
|
64
66
|
_PACKAGE_COUNT_RE = re.compile(r"^Packages \(([0-9]+)\)")
|
|
65
67
|
_PACKAGE_DOWNLOAD_RE = re.compile(r"^\s*([^ ]+) downloading\.\.\.\s*$")
|
|
@@ -78,20 +80,25 @@ def install_log_path(
|
|
|
78
80
|
*,
|
|
79
81
|
version: str,
|
|
80
82
|
clock: Callable[[], float] = time.time,
|
|
83
|
+
data_root: Path | None = None,
|
|
81
84
|
) -> Path:
|
|
82
|
-
|
|
83
|
-
if
|
|
84
|
-
raise OSError("
|
|
85
|
+
root = managed_windows_data_root(environ) if data_root is None else data_root
|
|
86
|
+
if root is None:
|
|
87
|
+
raise OSError("a non-virtualized Windows data directory is unavailable")
|
|
85
88
|
stamp = time.strftime("%Y%m%dT%H%M%SZ", time.gmtime(clock()))
|
|
86
89
|
return (
|
|
87
|
-
|
|
88
|
-
/ "Railmux"
|
|
90
|
+
root
|
|
89
91
|
/ "logs"
|
|
90
92
|
/ f"install-{version}-{stamp}-{os.getpid()}.log"
|
|
91
93
|
)
|
|
92
94
|
|
|
93
95
|
|
|
94
|
-
def _prune_install_logs(
|
|
96
|
+
def _prune_install_logs(
|
|
97
|
+
directory: Path,
|
|
98
|
+
*,
|
|
99
|
+
keep: int = _LOG_KEEP_COUNT,
|
|
100
|
+
preserve: Path | None = None,
|
|
101
|
+
) -> None:
|
|
95
102
|
candidates: list[Path] = []
|
|
96
103
|
try:
|
|
97
104
|
entries = list(directory.iterdir())
|
|
@@ -107,7 +114,11 @@ def _prune_install_logs(directory: Path, *, keep: int = _LOG_KEEP_COUNT) -> None
|
|
|
107
114
|
)
|
|
108
115
|
except OSError:
|
|
109
116
|
return
|
|
110
|
-
|
|
117
|
+
preserved = preserve if preserve in candidates else None
|
|
118
|
+
others = [candidate for candidate in candidates if candidate != preserved]
|
|
119
|
+
other_limit = max(keep, 1) - (1 if preserved is not None else 0)
|
|
120
|
+
stale_candidates = others[max(other_limit, 0) :]
|
|
121
|
+
for stale in stale_candidates:
|
|
111
122
|
try:
|
|
112
123
|
stale.unlink()
|
|
113
124
|
except OSError:
|
|
@@ -147,12 +158,15 @@ class InstallReporter:
|
|
|
147
158
|
self._pip_network_failure = False
|
|
148
159
|
self._hard_failed_hosts: set[str] = set()
|
|
149
160
|
self._msys2_restart_requested = False
|
|
161
|
+
self._msys2_restart_lines: deque[str] = deque(maxlen=3)
|
|
150
162
|
|
|
151
163
|
def __enter__(self) -> InstallReporter:
|
|
152
164
|
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
153
165
|
self._log = self.path.open("x", encoding="utf-8", newline="\n")
|
|
154
166
|
self._write_log("Railmux Windows runtime installation\n")
|
|
155
|
-
|
|
167
|
+
# Windows may defer the visible last-write timestamp of an open file.
|
|
168
|
+
# Never let retention mistake the active evidence log for an old one.
|
|
169
|
+
_prune_install_logs(self.path.parent, preserve=self.path)
|
|
156
170
|
return self
|
|
157
171
|
|
|
158
172
|
def __exit__(self, _exc_type, _exc, _traceback) -> None:
|
|
@@ -215,6 +229,7 @@ class InstallReporter:
|
|
|
215
229
|
self._pip_network_failure = False
|
|
216
230
|
self._hard_failed_hosts.clear()
|
|
217
231
|
self._msys2_restart_requested = False
|
|
232
|
+
self._msys2_restart_lines.clear()
|
|
218
233
|
self._write_log(f"\n--- {label} ---\n")
|
|
219
234
|
|
|
220
235
|
def command_output(self, output: bytes | str | None) -> None:
|
|
@@ -265,7 +280,10 @@ class InstallReporter:
|
|
|
265
280
|
if line.strip():
|
|
266
281
|
self._tail.append(line)
|
|
267
282
|
lowered = line.lower()
|
|
268
|
-
|
|
283
|
+
normalized = " ".join(_ANSI_ESCAPE_RE.sub("", lowered).split())
|
|
284
|
+
if normalized:
|
|
285
|
+
self._msys2_restart_lines.append(normalized)
|
|
286
|
+
if _MSYS2_RESTART_MARKER in " ".join(self._msys2_restart_lines):
|
|
269
287
|
self._msys2_restart_requested = True
|
|
270
288
|
if any(marker in lowered for marker in _NETWORK_ERROR_MARKERS):
|
|
271
289
|
self._network_failure = True
|
|
@@ -42,6 +42,10 @@ from railmux.windows_pacman import (
|
|
|
42
42
|
validate_transaction_package_mirrors,
|
|
43
43
|
write_msys_only_pacman_config,
|
|
44
44
|
)
|
|
45
|
+
from railmux.windows_paths import (
|
|
46
|
+
legacy_local_app_data_root,
|
|
47
|
+
managed_windows_data_root,
|
|
48
|
+
)
|
|
45
49
|
from railmux.terminal_status import STYLE_ACCENT, STYLE_MUTED, styled
|
|
46
50
|
|
|
47
51
|
|
|
@@ -120,6 +124,28 @@ _PACKAGE_INVENTORY_COMMAND = "set -o pipefail; pacman -Q | LC_ALL=C sort"
|
|
|
120
124
|
_PACKAGE_INVENTORY_LIMIT = 1024 * 1024
|
|
121
125
|
_CORE_RUNTIME_PACKAGES = ("tmux", "python", "python-pip")
|
|
122
126
|
_MAX_BASE_UPDATE_PASSES = 3
|
|
127
|
+
_PACMAN_LOG_RELATIVE = Path("var") / "log" / "pacman.log"
|
|
128
|
+
_PACMAN_LOG_MAX_SIZE = 8 * 1024 * 1024
|
|
129
|
+
_PACMAN_LOG_APPEND_LIMIT = 2 * 1024 * 1024
|
|
130
|
+
_PACMAN_LOCAL_DB_ENTRY_LIMIT = 4096
|
|
131
|
+
_PACMAN_LOCAL_DESC_LIMIT = 64 * 1024
|
|
132
|
+
# Keep this aligned with alpm_pkg_is_core_package in the pacman build shipped
|
|
133
|
+
# by the pinned base. Any one of these packages makes MSYS2 close its processes.
|
|
134
|
+
_PACMAN_CORE_PACKAGES = frozenset(
|
|
135
|
+
{
|
|
136
|
+
"bash",
|
|
137
|
+
"filesystem",
|
|
138
|
+
"mintty",
|
|
139
|
+
"msys2-runtime",
|
|
140
|
+
"msys2-runtime-devel",
|
|
141
|
+
"pacman",
|
|
142
|
+
"pacman-mirrors",
|
|
143
|
+
}
|
|
144
|
+
)
|
|
145
|
+
_PACMAN_CORE_CHANGE_RE = re.compile(
|
|
146
|
+
r"\[ALPM] (?:upgraded|downgraded) ([^ ]+) \(([^ ]+) -> ([^)]+)\)",
|
|
147
|
+
re.IGNORECASE,
|
|
148
|
+
)
|
|
123
149
|
_PRIVATE_GPG_SHUTDOWN_COMMAND = (
|
|
124
150
|
"gpgconf --homedir /etc/pacman.d/gnupg --kill all"
|
|
125
151
|
)
|
|
@@ -200,6 +226,16 @@ ArchiveProbe = Callable[[str, str, int], _ArchiveProbe]
|
|
|
200
226
|
ArchiveResume = Callable[[str, str, int, int, Callable[[bytes], None]], None]
|
|
201
227
|
|
|
202
228
|
|
|
229
|
+
@dataclass(frozen=True)
|
|
230
|
+
class _PacmanLogCheckpoint:
|
|
231
|
+
existed: bool
|
|
232
|
+
size: int
|
|
233
|
+
identity: tuple[int, int] | None
|
|
234
|
+
tail_offset: int
|
|
235
|
+
tail_digest: str
|
|
236
|
+
core_versions: tuple[tuple[str, str], ...]
|
|
237
|
+
|
|
238
|
+
|
|
203
239
|
def _format_download_size(size: int) -> str:
|
|
204
240
|
if size >= 1024 * 1024:
|
|
205
241
|
return f"{size / (1024 * 1024):.1f} MiB"
|
|
@@ -522,17 +558,17 @@ class Msys2Runtime:
|
|
|
522
558
|
|
|
523
559
|
|
|
524
560
|
def _managed_base(environ: Mapping[str, str]) -> Path | None:
|
|
525
|
-
|
|
526
|
-
if
|
|
561
|
+
data_root = managed_windows_data_root(environ)
|
|
562
|
+
if data_root is None:
|
|
527
563
|
return None
|
|
528
|
-
return
|
|
564
|
+
return data_root / "runtimes"
|
|
529
565
|
|
|
530
566
|
|
|
531
567
|
def _managed_cache(environ: Mapping[str, str]) -> Path | None:
|
|
532
|
-
|
|
533
|
-
if
|
|
568
|
+
data_root = managed_windows_data_root(environ)
|
|
569
|
+
if data_root is None:
|
|
534
570
|
return None
|
|
535
|
-
return
|
|
571
|
+
return data_root / "cache"
|
|
536
572
|
|
|
537
573
|
|
|
538
574
|
def _path_is_link_or_reparse(path: Path) -> bool:
|
|
@@ -563,6 +599,7 @@ def _prepare_cached_archive(
|
|
|
563
599
|
cache: Path,
|
|
564
600
|
*,
|
|
565
601
|
downloader: Downloader | None,
|
|
602
|
+
prior_caches: Sequence[Path] = (),
|
|
566
603
|
) -> tuple[Path, str]:
|
|
567
604
|
cache.mkdir(parents=True, exist_ok=True)
|
|
568
605
|
archive = cache / MSYS2_ARCHIVE_NAME
|
|
@@ -572,6 +609,18 @@ def _prepare_cached_archive(
|
|
|
572
609
|
archive.unlink(missing_ok=True)
|
|
573
610
|
except OSError as exc:
|
|
574
611
|
raise RuntimeInstallError("could not replace the private archive cache") from exc
|
|
612
|
+
for prior_cache in prior_caches:
|
|
613
|
+
prior_archive = prior_cache / MSYS2_ARCHIVE_NAME
|
|
614
|
+
if prior_archive == archive or not _archive_cache_is_valid(prior_archive):
|
|
615
|
+
continue
|
|
616
|
+
try:
|
|
617
|
+
shutil.copyfile(prior_archive, archive)
|
|
618
|
+
except OSError:
|
|
619
|
+
archive.unlink(missing_ok=True)
|
|
620
|
+
continue
|
|
621
|
+
if _archive_cache_is_valid(archive):
|
|
622
|
+
return archive, "verified prior local cache"
|
|
623
|
+
archive.unlink(missing_ok=True)
|
|
575
624
|
source = download_from_sources(
|
|
576
625
|
archive,
|
|
577
626
|
MSYS2_ARCHIVE_SHA256,
|
|
@@ -1146,7 +1195,9 @@ def apply_managed_runtime_prune(
|
|
|
1146
1195
|
"""Re-plan under the install lock, then remove only unchanged candidates."""
|
|
1147
1196
|
base = _managed_base(environ)
|
|
1148
1197
|
if base is None:
|
|
1149
|
-
raise RuntimeInstallError(
|
|
1198
|
+
raise RuntimeInstallError(
|
|
1199
|
+
"a non-virtualized Windows user data directory is unavailable"
|
|
1200
|
+
)
|
|
1150
1201
|
effective_lock = install_lock if lock_factory is None else lock_factory
|
|
1151
1202
|
with effective_lock(base):
|
|
1152
1203
|
current = plan_managed_runtime_prune(
|
|
@@ -1638,9 +1689,11 @@ def _run_checked(
|
|
|
1638
1689
|
def _explain_windows_status(returncode: int, reporter: InstallReporter) -> None:
|
|
1639
1690
|
if _NATIVE_WINDOWS and returncode & 0xFFFFFFFF == 0xC0000135:
|
|
1640
1691
|
reporter.note(
|
|
1641
|
-
"Windows reported STATUS_DLL_NOT_FOUND
|
|
1642
|
-
"
|
|
1643
|
-
"
|
|
1692
|
+
"Windows reported STATUS_DLL_NOT_FOUND, but this pass had neither "
|
|
1693
|
+
"pacman's restart announcement nor a newly completed core "
|
|
1694
|
+
"transaction with a matching package-database version change. "
|
|
1695
|
+
"The temporary base will not be published; a missing or "
|
|
1696
|
+
"quarantined staged DLL remains possible.",
|
|
1644
1697
|
level="error",
|
|
1645
1698
|
)
|
|
1646
1699
|
|
|
@@ -1676,6 +1729,215 @@ def _report_mirror_decision(
|
|
|
1676
1729
|
)
|
|
1677
1730
|
|
|
1678
1731
|
|
|
1732
|
+
def _pacman_desc_value(text: str, field: str) -> str | None:
|
|
1733
|
+
lines = text.splitlines()
|
|
1734
|
+
marker = f"%{field}%"
|
|
1735
|
+
for index, line in enumerate(lines[:-1]):
|
|
1736
|
+
if line == marker and lines[index + 1]:
|
|
1737
|
+
return lines[index + 1]
|
|
1738
|
+
return None
|
|
1739
|
+
|
|
1740
|
+
|
|
1741
|
+
def _pacman_core_versions(root: Path) -> dict[str, str]:
|
|
1742
|
+
local_db = root / "var" / "lib" / "pacman" / "local"
|
|
1743
|
+
try:
|
|
1744
|
+
if _path_is_link_or_reparse(local_db) or not local_db.is_dir():
|
|
1745
|
+
raise RuntimeInstallError("the private pacman database is unsafe")
|
|
1746
|
+
entries = list(local_db.iterdir())
|
|
1747
|
+
except FileNotFoundError:
|
|
1748
|
+
return {}
|
|
1749
|
+
except OSError as exc:
|
|
1750
|
+
raise RuntimeInstallError("could not inspect the private pacman database") from exc
|
|
1751
|
+
if len(entries) > _PACMAN_LOCAL_DB_ENTRY_LIMIT:
|
|
1752
|
+
raise RuntimeInstallError("the private pacman database is unexpectedly large")
|
|
1753
|
+
versions: dict[str, str] = {}
|
|
1754
|
+
for entry in entries:
|
|
1755
|
+
if not any(
|
|
1756
|
+
entry.name.startswith(prefix)
|
|
1757
|
+
for prefix in (
|
|
1758
|
+
"bash-",
|
|
1759
|
+
"filesystem-",
|
|
1760
|
+
"mintty-",
|
|
1761
|
+
"msys2-runtime-",
|
|
1762
|
+
"pacman-",
|
|
1763
|
+
)
|
|
1764
|
+
):
|
|
1765
|
+
continue
|
|
1766
|
+
desc = entry / "desc"
|
|
1767
|
+
try:
|
|
1768
|
+
if (
|
|
1769
|
+
_path_is_link_or_reparse(entry)
|
|
1770
|
+
or not entry.is_dir()
|
|
1771
|
+
or _path_is_link_or_reparse(desc)
|
|
1772
|
+
or not desc.is_file()
|
|
1773
|
+
or desc.stat().st_size > _PACMAN_LOCAL_DESC_LIMIT
|
|
1774
|
+
):
|
|
1775
|
+
raise RuntimeInstallError("the private pacman database is unsafe")
|
|
1776
|
+
text = desc.read_text(encoding="utf-8")
|
|
1777
|
+
except RuntimeInstallError:
|
|
1778
|
+
raise
|
|
1779
|
+
except (OSError, UnicodeError) as exc:
|
|
1780
|
+
raise RuntimeInstallError(
|
|
1781
|
+
"could not read the private pacman database"
|
|
1782
|
+
) from exc
|
|
1783
|
+
name = _pacman_desc_value(text, "NAME")
|
|
1784
|
+
version = _pacman_desc_value(text, "VERSION")
|
|
1785
|
+
if name is None or version is None or not _is_pacman_core_package(name):
|
|
1786
|
+
continue
|
|
1787
|
+
if name in versions:
|
|
1788
|
+
raise RuntimeInstallError(
|
|
1789
|
+
"the private pacman database contains duplicate core packages"
|
|
1790
|
+
)
|
|
1791
|
+
versions[name] = version
|
|
1792
|
+
return versions
|
|
1793
|
+
|
|
1794
|
+
|
|
1795
|
+
def _pacman_log_checkpoint(root: Path) -> _PacmanLogCheckpoint:
|
|
1796
|
+
"""Pin the append-only pacman journal before a staged transaction."""
|
|
1797
|
+
path = root / _PACMAN_LOG_RELATIVE
|
|
1798
|
+
try:
|
|
1799
|
+
info = path.lstat()
|
|
1800
|
+
except FileNotFoundError:
|
|
1801
|
+
return _PacmanLogCheckpoint(
|
|
1802
|
+
False,
|
|
1803
|
+
0,
|
|
1804
|
+
None,
|
|
1805
|
+
0,
|
|
1806
|
+
hashlib.sha256().hexdigest(),
|
|
1807
|
+
tuple(sorted(_pacman_core_versions(root).items())),
|
|
1808
|
+
)
|
|
1809
|
+
except OSError as exc:
|
|
1810
|
+
raise RuntimeInstallError("could not inspect the private pacman log") from exc
|
|
1811
|
+
try:
|
|
1812
|
+
unsafe = _path_is_link_or_reparse(path)
|
|
1813
|
+
except OSError as exc:
|
|
1814
|
+
raise RuntimeInstallError("could not inspect the private pacman log") from exc
|
|
1815
|
+
if (
|
|
1816
|
+
unsafe
|
|
1817
|
+
or not stat.S_ISREG(info.st_mode)
|
|
1818
|
+
or info.st_size < 0
|
|
1819
|
+
or info.st_size > _PACMAN_LOG_MAX_SIZE
|
|
1820
|
+
):
|
|
1821
|
+
raise RuntimeInstallError("the private pacman log is unsafe")
|
|
1822
|
+
tail_offset = max(0, info.st_size - 4096)
|
|
1823
|
+
try:
|
|
1824
|
+
with path.open("rb") as stream:
|
|
1825
|
+
stream.seek(tail_offset)
|
|
1826
|
+
tail = stream.read(info.st_size - tail_offset)
|
|
1827
|
+
except OSError as exc:
|
|
1828
|
+
raise RuntimeInstallError("could not read the private pacman log") from exc
|
|
1829
|
+
return _PacmanLogCheckpoint(
|
|
1830
|
+
True,
|
|
1831
|
+
info.st_size,
|
|
1832
|
+
(info.st_dev, info.st_ino),
|
|
1833
|
+
tail_offset,
|
|
1834
|
+
hashlib.sha256(tail).hexdigest(),
|
|
1835
|
+
tuple(sorted(_pacman_core_versions(root).items())),
|
|
1836
|
+
)
|
|
1837
|
+
|
|
1838
|
+
|
|
1839
|
+
def _is_pacman_core_package(name: str) -> bool:
|
|
1840
|
+
return name in _PACMAN_CORE_PACKAGES or name.startswith("msys2-runtime-")
|
|
1841
|
+
|
|
1842
|
+
|
|
1843
|
+
def _pacman_log_core_transaction(
|
|
1844
|
+
root: Path, checkpoint: _PacmanLogCheckpoint
|
|
1845
|
+
) -> tuple[str, ...]:
|
|
1846
|
+
"""Return core packages from one newly completed journal transaction."""
|
|
1847
|
+
path = root / _PACMAN_LOG_RELATIVE
|
|
1848
|
+
try:
|
|
1849
|
+
info = path.lstat()
|
|
1850
|
+
unsafe = _path_is_link_or_reparse(path)
|
|
1851
|
+
except OSError:
|
|
1852
|
+
return ()
|
|
1853
|
+
if (
|
|
1854
|
+
unsafe
|
|
1855
|
+
or not stat.S_ISREG(info.st_mode)
|
|
1856
|
+
or info.st_size < checkpoint.size
|
|
1857
|
+
or info.st_size > _PACMAN_LOG_MAX_SIZE
|
|
1858
|
+
or info.st_size - checkpoint.size > _PACMAN_LOG_APPEND_LIMIT
|
|
1859
|
+
):
|
|
1860
|
+
return ()
|
|
1861
|
+
if checkpoint.existed and (info.st_dev, info.st_ino) != checkpoint.identity:
|
|
1862
|
+
return ()
|
|
1863
|
+
try:
|
|
1864
|
+
with path.open("rb") as stream:
|
|
1865
|
+
if checkpoint.existed:
|
|
1866
|
+
stream.seek(checkpoint.tail_offset)
|
|
1867
|
+
previous_tail = stream.read(checkpoint.size - checkpoint.tail_offset)
|
|
1868
|
+
if hashlib.sha256(previous_tail).hexdigest() != checkpoint.tail_digest:
|
|
1869
|
+
return ()
|
|
1870
|
+
stream.seek(checkpoint.size)
|
|
1871
|
+
appended = stream.read(_PACMAN_LOG_APPEND_LIMIT + 1)
|
|
1872
|
+
except OSError:
|
|
1873
|
+
return ()
|
|
1874
|
+
if not appended or len(appended) > _PACMAN_LOG_APPEND_LIMIT:
|
|
1875
|
+
return ()
|
|
1876
|
+
|
|
1877
|
+
core_started = False
|
|
1878
|
+
transaction_started = False
|
|
1879
|
+
changed: dict[str, tuple[str, str]] = {}
|
|
1880
|
+
for line in appended.decode("utf-8", errors="replace").splitlines():
|
|
1881
|
+
lowered = line.lower()
|
|
1882
|
+
if "[pacman] starting core system upgrade" in lowered:
|
|
1883
|
+
core_started = True
|
|
1884
|
+
transaction_started = False
|
|
1885
|
+
changed.clear()
|
|
1886
|
+
continue
|
|
1887
|
+
if not core_started:
|
|
1888
|
+
continue
|
|
1889
|
+
if "[alpm] transaction started" in lowered:
|
|
1890
|
+
transaction_started = True
|
|
1891
|
+
changed.clear()
|
|
1892
|
+
continue
|
|
1893
|
+
if not transaction_started:
|
|
1894
|
+
continue
|
|
1895
|
+
match = _PACMAN_CORE_CHANGE_RE.search(line)
|
|
1896
|
+
if match is not None and _is_pacman_core_package(match.group(1)):
|
|
1897
|
+
changed[match.group(1)] = (match.group(2), match.group(3))
|
|
1898
|
+
if "[alpm] transaction completed" in lowered:
|
|
1899
|
+
if changed:
|
|
1900
|
+
try:
|
|
1901
|
+
after = _pacman_core_versions(root)
|
|
1902
|
+
except RuntimeInstallError:
|
|
1903
|
+
return ()
|
|
1904
|
+
before = dict(checkpoint.core_versions)
|
|
1905
|
+
proven = tuple(
|
|
1906
|
+
sorted(
|
|
1907
|
+
name
|
|
1908
|
+
for name, (old_version, new_version) in changed.items()
|
|
1909
|
+
if before.get(name) == old_version
|
|
1910
|
+
and after.get(name) == new_version
|
|
1911
|
+
and old_version != new_version
|
|
1912
|
+
)
|
|
1913
|
+
)
|
|
1914
|
+
if proven:
|
|
1915
|
+
return proven
|
|
1916
|
+
transaction_started = False
|
|
1917
|
+
return ()
|
|
1918
|
+
|
|
1919
|
+
|
|
1920
|
+
def _restart_evidence(
|
|
1921
|
+
root: Path,
|
|
1922
|
+
checkpoint: _PacmanLogCheckpoint,
|
|
1923
|
+
reporter: InstallReporter,
|
|
1924
|
+
returncode: int,
|
|
1925
|
+
) -> bool:
|
|
1926
|
+
if reporter.command_requested_msys2_restart:
|
|
1927
|
+
return True
|
|
1928
|
+
if returncode & 0xFFFFFFFF != 0xC0000135:
|
|
1929
|
+
return False
|
|
1930
|
+
packages = _pacman_log_core_transaction(root, checkpoint)
|
|
1931
|
+
if not packages:
|
|
1932
|
+
return False
|
|
1933
|
+
reporter.note(
|
|
1934
|
+
"The private pacman journal confirms a completed core update "
|
|
1935
|
+
f"({', '.join(packages)}); its final console prompt was not captured.",
|
|
1936
|
+
level="warning",
|
|
1937
|
+
)
|
|
1938
|
+
return True
|
|
1939
|
+
|
|
1940
|
+
|
|
1679
1941
|
def _refresh_pacman_mirrors(
|
|
1680
1942
|
root: Path,
|
|
1681
1943
|
*,
|
|
@@ -1705,6 +1967,7 @@ def _run_pacman_with_recovery(
|
|
|
1705
1967
|
mirror_optimizer: MirrorOptimizer,
|
|
1706
1968
|
allow_core_restart: bool = False,
|
|
1707
1969
|
) -> bool:
|
|
1970
|
+
checkpoint = _pacman_log_checkpoint(root) if not packages else None
|
|
1708
1971
|
argv = _bash_command(root, _pacman_command(packages=packages, relaxed=False), str(cache))
|
|
1709
1972
|
returncode = _run_checked(
|
|
1710
1973
|
argv,
|
|
@@ -1715,9 +1978,23 @@ def _run_pacman_with_recovery(
|
|
|
1715
1978
|
progress="pacman",
|
|
1716
1979
|
allow_failure=True,
|
|
1717
1980
|
)
|
|
1718
|
-
restart_requested =
|
|
1981
|
+
restart_requested = (
|
|
1982
|
+
_restart_evidence(root, checkpoint, reporter, returncode)
|
|
1983
|
+
if checkpoint is not None
|
|
1984
|
+
else False
|
|
1985
|
+
)
|
|
1719
1986
|
if returncode == 0:
|
|
1720
1987
|
return restart_requested
|
|
1988
|
+
if not packages and restart_requested and not allow_core_restart:
|
|
1989
|
+
reporter.note(
|
|
1990
|
+
"MSYS2 requested another core restart on the final permitted "
|
|
1991
|
+
"update pass; the temporary base will not be published.",
|
|
1992
|
+
level="error",
|
|
1993
|
+
)
|
|
1994
|
+
reporter.command_failed(label, returncode)
|
|
1995
|
+
raise RuntimeInstallError(
|
|
1996
|
+
f"{label} exhausted the permitted core-update restarts"
|
|
1997
|
+
)
|
|
1721
1998
|
if allow_core_restart and not packages and restart_requested:
|
|
1722
1999
|
reporter.note(
|
|
1723
2000
|
"MSYS2 replaced core runtime files and closed its own process as "
|
|
@@ -1759,6 +2036,7 @@ def _run_pacman_with_recovery(
|
|
|
1759
2036
|
_pacman_command(packages=packages, relaxed=True),
|
|
1760
2037
|
str(cache),
|
|
1761
2038
|
)
|
|
2039
|
+
retry_checkpoint = _pacman_log_checkpoint(root) if not packages else None
|
|
1762
2040
|
retry_returncode = _run_checked(
|
|
1763
2041
|
retry_argv,
|
|
1764
2042
|
env=env,
|
|
@@ -1768,9 +2046,24 @@ def _run_pacman_with_recovery(
|
|
|
1768
2046
|
progress="pacman",
|
|
1769
2047
|
allow_failure=True,
|
|
1770
2048
|
)
|
|
1771
|
-
restart_requested =
|
|
2049
|
+
restart_requested = (
|
|
2050
|
+
_restart_evidence(root, retry_checkpoint, reporter, retry_returncode)
|
|
2051
|
+
if retry_checkpoint is not None
|
|
2052
|
+
else False
|
|
2053
|
+
)
|
|
1772
2054
|
if retry_returncode == 0:
|
|
1773
2055
|
return restart_requested
|
|
2056
|
+
if not packages and restart_requested and not allow_core_restart:
|
|
2057
|
+
reporter.note(
|
|
2058
|
+
"MSYS2 requested another core restart on the final permitted "
|
|
2059
|
+
"update pass; the temporary base will not be published.",
|
|
2060
|
+
level="error",
|
|
2061
|
+
)
|
|
2062
|
+
reporter.command_failed(f"{label} resilient retry", retry_returncode)
|
|
2063
|
+
raise RuntimeInstallError(
|
|
2064
|
+
f"{label} resilient retry exhausted the permitted "
|
|
2065
|
+
"core-update restarts"
|
|
2066
|
+
)
|
|
1774
2067
|
if allow_core_restart and not packages and restart_requested:
|
|
1775
2068
|
reporter.note(
|
|
1776
2069
|
"MSYS2 replaced core runtime files and closed its own process as "
|
|
@@ -2355,11 +2648,16 @@ def install_managed_runtime(
|
|
|
2355
2648
|
base = _managed_base(environ)
|
|
2356
2649
|
cache_base = _managed_cache(environ)
|
|
2357
2650
|
if base is None or cache_base is None:
|
|
2358
|
-
raise RuntimeInstallError(
|
|
2651
|
+
raise RuntimeInstallError(
|
|
2652
|
+
"a non-virtualized Windows user data directory is unavailable"
|
|
2653
|
+
)
|
|
2654
|
+
data_root = base.parent
|
|
2359
2655
|
try:
|
|
2360
2656
|
base.mkdir(parents=True, exist_ok=True)
|
|
2361
2657
|
cache_base.mkdir(parents=True, exist_ok=True)
|
|
2362
|
-
log_path = install_log_path(
|
|
2658
|
+
log_path = install_log_path(
|
|
2659
|
+
environ, version=version, data_root=data_root
|
|
2660
|
+
)
|
|
2363
2661
|
except OSError as exc:
|
|
2364
2662
|
raise RuntimeInstallError("could not create the private runtime log") from exc
|
|
2365
2663
|
final_root = base / "shared" / MSYS2_RUNTIME_ID
|
|
@@ -2475,6 +2773,11 @@ def install_managed_runtime(
|
|
|
2475
2773
|
archive, source = _prepare_cached_archive(
|
|
2476
2774
|
cache_base,
|
|
2477
2775
|
downloader=downloader,
|
|
2776
|
+
prior_caches=tuple(
|
|
2777
|
+
legacy / "cache"
|
|
2778
|
+
for legacy in (legacy_local_app_data_root(environ),)
|
|
2779
|
+
if legacy is not None and legacy != data_root
|
|
2780
|
+
),
|
|
2478
2781
|
)
|
|
2479
2782
|
reporter.done(source)
|
|
2480
2783
|
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Stable native-Windows storage paths for the managed runtime.
|
|
2
|
+
|
|
3
|
+
Microsoft Store and other MSIX-packaged Python interpreters can virtualize new
|
|
4
|
+
writes beneath AppData. Railmux's runtime contains executables that must be
|
|
5
|
+
opened by child processes outside that virtualized view, so packaged Python
|
|
6
|
+
uses a user-profile directory instead.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import ctypes
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
from collections.abc import Mapping
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
_ERROR_INSUFFICIENT_BUFFER = 122
|
|
19
|
+
_APPMODEL_ERROR_NO_PACKAGE = 15700
|
|
20
|
+
_PACKAGED_PROFILE_DIRECTORY = ".railmux"
|
|
21
|
+
_PACKAGED_WINDOWS_DIRECTORY = "windows"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _has_windows_package_identity() -> bool:
|
|
25
|
+
"""Return whether this process has an MSIX/AppX package identity."""
|
|
26
|
+
if os.name != "nt":
|
|
27
|
+
return False
|
|
28
|
+
try:
|
|
29
|
+
get_family = ctypes.windll.kernel32.GetCurrentPackageFamilyName
|
|
30
|
+
length = ctypes.c_uint32(0)
|
|
31
|
+
result = int(get_family(ctypes.byref(length), None))
|
|
32
|
+
except (AttributeError, OSError, TypeError, ValueError):
|
|
33
|
+
return _looks_like_packaged_executable(sys.executable)
|
|
34
|
+
if result == _APPMODEL_ERROR_NO_PACKAGE:
|
|
35
|
+
return False
|
|
36
|
+
if result == _ERROR_INSUFFICIENT_BUFFER and length.value > 0:
|
|
37
|
+
return True
|
|
38
|
+
# A Store Python launcher and interpreter both have characteristic physical
|
|
39
|
+
# paths. Keep this fallback fail-safe if the package API is unavailable or
|
|
40
|
+
# returns an unexpected platform error.
|
|
41
|
+
return _looks_like_packaged_executable(sys.executable)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _looks_like_packaged_executable(executable: str) -> bool:
|
|
45
|
+
normalized = executable.replace("/", "\\").casefold()
|
|
46
|
+
return "\\windowsapps\\" in normalized or (
|
|
47
|
+
"\\appdata\\local\\packages\\" in normalized and "\\localcache\\" in normalized
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def managed_windows_data_root(environ: Mapping[str, str]) -> Path | None:
|
|
52
|
+
"""Select one data root visible to both Python and MSYS2 child processes."""
|
|
53
|
+
if _has_windows_package_identity():
|
|
54
|
+
user_profile = environ.get("USERPROFILE", "").strip()
|
|
55
|
+
if not user_profile:
|
|
56
|
+
return None
|
|
57
|
+
return (
|
|
58
|
+
Path(user_profile)
|
|
59
|
+
/ _PACKAGED_PROFILE_DIRECTORY
|
|
60
|
+
/ _PACKAGED_WINDOWS_DIRECTORY
|
|
61
|
+
)
|
|
62
|
+
local_app_data = environ.get("LOCALAPPDATA", "").strip()
|
|
63
|
+
if not local_app_data:
|
|
64
|
+
return None
|
|
65
|
+
return Path(local_app_data) / "Railmux"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def legacy_local_app_data_root(environ: Mapping[str, str]) -> Path | None:
|
|
69
|
+
"""Return the pre-packaged-Python-fix location for read-only cache reuse."""
|
|
70
|
+
local_app_data = environ.get("LOCALAPPDATA", "").strip()
|
|
71
|
+
if not local_app_data:
|
|
72
|
+
return None
|
|
73
|
+
return Path(local_app_data) / "Railmux"
|
|
@@ -63,6 +63,7 @@ src/railmux/windows_bootstrap.py
|
|
|
63
63
|
src/railmux/windows_install_log.py
|
|
64
64
|
src/railmux/windows_msys2.py
|
|
65
65
|
src/railmux/windows_pacman.py
|
|
66
|
+
src/railmux/windows_paths.py
|
|
66
67
|
src/railmux/windows_tmux_lifecycle.py
|
|
67
68
|
src/railmux/windows_ui_transition.py
|
|
68
69
|
src/railmux.egg-info/PKG-INFO
|
|
@@ -168,6 +169,7 @@ tests/test_windows_bootstrap.py
|
|
|
168
169
|
tests/test_windows_install_log.py
|
|
169
170
|
tests/test_windows_msys2.py
|
|
170
171
|
tests/test_windows_pacman.py
|
|
172
|
+
tests/test_windows_paths.py
|
|
171
173
|
tests/test_windows_tmux_lifecycle.py
|
|
172
174
|
tests/test_windows_ui_transition.py
|
|
173
175
|
tests/test_windows_wrapper_contract.py
|