railmux 0.4.0.dev27__tar.gz → 0.4.0.dev29__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.dev27/src/railmux.egg-info → railmux-0.4.0.dev29}/PKG-INFO +1 -1
- railmux-0.4.0.dev29/src/railmux/__init__.py +1 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/windows_install_log.py +32 -3
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/windows_msys2.py +429 -23
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29/src/railmux.egg-info}/PKG-INFO +1 -1
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_windows_install_log.py +52 -2
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_windows_msys2.py +371 -1
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tools/windows_archive_smoke.py +54 -0
- railmux-0.4.0.dev27/src/railmux/__init__.py +0 -1
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/LICENSE +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/MANIFEST.in +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/README.md +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/pyproject.toml +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/setup.cfg +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/__main__.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/atomic_file.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/background_index.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/cli.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/codex_index.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/config.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/config_cli.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/diagnostics.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/discovery.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/display_transport.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/entrypoint.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/fast_display_client.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/fast_display_history.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/fast_display_input.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/fast_display_protocol.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/fast_display_server.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/favorites.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/fuzzy.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/help_workspace.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/launcher.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/legacy_sessions.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/local_clipboard.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/local_open.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/models.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/modes.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/mouse_manager.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/orphan_marker.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/pane_surface.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/path_codec.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/provider_paths.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/remote_config.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/renames.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/restart_state.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/runtime_config.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/scroll_agent.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/scroll_manager.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/selection_isolation.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/self_update.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/session_cache.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/session_index.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/session_lease.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/setting_contracts.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/settings.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/ssh_args.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/ssh_compat.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/ssh_display_diagnostics.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/ssh_doctor.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/system_deps.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/terminal_status.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/tmux_binding_manager.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/tmux_ctl.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/tmux_health.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/tmux_server.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/tool_panes.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/transcript.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/ui/__init__.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/ui/_widgets.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/ui/app.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/ui/keymap.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/ui/modals.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/ui/projects_pane.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/ui/running_pane.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/ui/sessions_pane.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/ui/sidebar.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/ui/statusbar.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/ui/workspace.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/windows_attach_relay.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/windows_bootstrap.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/windows_pacman.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/windows_tmux_lifecycle.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/windows_ui_transition.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux.egg-info/SOURCES.txt +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux.egg-info/dependency_links.txt +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux.egg-info/entry_points.txt +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux.egg-info/requires.txt +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux.egg-info/top_level.txt +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_agent_workspace.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_atomic_file.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_background_index.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_cli.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_codex_delete.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_codex_index.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_codex_lineage.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_codex_transcript.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_config.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_config_cli.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_denested_transport_benchmark.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_diagnostics.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_discovery.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_display_transport.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_fast_full_window.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_focus_highlight.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_fullscreen_binding.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_fuzzy.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_help_agent.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_help_workspace.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_history_cleanup.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_keymap.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_launcher.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_layout_profile.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_legacy_sessions.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_local_clipboard.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_local_open.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_modals.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_mode_selection.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_modes.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_mouse_manager.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_new_project.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_options.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_orphan_recovery.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_pane_surface.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_path_codec.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_projects_pane.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_provider_paths.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_release_notes.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_release_workflow.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_remote_config.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_renames.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_restart_state.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_running_pane.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_running_sort.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_runtime_config.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_scroll_agent.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_scroll_manager.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_selection_isolation.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_self_update.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_session_cache.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_session_index.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_session_lease.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_sessions_pane.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_setting_contracts.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_settings.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_sidebar.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_soft_quit.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_ssh_args.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_ssh_compat.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_ssh_display_benchmark.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_ssh_display_diagnostics.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_ssh_doctor.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_status.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_statusbar.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_system_deps.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_terminal_size.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_terminal_status.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_termux_local_touch.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_tmux_binding_manager.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_tmux_ctl.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_tmux_health.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_tmux_integration.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_tmux_server.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_tmux_status.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_tool_panes.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_transcript.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_utf8_state.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_web_demo_recorder.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_wheel_smoke.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_widgets.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_windows_attach_relay.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_windows_bootstrap.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_windows_pacman.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_windows_tmux_lifecycle.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_windows_ui_transition.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_windows_wrapper_contract.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_yolo_prompt.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tools/denested_transport_benchmark.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tools/release_notes.py +0 -0
- {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tools/wheel_smoke.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.0.dev29"
|
|
@@ -56,6 +56,11 @@ _PIP_NETWORK_ERROR_MARKERS = (
|
|
|
56
56
|
"connection aborted",
|
|
57
57
|
"temporary failure in name resolution",
|
|
58
58
|
)
|
|
59
|
+
_MSYS2_RESTART_MARKER = (
|
|
60
|
+
"to complete this update all msys2 processes including this terminal "
|
|
61
|
+
"will be closed"
|
|
62
|
+
)
|
|
63
|
+
_ANSI_ESCAPE_RE = re.compile(r"\x1b\[[0-?]*[ -/]*[@-~]")
|
|
59
64
|
_EXTRACTION_PERCENT_RE = re.compile(r"(?:^|\s)([0-9]{1,3})%\s")
|
|
60
65
|
_PACKAGE_COUNT_RE = re.compile(r"^Packages \(([0-9]+)\)")
|
|
61
66
|
_PACKAGE_DOWNLOAD_RE = re.compile(r"^\s*([^ ]+) downloading\.\.\.\s*$")
|
|
@@ -87,7 +92,12 @@ def install_log_path(
|
|
|
87
92
|
)
|
|
88
93
|
|
|
89
94
|
|
|
90
|
-
def _prune_install_logs(
|
|
95
|
+
def _prune_install_logs(
|
|
96
|
+
directory: Path,
|
|
97
|
+
*,
|
|
98
|
+
keep: int = _LOG_KEEP_COUNT,
|
|
99
|
+
preserve: Path | None = None,
|
|
100
|
+
) -> None:
|
|
91
101
|
candidates: list[Path] = []
|
|
92
102
|
try:
|
|
93
103
|
entries = list(directory.iterdir())
|
|
@@ -103,7 +113,11 @@ def _prune_install_logs(directory: Path, *, keep: int = _LOG_KEEP_COUNT) -> None
|
|
|
103
113
|
)
|
|
104
114
|
except OSError:
|
|
105
115
|
return
|
|
106
|
-
|
|
116
|
+
preserved = preserve if preserve in candidates else None
|
|
117
|
+
others = [candidate for candidate in candidates if candidate != preserved]
|
|
118
|
+
other_limit = max(keep, 1) - (1 if preserved is not None else 0)
|
|
119
|
+
stale_candidates = others[max(other_limit, 0) :]
|
|
120
|
+
for stale in stale_candidates:
|
|
107
121
|
try:
|
|
108
122
|
stale.unlink()
|
|
109
123
|
except OSError:
|
|
@@ -142,12 +156,16 @@ class InstallReporter:
|
|
|
142
156
|
self._network_failure = False
|
|
143
157
|
self._pip_network_failure = False
|
|
144
158
|
self._hard_failed_hosts: set[str] = set()
|
|
159
|
+
self._msys2_restart_requested = False
|
|
160
|
+
self._msys2_restart_lines: deque[str] = deque(maxlen=3)
|
|
145
161
|
|
|
146
162
|
def __enter__(self) -> InstallReporter:
|
|
147
163
|
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
148
164
|
self._log = self.path.open("x", encoding="utf-8", newline="\n")
|
|
149
165
|
self._write_log("Railmux Windows runtime installation\n")
|
|
150
|
-
|
|
166
|
+
# Windows may defer the visible last-write timestamp of an open file.
|
|
167
|
+
# Never let retention mistake the active evidence log for an old one.
|
|
168
|
+
_prune_install_logs(self.path.parent, preserve=self.path)
|
|
151
169
|
return self
|
|
152
170
|
|
|
153
171
|
def __exit__(self, _exc_type, _exc, _traceback) -> None:
|
|
@@ -209,6 +227,8 @@ class InstallReporter:
|
|
|
209
227
|
self._network_failure = False
|
|
210
228
|
self._pip_network_failure = False
|
|
211
229
|
self._hard_failed_hosts.clear()
|
|
230
|
+
self._msys2_restart_requested = False
|
|
231
|
+
self._msys2_restart_lines.clear()
|
|
212
232
|
self._write_log(f"\n--- {label} ---\n")
|
|
213
233
|
|
|
214
234
|
def command_output(self, output: bytes | str | None) -> None:
|
|
@@ -259,6 +279,11 @@ class InstallReporter:
|
|
|
259
279
|
if line.strip():
|
|
260
280
|
self._tail.append(line)
|
|
261
281
|
lowered = line.lower()
|
|
282
|
+
normalized = " ".join(_ANSI_ESCAPE_RE.sub("", lowered).split())
|
|
283
|
+
if normalized:
|
|
284
|
+
self._msys2_restart_lines.append(normalized)
|
|
285
|
+
if _MSYS2_RESTART_MARKER in " ".join(self._msys2_restart_lines):
|
|
286
|
+
self._msys2_restart_requested = True
|
|
262
287
|
if any(marker in lowered for marker in _NETWORK_ERROR_MARKERS):
|
|
263
288
|
self._network_failure = True
|
|
264
289
|
if any(marker in lowered for marker in _PIP_NETWORK_ERROR_MARKERS):
|
|
@@ -364,6 +389,10 @@ class InstallReporter:
|
|
|
364
389
|
def hard_failed_mirror_hosts(self) -> frozenset[str]:
|
|
365
390
|
return frozenset(self._hard_failed_hosts)
|
|
366
391
|
|
|
392
|
+
@property
|
|
393
|
+
def command_requested_msys2_restart(self) -> bool:
|
|
394
|
+
return self._msys2_restart_requested
|
|
395
|
+
|
|
367
396
|
def heartbeat(self) -> None:
|
|
368
397
|
now = self._clock()
|
|
369
398
|
if now - self._last_heartbeat < _HEARTBEAT_SECONDS:
|
|
@@ -119,6 +119,32 @@ _PACKAGE_URL_COMMAND = (
|
|
|
119
119
|
_PACKAGE_INVENTORY_COMMAND = "set -o pipefail; pacman -Q | LC_ALL=C sort"
|
|
120
120
|
_PACKAGE_INVENTORY_LIMIT = 1024 * 1024
|
|
121
121
|
_CORE_RUNTIME_PACKAGES = ("tmux", "python", "python-pip")
|
|
122
|
+
_MAX_BASE_UPDATE_PASSES = 3
|
|
123
|
+
_PACMAN_LOG_RELATIVE = Path("var") / "log" / "pacman.log"
|
|
124
|
+
_PACMAN_LOG_MAX_SIZE = 8 * 1024 * 1024
|
|
125
|
+
_PACMAN_LOG_APPEND_LIMIT = 2 * 1024 * 1024
|
|
126
|
+
_PACMAN_LOCAL_DB_ENTRY_LIMIT = 4096
|
|
127
|
+
_PACMAN_LOCAL_DESC_LIMIT = 64 * 1024
|
|
128
|
+
# Keep this aligned with alpm_pkg_is_core_package in the pacman build shipped
|
|
129
|
+
# by the pinned base. Any one of these packages makes MSYS2 close its processes.
|
|
130
|
+
_PACMAN_CORE_PACKAGES = frozenset(
|
|
131
|
+
{
|
|
132
|
+
"bash",
|
|
133
|
+
"filesystem",
|
|
134
|
+
"mintty",
|
|
135
|
+
"msys2-runtime",
|
|
136
|
+
"msys2-runtime-devel",
|
|
137
|
+
"pacman",
|
|
138
|
+
"pacman-mirrors",
|
|
139
|
+
}
|
|
140
|
+
)
|
|
141
|
+
_PACMAN_CORE_CHANGE_RE = re.compile(
|
|
142
|
+
r"\[ALPM] (?:upgraded|downgraded) ([^ ]+) \(([^ ]+) -> ([^)]+)\)",
|
|
143
|
+
re.IGNORECASE,
|
|
144
|
+
)
|
|
145
|
+
_PRIVATE_GPG_SHUTDOWN_COMMAND = (
|
|
146
|
+
"gpgconf --homedir /etc/pacman.d/gnupg --kill all"
|
|
147
|
+
)
|
|
122
148
|
_IN_USE_APPS_COMMAND = (
|
|
123
149
|
"for f in /proc/[0-9]*/cmdline; do "
|
|
124
150
|
"[ -r \"$f\" ] || continue; tr '\\0' '\\n' <\"$f\"; done"
|
|
@@ -196,6 +222,16 @@ ArchiveProbe = Callable[[str, str, int], _ArchiveProbe]
|
|
|
196
222
|
ArchiveResume = Callable[[str, str, int, int, Callable[[bytes], None]], None]
|
|
197
223
|
|
|
198
224
|
|
|
225
|
+
@dataclass(frozen=True)
|
|
226
|
+
class _PacmanLogCheckpoint:
|
|
227
|
+
existed: bool
|
|
228
|
+
size: int
|
|
229
|
+
identity: tuple[int, int] | None
|
|
230
|
+
tail_offset: int
|
|
231
|
+
tail_digest: str
|
|
232
|
+
core_versions: tuple[tuple[str, str], ...]
|
|
233
|
+
|
|
234
|
+
|
|
199
235
|
def _format_download_size(size: int) -> str:
|
|
200
236
|
if size >= 1024 * 1024:
|
|
201
237
|
return f"{size / (1024 * 1024):.1f} MiB"
|
|
@@ -1624,18 +1660,25 @@ def _run_checked(
|
|
|
1624
1660
|
if returncode:
|
|
1625
1661
|
if allow_failure:
|
|
1626
1662
|
return returncode
|
|
1627
|
-
|
|
1628
|
-
reporter.note(
|
|
1629
|
-
"Windows reported STATUS_DLL_NOT_FOUND. Check whether security "
|
|
1630
|
-
"software quarantined a file in the private MSYS2 staging tree.",
|
|
1631
|
-
level="error",
|
|
1632
|
-
)
|
|
1663
|
+
_explain_windows_status(returncode, reporter)
|
|
1633
1664
|
reporter.command_failed(label, returncode)
|
|
1634
1665
|
raise RuntimeInstallError(f"{label} failed with exit code {returncode}")
|
|
1635
1666
|
reporter.command_succeeded()
|
|
1636
1667
|
return 0
|
|
1637
1668
|
|
|
1638
1669
|
|
|
1670
|
+
def _explain_windows_status(returncode: int, reporter: InstallReporter) -> None:
|
|
1671
|
+
if _NATIVE_WINDOWS and returncode & 0xFFFFFFFF == 0xC0000135:
|
|
1672
|
+
reporter.note(
|
|
1673
|
+
"Windows reported STATUS_DLL_NOT_FOUND, but this pass had neither "
|
|
1674
|
+
"pacman's restart announcement nor a newly completed core "
|
|
1675
|
+
"transaction with a matching package-database version change. "
|
|
1676
|
+
"The temporary base will not be published; a missing or "
|
|
1677
|
+
"quarantined staged DLL remains possible.",
|
|
1678
|
+
level="error",
|
|
1679
|
+
)
|
|
1680
|
+
|
|
1681
|
+
|
|
1639
1682
|
def _pacman_command(*, packages: bool, relaxed: bool) -> str:
|
|
1640
1683
|
timeout = " --disable-download-timeout" if relaxed else ""
|
|
1641
1684
|
needed = f" --needed {_PACMAN_PACKAGES}" if packages else ""
|
|
@@ -1644,10 +1687,11 @@ def _pacman_command(*, packages: bool, relaxed: bool) -> str:
|
|
|
1644
1687
|
if not packages
|
|
1645
1688
|
else ""
|
|
1646
1689
|
)
|
|
1690
|
+
operation = "-Syu" if packages else "-Syuu"
|
|
1647
1691
|
return (
|
|
1648
1692
|
'cache=$(cygpath -u "$1") && mkdir -p "$cache" && '
|
|
1649
1693
|
f"{keyring}pacman --config {_PACMAN_CONFIG} --cachedir \"$cache\" "
|
|
1650
|
-
f"
|
|
1694
|
+
f"{operation} --noconfirm{timeout}{needed}"
|
|
1651
1695
|
)
|
|
1652
1696
|
|
|
1653
1697
|
|
|
@@ -1666,6 +1710,215 @@ def _report_mirror_decision(
|
|
|
1666
1710
|
)
|
|
1667
1711
|
|
|
1668
1712
|
|
|
1713
|
+
def _pacman_desc_value(text: str, field: str) -> str | None:
|
|
1714
|
+
lines = text.splitlines()
|
|
1715
|
+
marker = f"%{field}%"
|
|
1716
|
+
for index, line in enumerate(lines[:-1]):
|
|
1717
|
+
if line == marker and lines[index + 1]:
|
|
1718
|
+
return lines[index + 1]
|
|
1719
|
+
return None
|
|
1720
|
+
|
|
1721
|
+
|
|
1722
|
+
def _pacman_core_versions(root: Path) -> dict[str, str]:
|
|
1723
|
+
local_db = root / "var" / "lib" / "pacman" / "local"
|
|
1724
|
+
try:
|
|
1725
|
+
if _path_is_link_or_reparse(local_db) or not local_db.is_dir():
|
|
1726
|
+
raise RuntimeInstallError("the private pacman database is unsafe")
|
|
1727
|
+
entries = list(local_db.iterdir())
|
|
1728
|
+
except FileNotFoundError:
|
|
1729
|
+
return {}
|
|
1730
|
+
except OSError as exc:
|
|
1731
|
+
raise RuntimeInstallError("could not inspect the private pacman database") from exc
|
|
1732
|
+
if len(entries) > _PACMAN_LOCAL_DB_ENTRY_LIMIT:
|
|
1733
|
+
raise RuntimeInstallError("the private pacman database is unexpectedly large")
|
|
1734
|
+
versions: dict[str, str] = {}
|
|
1735
|
+
for entry in entries:
|
|
1736
|
+
if not any(
|
|
1737
|
+
entry.name.startswith(prefix)
|
|
1738
|
+
for prefix in (
|
|
1739
|
+
"bash-",
|
|
1740
|
+
"filesystem-",
|
|
1741
|
+
"mintty-",
|
|
1742
|
+
"msys2-runtime-",
|
|
1743
|
+
"pacman-",
|
|
1744
|
+
)
|
|
1745
|
+
):
|
|
1746
|
+
continue
|
|
1747
|
+
desc = entry / "desc"
|
|
1748
|
+
try:
|
|
1749
|
+
if (
|
|
1750
|
+
_path_is_link_or_reparse(entry)
|
|
1751
|
+
or not entry.is_dir()
|
|
1752
|
+
or _path_is_link_or_reparse(desc)
|
|
1753
|
+
or not desc.is_file()
|
|
1754
|
+
or desc.stat().st_size > _PACMAN_LOCAL_DESC_LIMIT
|
|
1755
|
+
):
|
|
1756
|
+
raise RuntimeInstallError("the private pacman database is unsafe")
|
|
1757
|
+
text = desc.read_text(encoding="utf-8")
|
|
1758
|
+
except RuntimeInstallError:
|
|
1759
|
+
raise
|
|
1760
|
+
except (OSError, UnicodeError) as exc:
|
|
1761
|
+
raise RuntimeInstallError(
|
|
1762
|
+
"could not read the private pacman database"
|
|
1763
|
+
) from exc
|
|
1764
|
+
name = _pacman_desc_value(text, "NAME")
|
|
1765
|
+
version = _pacman_desc_value(text, "VERSION")
|
|
1766
|
+
if name is None or version is None or not _is_pacman_core_package(name):
|
|
1767
|
+
continue
|
|
1768
|
+
if name in versions:
|
|
1769
|
+
raise RuntimeInstallError(
|
|
1770
|
+
"the private pacman database contains duplicate core packages"
|
|
1771
|
+
)
|
|
1772
|
+
versions[name] = version
|
|
1773
|
+
return versions
|
|
1774
|
+
|
|
1775
|
+
|
|
1776
|
+
def _pacman_log_checkpoint(root: Path) -> _PacmanLogCheckpoint:
|
|
1777
|
+
"""Pin the append-only pacman journal before a staged transaction."""
|
|
1778
|
+
path = root / _PACMAN_LOG_RELATIVE
|
|
1779
|
+
try:
|
|
1780
|
+
info = path.lstat()
|
|
1781
|
+
except FileNotFoundError:
|
|
1782
|
+
return _PacmanLogCheckpoint(
|
|
1783
|
+
False,
|
|
1784
|
+
0,
|
|
1785
|
+
None,
|
|
1786
|
+
0,
|
|
1787
|
+
hashlib.sha256().hexdigest(),
|
|
1788
|
+
tuple(sorted(_pacman_core_versions(root).items())),
|
|
1789
|
+
)
|
|
1790
|
+
except OSError as exc:
|
|
1791
|
+
raise RuntimeInstallError("could not inspect the private pacman log") from exc
|
|
1792
|
+
try:
|
|
1793
|
+
unsafe = _path_is_link_or_reparse(path)
|
|
1794
|
+
except OSError as exc:
|
|
1795
|
+
raise RuntimeInstallError("could not inspect the private pacman log") from exc
|
|
1796
|
+
if (
|
|
1797
|
+
unsafe
|
|
1798
|
+
or not stat.S_ISREG(info.st_mode)
|
|
1799
|
+
or info.st_size < 0
|
|
1800
|
+
or info.st_size > _PACMAN_LOG_MAX_SIZE
|
|
1801
|
+
):
|
|
1802
|
+
raise RuntimeInstallError("the private pacman log is unsafe")
|
|
1803
|
+
tail_offset = max(0, info.st_size - 4096)
|
|
1804
|
+
try:
|
|
1805
|
+
with path.open("rb") as stream:
|
|
1806
|
+
stream.seek(tail_offset)
|
|
1807
|
+
tail = stream.read(info.st_size - tail_offset)
|
|
1808
|
+
except OSError as exc:
|
|
1809
|
+
raise RuntimeInstallError("could not read the private pacman log") from exc
|
|
1810
|
+
return _PacmanLogCheckpoint(
|
|
1811
|
+
True,
|
|
1812
|
+
info.st_size,
|
|
1813
|
+
(info.st_dev, info.st_ino),
|
|
1814
|
+
tail_offset,
|
|
1815
|
+
hashlib.sha256(tail).hexdigest(),
|
|
1816
|
+
tuple(sorted(_pacman_core_versions(root).items())),
|
|
1817
|
+
)
|
|
1818
|
+
|
|
1819
|
+
|
|
1820
|
+
def _is_pacman_core_package(name: str) -> bool:
|
|
1821
|
+
return name in _PACMAN_CORE_PACKAGES or name.startswith("msys2-runtime-")
|
|
1822
|
+
|
|
1823
|
+
|
|
1824
|
+
def _pacman_log_core_transaction(
|
|
1825
|
+
root: Path, checkpoint: _PacmanLogCheckpoint
|
|
1826
|
+
) -> tuple[str, ...]:
|
|
1827
|
+
"""Return core packages from one newly completed journal transaction."""
|
|
1828
|
+
path = root / _PACMAN_LOG_RELATIVE
|
|
1829
|
+
try:
|
|
1830
|
+
info = path.lstat()
|
|
1831
|
+
unsafe = _path_is_link_or_reparse(path)
|
|
1832
|
+
except OSError:
|
|
1833
|
+
return ()
|
|
1834
|
+
if (
|
|
1835
|
+
unsafe
|
|
1836
|
+
or not stat.S_ISREG(info.st_mode)
|
|
1837
|
+
or info.st_size < checkpoint.size
|
|
1838
|
+
or info.st_size > _PACMAN_LOG_MAX_SIZE
|
|
1839
|
+
or info.st_size - checkpoint.size > _PACMAN_LOG_APPEND_LIMIT
|
|
1840
|
+
):
|
|
1841
|
+
return ()
|
|
1842
|
+
if checkpoint.existed and (info.st_dev, info.st_ino) != checkpoint.identity:
|
|
1843
|
+
return ()
|
|
1844
|
+
try:
|
|
1845
|
+
with path.open("rb") as stream:
|
|
1846
|
+
if checkpoint.existed:
|
|
1847
|
+
stream.seek(checkpoint.tail_offset)
|
|
1848
|
+
previous_tail = stream.read(checkpoint.size - checkpoint.tail_offset)
|
|
1849
|
+
if hashlib.sha256(previous_tail).hexdigest() != checkpoint.tail_digest:
|
|
1850
|
+
return ()
|
|
1851
|
+
stream.seek(checkpoint.size)
|
|
1852
|
+
appended = stream.read(_PACMAN_LOG_APPEND_LIMIT + 1)
|
|
1853
|
+
except OSError:
|
|
1854
|
+
return ()
|
|
1855
|
+
if not appended or len(appended) > _PACMAN_LOG_APPEND_LIMIT:
|
|
1856
|
+
return ()
|
|
1857
|
+
|
|
1858
|
+
core_started = False
|
|
1859
|
+
transaction_started = False
|
|
1860
|
+
changed: dict[str, tuple[str, str]] = {}
|
|
1861
|
+
for line in appended.decode("utf-8", errors="replace").splitlines():
|
|
1862
|
+
lowered = line.lower()
|
|
1863
|
+
if "[pacman] starting core system upgrade" in lowered:
|
|
1864
|
+
core_started = True
|
|
1865
|
+
transaction_started = False
|
|
1866
|
+
changed.clear()
|
|
1867
|
+
continue
|
|
1868
|
+
if not core_started:
|
|
1869
|
+
continue
|
|
1870
|
+
if "[alpm] transaction started" in lowered:
|
|
1871
|
+
transaction_started = True
|
|
1872
|
+
changed.clear()
|
|
1873
|
+
continue
|
|
1874
|
+
if not transaction_started:
|
|
1875
|
+
continue
|
|
1876
|
+
match = _PACMAN_CORE_CHANGE_RE.search(line)
|
|
1877
|
+
if match is not None and _is_pacman_core_package(match.group(1)):
|
|
1878
|
+
changed[match.group(1)] = (match.group(2), match.group(3))
|
|
1879
|
+
if "[alpm] transaction completed" in lowered:
|
|
1880
|
+
if changed:
|
|
1881
|
+
try:
|
|
1882
|
+
after = _pacman_core_versions(root)
|
|
1883
|
+
except RuntimeInstallError:
|
|
1884
|
+
return ()
|
|
1885
|
+
before = dict(checkpoint.core_versions)
|
|
1886
|
+
proven = tuple(
|
|
1887
|
+
sorted(
|
|
1888
|
+
name
|
|
1889
|
+
for name, (old_version, new_version) in changed.items()
|
|
1890
|
+
if before.get(name) == old_version
|
|
1891
|
+
and after.get(name) == new_version
|
|
1892
|
+
and old_version != new_version
|
|
1893
|
+
)
|
|
1894
|
+
)
|
|
1895
|
+
if proven:
|
|
1896
|
+
return proven
|
|
1897
|
+
transaction_started = False
|
|
1898
|
+
return ()
|
|
1899
|
+
|
|
1900
|
+
|
|
1901
|
+
def _restart_evidence(
|
|
1902
|
+
root: Path,
|
|
1903
|
+
checkpoint: _PacmanLogCheckpoint,
|
|
1904
|
+
reporter: InstallReporter,
|
|
1905
|
+
returncode: int,
|
|
1906
|
+
) -> bool:
|
|
1907
|
+
if reporter.command_requested_msys2_restart:
|
|
1908
|
+
return True
|
|
1909
|
+
if returncode & 0xFFFFFFFF != 0xC0000135:
|
|
1910
|
+
return False
|
|
1911
|
+
packages = _pacman_log_core_transaction(root, checkpoint)
|
|
1912
|
+
if not packages:
|
|
1913
|
+
return False
|
|
1914
|
+
reporter.note(
|
|
1915
|
+
"The private pacman journal confirms a completed core update "
|
|
1916
|
+
f"({', '.join(packages)}); its final console prompt was not captured.",
|
|
1917
|
+
level="warning",
|
|
1918
|
+
)
|
|
1919
|
+
return True
|
|
1920
|
+
|
|
1921
|
+
|
|
1669
1922
|
def _refresh_pacman_mirrors(
|
|
1670
1923
|
root: Path,
|
|
1671
1924
|
*,
|
|
@@ -1693,7 +1946,9 @@ def _run_pacman_with_recovery(
|
|
|
1693
1946
|
runner: Runner | None,
|
|
1694
1947
|
label: str,
|
|
1695
1948
|
mirror_optimizer: MirrorOptimizer,
|
|
1696
|
-
|
|
1949
|
+
allow_core_restart: bool = False,
|
|
1950
|
+
) -> bool:
|
|
1951
|
+
checkpoint = _pacman_log_checkpoint(root) if not packages else None
|
|
1697
1952
|
argv = _bash_command(root, _pacman_command(packages=packages, relaxed=False), str(cache))
|
|
1698
1953
|
returncode = _run_checked(
|
|
1699
1954
|
argv,
|
|
@@ -1704,11 +1959,34 @@ def _run_pacman_with_recovery(
|
|
|
1704
1959
|
progress="pacman",
|
|
1705
1960
|
allow_failure=True,
|
|
1706
1961
|
)
|
|
1962
|
+
restart_requested = (
|
|
1963
|
+
_restart_evidence(root, checkpoint, reporter, returncode)
|
|
1964
|
+
if checkpoint is not None
|
|
1965
|
+
else False
|
|
1966
|
+
)
|
|
1707
1967
|
if returncode == 0:
|
|
1708
|
-
return
|
|
1968
|
+
return restart_requested
|
|
1969
|
+
if not packages and restart_requested and not allow_core_restart:
|
|
1970
|
+
reporter.note(
|
|
1971
|
+
"MSYS2 requested another core restart on the final permitted "
|
|
1972
|
+
"update pass; the temporary base will not be published.",
|
|
1973
|
+
level="error",
|
|
1974
|
+
)
|
|
1975
|
+
reporter.command_failed(label, returncode)
|
|
1976
|
+
raise RuntimeInstallError(
|
|
1977
|
+
f"{label} exhausted the permitted core-update restarts"
|
|
1978
|
+
)
|
|
1979
|
+
if allow_core_restart and not packages and restart_requested:
|
|
1980
|
+
reporter.note(
|
|
1981
|
+
"MSYS2 replaced core runtime files and closed its own process as "
|
|
1982
|
+
"expected; continuing from a fresh Windows-launched shell.",
|
|
1983
|
+
level="warning",
|
|
1984
|
+
)
|
|
1985
|
+
return True
|
|
1709
1986
|
network_failure = reporter.command_had_network_failure
|
|
1710
1987
|
hard_failed_hosts = reporter.hard_failed_mirror_hosts
|
|
1711
1988
|
if not network_failure:
|
|
1989
|
+
_explain_windows_status(returncode, reporter)
|
|
1712
1990
|
reporter.command_failed(label, returncode)
|
|
1713
1991
|
raise RuntimeInstallError(f"{label} failed with exit code {returncode}")
|
|
1714
1992
|
reporter.note(
|
|
@@ -1739,13 +2017,128 @@ def _run_pacman_with_recovery(
|
|
|
1739
2017
|
_pacman_command(packages=packages, relaxed=True),
|
|
1740
2018
|
str(cache),
|
|
1741
2019
|
)
|
|
1742
|
-
|
|
2020
|
+
retry_checkpoint = _pacman_log_checkpoint(root) if not packages else None
|
|
2021
|
+
retry_returncode = _run_checked(
|
|
1743
2022
|
retry_argv,
|
|
1744
2023
|
env=env,
|
|
1745
2024
|
reporter=reporter,
|
|
1746
2025
|
runner=runner,
|
|
1747
2026
|
label=f"{label} resilient retry",
|
|
1748
2027
|
progress="pacman",
|
|
2028
|
+
allow_failure=True,
|
|
2029
|
+
)
|
|
2030
|
+
restart_requested = (
|
|
2031
|
+
_restart_evidence(root, retry_checkpoint, reporter, retry_returncode)
|
|
2032
|
+
if retry_checkpoint is not None
|
|
2033
|
+
else False
|
|
2034
|
+
)
|
|
2035
|
+
if retry_returncode == 0:
|
|
2036
|
+
return restart_requested
|
|
2037
|
+
if not packages and restart_requested and not allow_core_restart:
|
|
2038
|
+
reporter.note(
|
|
2039
|
+
"MSYS2 requested another core restart on the final permitted "
|
|
2040
|
+
"update pass; the temporary base will not be published.",
|
|
2041
|
+
level="error",
|
|
2042
|
+
)
|
|
2043
|
+
reporter.command_failed(f"{label} resilient retry", retry_returncode)
|
|
2044
|
+
raise RuntimeInstallError(
|
|
2045
|
+
f"{label} resilient retry exhausted the permitted "
|
|
2046
|
+
"core-update restarts"
|
|
2047
|
+
)
|
|
2048
|
+
if allow_core_restart and not packages and restart_requested:
|
|
2049
|
+
reporter.note(
|
|
2050
|
+
"MSYS2 replaced core runtime files and closed its own process as "
|
|
2051
|
+
"expected; continuing from a fresh Windows-launched shell.",
|
|
2052
|
+
level="warning",
|
|
2053
|
+
)
|
|
2054
|
+
return True
|
|
2055
|
+
_explain_windows_status(retry_returncode, reporter)
|
|
2056
|
+
reporter.command_failed(f"{label} resilient retry", retry_returncode)
|
|
2057
|
+
raise RuntimeInstallError(
|
|
2058
|
+
f"{label} resilient retry failed with exit code {retry_returncode}"
|
|
2059
|
+
)
|
|
2060
|
+
|
|
2061
|
+
|
|
2062
|
+
def _run_base_update_with_restarts(
|
|
2063
|
+
root: Path,
|
|
2064
|
+
*,
|
|
2065
|
+
cache: Path,
|
|
2066
|
+
env: Mapping[str, str],
|
|
2067
|
+
reporter: InstallReporter,
|
|
2068
|
+
runner: Runner | None,
|
|
2069
|
+
mirror_optimizer: MirrorOptimizer,
|
|
2070
|
+
) -> None:
|
|
2071
|
+
"""Run MSYS2's full upgrade in fresh processes until restart-safe."""
|
|
2072
|
+
try:
|
|
2073
|
+
for pass_number in range(1, _MAX_BASE_UPDATE_PASSES + 1):
|
|
2074
|
+
restart_requested = _run_pacman_with_recovery(
|
|
2075
|
+
root,
|
|
2076
|
+
packages=False,
|
|
2077
|
+
cache=cache,
|
|
2078
|
+
env=env,
|
|
2079
|
+
reporter=reporter,
|
|
2080
|
+
runner=runner,
|
|
2081
|
+
label=f"MSYS2 base update pass {pass_number}",
|
|
2082
|
+
mirror_optimizer=mirror_optimizer,
|
|
2083
|
+
allow_core_restart=pass_number < _MAX_BASE_UPDATE_PASSES,
|
|
2084
|
+
)
|
|
2085
|
+
if restart_requested:
|
|
2086
|
+
reporter.note(
|
|
2087
|
+
"Starting a new MSYS2 process for the remaining update…"
|
|
2088
|
+
)
|
|
2089
|
+
continue
|
|
2090
|
+
if pass_number == 1:
|
|
2091
|
+
# MSYS2's supported unattended update procedure invokes Syuu
|
|
2092
|
+
# twice, even when the first pass did not request a restart.
|
|
2093
|
+
reporter.note(
|
|
2094
|
+
"Starting the required second MSYS2 update pass in a new "
|
|
2095
|
+
"process…"
|
|
2096
|
+
)
|
|
2097
|
+
continue
|
|
2098
|
+
return
|
|
2099
|
+
raise RuntimeInstallError(
|
|
2100
|
+
"MSYS2 repeatedly requested a core-runtime restart; the temporary "
|
|
2101
|
+
"base was not published"
|
|
2102
|
+
)
|
|
2103
|
+
finally:
|
|
2104
|
+
_stop_private_gpg_agents(
|
|
2105
|
+
root,
|
|
2106
|
+
env=env,
|
|
2107
|
+
reporter=reporter,
|
|
2108
|
+
runner=runner,
|
|
2109
|
+
strict=sys.exc_info()[0] is None,
|
|
2110
|
+
)
|
|
2111
|
+
|
|
2112
|
+
|
|
2113
|
+
def _stop_private_gpg_agents(
|
|
2114
|
+
root: Path,
|
|
2115
|
+
*,
|
|
2116
|
+
env: Mapping[str, str],
|
|
2117
|
+
reporter: InstallReporter,
|
|
2118
|
+
runner: Runner | None,
|
|
2119
|
+
strict: bool,
|
|
2120
|
+
) -> None:
|
|
2121
|
+
returncode = _run_checked(
|
|
2122
|
+
_bash_command(root, _PRIVATE_GPG_SHUTDOWN_COMMAND),
|
|
2123
|
+
env=env,
|
|
2124
|
+
reporter=reporter,
|
|
2125
|
+
runner=runner,
|
|
2126
|
+
label="Stopping private MSYS2 keyring agents",
|
|
2127
|
+
allow_failure=True,
|
|
2128
|
+
)
|
|
2129
|
+
if returncode == 0:
|
|
2130
|
+
return
|
|
2131
|
+
if not strict:
|
|
2132
|
+
reporter.note(
|
|
2133
|
+
"Could not stop a private MSYS2 keyring agent while recovering "
|
|
2134
|
+
"from another installation failure.",
|
|
2135
|
+
level="warning",
|
|
2136
|
+
)
|
|
2137
|
+
return
|
|
2138
|
+
reporter.command_failed("Stopping private MSYS2 keyring agents", returncode)
|
|
2139
|
+
raise RuntimeInstallError(
|
|
2140
|
+
"stopping private MSYS2 keyring agents failed with exit code "
|
|
2141
|
+
f"{returncode}"
|
|
1749
2142
|
)
|
|
1750
2143
|
|
|
1751
2144
|
|
|
@@ -2399,16 +2792,20 @@ def install_managed_runtime(
|
|
|
2399
2792
|
"pacman is noninteractive; displayed [Y/n] prompts do "
|
|
2400
2793
|
"not require input."
|
|
2401
2794
|
)
|
|
2795
|
+
reporter.note(
|
|
2796
|
+
"Core updates may close the first MSYS2 process; "
|
|
2797
|
+
"Railmux will start the required fresh process "
|
|
2798
|
+
"automatically.",
|
|
2799
|
+
level="muted",
|
|
2800
|
+
)
|
|
2402
2801
|
package_cache = cache_base / f"pacman-{MSYS2_RUNTIME_ID}"
|
|
2403
2802
|
package_cache.mkdir(parents=True, exist_ok=True)
|
|
2404
|
-
|
|
2803
|
+
_run_base_update_with_restarts(
|
|
2405
2804
|
root,
|
|
2406
|
-
packages=False,
|
|
2407
2805
|
cache=package_cache,
|
|
2408
2806
|
env=child_env,
|
|
2409
2807
|
reporter=reporter,
|
|
2410
2808
|
runner=runner,
|
|
2411
|
-
label="MSYS2 base update",
|
|
2412
2809
|
mirror_optimizer=mirror_optimizer,
|
|
2413
2810
|
)
|
|
2414
2811
|
reporter.done()
|
|
@@ -2445,16 +2842,25 @@ def install_managed_runtime(
|
|
|
2445
2842
|
reporter=reporter,
|
|
2446
2843
|
runner=runner,
|
|
2447
2844
|
)
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2845
|
+
try:
|
|
2846
|
+
_run_pacman_with_recovery(
|
|
2847
|
+
root,
|
|
2848
|
+
packages=True,
|
|
2849
|
+
cache=package_cache,
|
|
2850
|
+
env=child_env,
|
|
2851
|
+
reporter=reporter,
|
|
2852
|
+
runner=runner,
|
|
2853
|
+
label="MSYS2 package installation",
|
|
2854
|
+
mirror_optimizer=mirror_optimizer,
|
|
2855
|
+
)
|
|
2856
|
+
finally:
|
|
2857
|
+
_stop_private_gpg_agents(
|
|
2858
|
+
root,
|
|
2859
|
+
env=child_env,
|
|
2860
|
+
reporter=reporter,
|
|
2861
|
+
runner=runner,
|
|
2862
|
+
strict=sys.exc_info()[0] is None,
|
|
2863
|
+
)
|
|
2458
2864
|
reporter.done()
|
|
2459
2865
|
|
|
2460
2866
|
identity = _ensure_base_content_identity(
|