railmux 0.4.0.dev26__tar.gz → 0.4.0.dev28__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.dev26 → railmux-0.4.0.dev28}/MANIFEST.in +1 -0
- {railmux-0.4.0.dev26/src/railmux.egg-info → railmux-0.4.0.dev28}/PKG-INFO +1 -1
- railmux-0.4.0.dev28/src/railmux/__init__.py +1 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/windows_install_log.py +12 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/windows_msys2.py +168 -19
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28/src/railmux.egg-info}/PKG-INFO +1 -1
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux.egg-info/SOURCES.txt +2 -1
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_windows_install_log.py +41 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_windows_msys2.py +142 -5
- railmux-0.4.0.dev28/tools/windows_archive_smoke.py +85 -0
- railmux-0.4.0.dev26/src/railmux/__init__.py +0 -1
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/LICENSE +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/README.md +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/pyproject.toml +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/setup.cfg +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/__main__.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/atomic_file.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/background_index.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/cli.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/codex_index.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/config.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/config_cli.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/diagnostics.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/discovery.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/display_transport.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/entrypoint.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/fast_display_client.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/fast_display_history.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/fast_display_input.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/fast_display_protocol.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/fast_display_server.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/favorites.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/fuzzy.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/help_workspace.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/launcher.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/legacy_sessions.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/local_clipboard.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/local_open.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/models.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/modes.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/mouse_manager.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/orphan_marker.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/pane_surface.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/path_codec.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/provider_paths.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/remote_config.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/renames.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/restart_state.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/runtime_config.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/scroll_agent.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/scroll_manager.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/selection_isolation.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/self_update.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/session_cache.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/session_index.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/session_lease.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/setting_contracts.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/settings.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/ssh_args.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/ssh_compat.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/ssh_display_diagnostics.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/ssh_doctor.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/system_deps.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/terminal_status.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/tmux_binding_manager.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/tmux_ctl.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/tmux_health.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/tmux_server.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/tool_panes.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/transcript.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/ui/__init__.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/ui/_widgets.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/ui/app.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/ui/keymap.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/ui/modals.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/ui/projects_pane.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/ui/running_pane.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/ui/sessions_pane.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/ui/sidebar.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/ui/statusbar.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/ui/workspace.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/windows_attach_relay.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/windows_bootstrap.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/windows_pacman.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/windows_tmux_lifecycle.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/windows_ui_transition.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux.egg-info/dependency_links.txt +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux.egg-info/entry_points.txt +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux.egg-info/requires.txt +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux.egg-info/top_level.txt +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_agent_workspace.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_atomic_file.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_background_index.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_cli.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_codex_delete.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_codex_index.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_codex_lineage.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_codex_transcript.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_config.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_config_cli.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_denested_transport_benchmark.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_diagnostics.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_discovery.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_display_transport.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_fast_full_window.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_focus_highlight.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_fullscreen_binding.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_fuzzy.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_help_agent.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_help_workspace.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_history_cleanup.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_keymap.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_launcher.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_layout_profile.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_legacy_sessions.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_local_clipboard.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_local_open.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_modals.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_mode_selection.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_modes.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_mouse_manager.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_new_project.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_options.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_orphan_recovery.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_pane_surface.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_path_codec.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_projects_pane.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_provider_paths.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_release_notes.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_release_workflow.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_remote_config.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_renames.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_restart_state.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_running_pane.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_running_sort.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_runtime_config.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_scroll_agent.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_scroll_manager.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_selection_isolation.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_self_update.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_session_cache.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_session_index.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_session_lease.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_sessions_pane.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_setting_contracts.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_settings.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_sidebar.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_soft_quit.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_ssh_args.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_ssh_compat.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_ssh_display_benchmark.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_ssh_display_diagnostics.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_ssh_doctor.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_status.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_statusbar.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_system_deps.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_terminal_size.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_terminal_status.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_termux_local_touch.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_tmux_binding_manager.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_tmux_ctl.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_tmux_health.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_tmux_integration.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_tmux_server.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_tmux_status.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_tool_panes.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_transcript.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_utf8_state.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_web_demo_recorder.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_wheel_smoke.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_widgets.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_windows_attach_relay.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_windows_bootstrap.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_windows_pacman.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_windows_tmux_lifecycle.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_windows_ui_transition.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_windows_wrapper_contract.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_yolo_prompt.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tools/denested_transport_benchmark.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tools/release_notes.py +0 -0
- {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tools/wheel_smoke.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.0.dev28"
|
|
@@ -56,6 +56,10 @@ _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
|
+
)
|
|
59
63
|
_EXTRACTION_PERCENT_RE = re.compile(r"(?:^|\s)([0-9]{1,3})%\s")
|
|
60
64
|
_PACKAGE_COUNT_RE = re.compile(r"^Packages \(([0-9]+)\)")
|
|
61
65
|
_PACKAGE_DOWNLOAD_RE = re.compile(r"^\s*([^ ]+) downloading\.\.\.\s*$")
|
|
@@ -142,6 +146,7 @@ class InstallReporter:
|
|
|
142
146
|
self._network_failure = False
|
|
143
147
|
self._pip_network_failure = False
|
|
144
148
|
self._hard_failed_hosts: set[str] = set()
|
|
149
|
+
self._msys2_restart_requested = False
|
|
145
150
|
|
|
146
151
|
def __enter__(self) -> InstallReporter:
|
|
147
152
|
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
@@ -209,6 +214,7 @@ class InstallReporter:
|
|
|
209
214
|
self._network_failure = False
|
|
210
215
|
self._pip_network_failure = False
|
|
211
216
|
self._hard_failed_hosts.clear()
|
|
217
|
+
self._msys2_restart_requested = False
|
|
212
218
|
self._write_log(f"\n--- {label} ---\n")
|
|
213
219
|
|
|
214
220
|
def command_output(self, output: bytes | str | None) -> None:
|
|
@@ -259,6 +265,8 @@ class InstallReporter:
|
|
|
259
265
|
if line.strip():
|
|
260
266
|
self._tail.append(line)
|
|
261
267
|
lowered = line.lower()
|
|
268
|
+
if _MSYS2_RESTART_MARKER in lowered:
|
|
269
|
+
self._msys2_restart_requested = True
|
|
262
270
|
if any(marker in lowered for marker in _NETWORK_ERROR_MARKERS):
|
|
263
271
|
self._network_failure = True
|
|
264
272
|
if any(marker in lowered for marker in _PIP_NETWORK_ERROR_MARKERS):
|
|
@@ -364,6 +372,10 @@ class InstallReporter:
|
|
|
364
372
|
def hard_failed_mirror_hosts(self) -> frozenset[str]:
|
|
365
373
|
return frozenset(self._hard_failed_hosts)
|
|
366
374
|
|
|
375
|
+
@property
|
|
376
|
+
def command_requested_msys2_restart(self) -> bool:
|
|
377
|
+
return self._msys2_restart_requested
|
|
378
|
+
|
|
367
379
|
def heartbeat(self) -> None:
|
|
368
380
|
now = self._clock()
|
|
369
381
|
if now - self._last_heartbeat < _HEARTBEAT_SECONDS:
|
|
@@ -80,6 +80,7 @@ MSYS2_BASE_LINEAGE_SHA256 = (
|
|
|
80
80
|
"6fe0cc8154132040e034ff4daface2a4163a9d1f6ebaaa1133394bff460bd5cf"
|
|
81
81
|
)
|
|
82
82
|
MSYS2_RUNTIME_ID = f"msys2-{MSYS2_RELEASE}"
|
|
83
|
+
_NATIVE_WINDOWS = os.name == "nt"
|
|
83
84
|
RUNTIME_SCHEMA = 1
|
|
84
85
|
|
|
85
86
|
_RUNTIME_OVERRIDE = "RAILMUX_MSYS2_ROOT"
|
|
@@ -118,6 +119,10 @@ _PACKAGE_URL_COMMAND = (
|
|
|
118
119
|
_PACKAGE_INVENTORY_COMMAND = "set -o pipefail; pacman -Q | LC_ALL=C sort"
|
|
119
120
|
_PACKAGE_INVENTORY_LIMIT = 1024 * 1024
|
|
120
121
|
_CORE_RUNTIME_PACKAGES = ("tmux", "python", "python-pip")
|
|
122
|
+
_MAX_BASE_UPDATE_PASSES = 3
|
|
123
|
+
_PRIVATE_GPG_SHUTDOWN_COMMAND = (
|
|
124
|
+
"gpgconf --homedir /etc/pacman.d/gnupg --kill all"
|
|
125
|
+
)
|
|
121
126
|
_IN_USE_APPS_COMMAND = (
|
|
122
127
|
"for f in /proc/[0-9]*/cmdline; do "
|
|
123
128
|
"[ -r \"$f\" ] || continue; tr '\\0' '\\n' <\"$f\"; done"
|
|
@@ -621,6 +626,16 @@ def _ensure_safe_archive_directory(root: Path, parts: Sequence[str]) -> Path:
|
|
|
621
626
|
return current
|
|
622
627
|
|
|
623
628
|
|
|
629
|
+
def _apply_archive_mode(path: Path, mode: int) -> None:
|
|
630
|
+
if _NATIVE_WINDOWS:
|
|
631
|
+
# Windows chmod maps a missing owner-write bit to the NTFS read-only
|
|
632
|
+
# attribute; applying tar's POSIX 0444/0555 modes would prevent pacman
|
|
633
|
+
# from replacing those files during the first base update.
|
|
634
|
+
os.chmod(path, stat.S_IWRITE)
|
|
635
|
+
else:
|
|
636
|
+
os.chmod(path, mode)
|
|
637
|
+
|
|
638
|
+
|
|
624
639
|
def _extract_msys2_archive(
|
|
625
640
|
archive: Path,
|
|
626
641
|
destination: Path,
|
|
@@ -696,7 +711,7 @@ def _extract_msys2_archive(
|
|
|
696
711
|
)
|
|
697
712
|
output.write(chunk)
|
|
698
713
|
remaining -= len(chunk)
|
|
699
|
-
|
|
714
|
+
_apply_archive_mode(target, member.mode & 0o777)
|
|
700
715
|
except RuntimeInstallError:
|
|
701
716
|
raise
|
|
702
717
|
except OSError as exc:
|
|
@@ -715,7 +730,7 @@ def _extract_msys2_archive(
|
|
|
715
730
|
"the verified MSYS2 archive expanded to an unexpected size"
|
|
716
731
|
)
|
|
717
732
|
for directory, mode in reversed(directories):
|
|
718
|
-
|
|
733
|
+
_apply_archive_mode(directory, mode)
|
|
719
734
|
except RuntimeInstallError:
|
|
720
735
|
reporter.note(
|
|
721
736
|
"MSYS2 archive extraction failed; the temporary base was not "
|
|
@@ -1613,12 +1628,23 @@ def _run_checked(
|
|
|
1613
1628
|
if returncode:
|
|
1614
1629
|
if allow_failure:
|
|
1615
1630
|
return returncode
|
|
1631
|
+
_explain_windows_status(returncode, reporter)
|
|
1616
1632
|
reporter.command_failed(label, returncode)
|
|
1617
1633
|
raise RuntimeInstallError(f"{label} failed with exit code {returncode}")
|
|
1618
1634
|
reporter.command_succeeded()
|
|
1619
1635
|
return 0
|
|
1620
1636
|
|
|
1621
1637
|
|
|
1638
|
+
def _explain_windows_status(returncode: int, reporter: InstallReporter) -> None:
|
|
1639
|
+
if _NATIVE_WINDOWS and returncode & 0xFFFFFFFF == 0xC0000135:
|
|
1640
|
+
reporter.note(
|
|
1641
|
+
"Windows reported STATUS_DLL_NOT_FOUND outside a confirmed MSYS2 "
|
|
1642
|
+
"core-update restart. Check whether security software quarantined "
|
|
1643
|
+
"a file in the private MSYS2 staging tree.",
|
|
1644
|
+
level="error",
|
|
1645
|
+
)
|
|
1646
|
+
|
|
1647
|
+
|
|
1622
1648
|
def _pacman_command(*, packages: bool, relaxed: bool) -> str:
|
|
1623
1649
|
timeout = " --disable-download-timeout" if relaxed else ""
|
|
1624
1650
|
needed = f" --needed {_PACMAN_PACKAGES}" if packages else ""
|
|
@@ -1627,10 +1653,11 @@ def _pacman_command(*, packages: bool, relaxed: bool) -> str:
|
|
|
1627
1653
|
if not packages
|
|
1628
1654
|
else ""
|
|
1629
1655
|
)
|
|
1656
|
+
operation = "-Syu" if packages else "-Syuu"
|
|
1630
1657
|
return (
|
|
1631
1658
|
'cache=$(cygpath -u "$1") && mkdir -p "$cache" && '
|
|
1632
1659
|
f"{keyring}pacman --config {_PACMAN_CONFIG} --cachedir \"$cache\" "
|
|
1633
|
-
f"
|
|
1660
|
+
f"{operation} --noconfirm{timeout}{needed}"
|
|
1634
1661
|
)
|
|
1635
1662
|
|
|
1636
1663
|
|
|
@@ -1676,7 +1703,8 @@ def _run_pacman_with_recovery(
|
|
|
1676
1703
|
runner: Runner | None,
|
|
1677
1704
|
label: str,
|
|
1678
1705
|
mirror_optimizer: MirrorOptimizer,
|
|
1679
|
-
|
|
1706
|
+
allow_core_restart: bool = False,
|
|
1707
|
+
) -> bool:
|
|
1680
1708
|
argv = _bash_command(root, _pacman_command(packages=packages, relaxed=False), str(cache))
|
|
1681
1709
|
returncode = _run_checked(
|
|
1682
1710
|
argv,
|
|
@@ -1687,11 +1715,20 @@ def _run_pacman_with_recovery(
|
|
|
1687
1715
|
progress="pacman",
|
|
1688
1716
|
allow_failure=True,
|
|
1689
1717
|
)
|
|
1718
|
+
restart_requested = reporter.command_requested_msys2_restart
|
|
1690
1719
|
if returncode == 0:
|
|
1691
|
-
return
|
|
1720
|
+
return restart_requested
|
|
1721
|
+
if allow_core_restart and not packages and restart_requested:
|
|
1722
|
+
reporter.note(
|
|
1723
|
+
"MSYS2 replaced core runtime files and closed its own process as "
|
|
1724
|
+
"expected; continuing from a fresh Windows-launched shell.",
|
|
1725
|
+
level="warning",
|
|
1726
|
+
)
|
|
1727
|
+
return True
|
|
1692
1728
|
network_failure = reporter.command_had_network_failure
|
|
1693
1729
|
hard_failed_hosts = reporter.hard_failed_mirror_hosts
|
|
1694
1730
|
if not network_failure:
|
|
1731
|
+
_explain_windows_status(returncode, reporter)
|
|
1695
1732
|
reporter.command_failed(label, returncode)
|
|
1696
1733
|
raise RuntimeInstallError(f"{label} failed with exit code {returncode}")
|
|
1697
1734
|
reporter.note(
|
|
@@ -1722,13 +1759,112 @@ def _run_pacman_with_recovery(
|
|
|
1722
1759
|
_pacman_command(packages=packages, relaxed=True),
|
|
1723
1760
|
str(cache),
|
|
1724
1761
|
)
|
|
1725
|
-
_run_checked(
|
|
1762
|
+
retry_returncode = _run_checked(
|
|
1726
1763
|
retry_argv,
|
|
1727
1764
|
env=env,
|
|
1728
1765
|
reporter=reporter,
|
|
1729
1766
|
runner=runner,
|
|
1730
1767
|
label=f"{label} resilient retry",
|
|
1731
1768
|
progress="pacman",
|
|
1769
|
+
allow_failure=True,
|
|
1770
|
+
)
|
|
1771
|
+
restart_requested = reporter.command_requested_msys2_restart
|
|
1772
|
+
if retry_returncode == 0:
|
|
1773
|
+
return restart_requested
|
|
1774
|
+
if allow_core_restart and not packages and restart_requested:
|
|
1775
|
+
reporter.note(
|
|
1776
|
+
"MSYS2 replaced core runtime files and closed its own process as "
|
|
1777
|
+
"expected; continuing from a fresh Windows-launched shell.",
|
|
1778
|
+
level="warning",
|
|
1779
|
+
)
|
|
1780
|
+
return True
|
|
1781
|
+
_explain_windows_status(retry_returncode, reporter)
|
|
1782
|
+
reporter.command_failed(f"{label} resilient retry", retry_returncode)
|
|
1783
|
+
raise RuntimeInstallError(
|
|
1784
|
+
f"{label} resilient retry failed with exit code {retry_returncode}"
|
|
1785
|
+
)
|
|
1786
|
+
|
|
1787
|
+
|
|
1788
|
+
def _run_base_update_with_restarts(
|
|
1789
|
+
root: Path,
|
|
1790
|
+
*,
|
|
1791
|
+
cache: Path,
|
|
1792
|
+
env: Mapping[str, str],
|
|
1793
|
+
reporter: InstallReporter,
|
|
1794
|
+
runner: Runner | None,
|
|
1795
|
+
mirror_optimizer: MirrorOptimizer,
|
|
1796
|
+
) -> None:
|
|
1797
|
+
"""Run MSYS2's full upgrade in fresh processes until restart-safe."""
|
|
1798
|
+
try:
|
|
1799
|
+
for pass_number in range(1, _MAX_BASE_UPDATE_PASSES + 1):
|
|
1800
|
+
restart_requested = _run_pacman_with_recovery(
|
|
1801
|
+
root,
|
|
1802
|
+
packages=False,
|
|
1803
|
+
cache=cache,
|
|
1804
|
+
env=env,
|
|
1805
|
+
reporter=reporter,
|
|
1806
|
+
runner=runner,
|
|
1807
|
+
label=f"MSYS2 base update pass {pass_number}",
|
|
1808
|
+
mirror_optimizer=mirror_optimizer,
|
|
1809
|
+
allow_core_restart=pass_number < _MAX_BASE_UPDATE_PASSES,
|
|
1810
|
+
)
|
|
1811
|
+
if restart_requested:
|
|
1812
|
+
reporter.note(
|
|
1813
|
+
"Starting a new MSYS2 process for the remaining update…"
|
|
1814
|
+
)
|
|
1815
|
+
continue
|
|
1816
|
+
if pass_number == 1:
|
|
1817
|
+
# MSYS2's supported unattended update procedure invokes Syuu
|
|
1818
|
+
# twice, even when the first pass did not request a restart.
|
|
1819
|
+
reporter.note(
|
|
1820
|
+
"Starting the required second MSYS2 update pass in a new "
|
|
1821
|
+
"process…"
|
|
1822
|
+
)
|
|
1823
|
+
continue
|
|
1824
|
+
return
|
|
1825
|
+
raise RuntimeInstallError(
|
|
1826
|
+
"MSYS2 repeatedly requested a core-runtime restart; the temporary "
|
|
1827
|
+
"base was not published"
|
|
1828
|
+
)
|
|
1829
|
+
finally:
|
|
1830
|
+
_stop_private_gpg_agents(
|
|
1831
|
+
root,
|
|
1832
|
+
env=env,
|
|
1833
|
+
reporter=reporter,
|
|
1834
|
+
runner=runner,
|
|
1835
|
+
strict=sys.exc_info()[0] is None,
|
|
1836
|
+
)
|
|
1837
|
+
|
|
1838
|
+
|
|
1839
|
+
def _stop_private_gpg_agents(
|
|
1840
|
+
root: Path,
|
|
1841
|
+
*,
|
|
1842
|
+
env: Mapping[str, str],
|
|
1843
|
+
reporter: InstallReporter,
|
|
1844
|
+
runner: Runner | None,
|
|
1845
|
+
strict: bool,
|
|
1846
|
+
) -> None:
|
|
1847
|
+
returncode = _run_checked(
|
|
1848
|
+
_bash_command(root, _PRIVATE_GPG_SHUTDOWN_COMMAND),
|
|
1849
|
+
env=env,
|
|
1850
|
+
reporter=reporter,
|
|
1851
|
+
runner=runner,
|
|
1852
|
+
label="Stopping private MSYS2 keyring agents",
|
|
1853
|
+
allow_failure=True,
|
|
1854
|
+
)
|
|
1855
|
+
if returncode == 0:
|
|
1856
|
+
return
|
|
1857
|
+
if not strict:
|
|
1858
|
+
reporter.note(
|
|
1859
|
+
"Could not stop a private MSYS2 keyring agent while recovering "
|
|
1860
|
+
"from another installation failure.",
|
|
1861
|
+
level="warning",
|
|
1862
|
+
)
|
|
1863
|
+
return
|
|
1864
|
+
reporter.command_failed("Stopping private MSYS2 keyring agents", returncode)
|
|
1865
|
+
raise RuntimeInstallError(
|
|
1866
|
+
"stopping private MSYS2 keyring agents failed with exit code "
|
|
1867
|
+
f"{returncode}"
|
|
1732
1868
|
)
|
|
1733
1869
|
|
|
1734
1870
|
|
|
@@ -2382,16 +2518,20 @@ def install_managed_runtime(
|
|
|
2382
2518
|
"pacman is noninteractive; displayed [Y/n] prompts do "
|
|
2383
2519
|
"not require input."
|
|
2384
2520
|
)
|
|
2521
|
+
reporter.note(
|
|
2522
|
+
"Core updates may close the first MSYS2 process; "
|
|
2523
|
+
"Railmux will start the required fresh process "
|
|
2524
|
+
"automatically.",
|
|
2525
|
+
level="muted",
|
|
2526
|
+
)
|
|
2385
2527
|
package_cache = cache_base / f"pacman-{MSYS2_RUNTIME_ID}"
|
|
2386
2528
|
package_cache.mkdir(parents=True, exist_ok=True)
|
|
2387
|
-
|
|
2529
|
+
_run_base_update_with_restarts(
|
|
2388
2530
|
root,
|
|
2389
|
-
packages=False,
|
|
2390
2531
|
cache=package_cache,
|
|
2391
2532
|
env=child_env,
|
|
2392
2533
|
reporter=reporter,
|
|
2393
2534
|
runner=runner,
|
|
2394
|
-
label="MSYS2 base update",
|
|
2395
2535
|
mirror_optimizer=mirror_optimizer,
|
|
2396
2536
|
)
|
|
2397
2537
|
reporter.done()
|
|
@@ -2428,16 +2568,25 @@ def install_managed_runtime(
|
|
|
2428
2568
|
reporter=reporter,
|
|
2429
2569
|
runner=runner,
|
|
2430
2570
|
)
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2571
|
+
try:
|
|
2572
|
+
_run_pacman_with_recovery(
|
|
2573
|
+
root,
|
|
2574
|
+
packages=True,
|
|
2575
|
+
cache=package_cache,
|
|
2576
|
+
env=child_env,
|
|
2577
|
+
reporter=reporter,
|
|
2578
|
+
runner=runner,
|
|
2579
|
+
label="MSYS2 package installation",
|
|
2580
|
+
mirror_optimizer=mirror_optimizer,
|
|
2581
|
+
)
|
|
2582
|
+
finally:
|
|
2583
|
+
_stop_private_gpg_agents(
|
|
2584
|
+
root,
|
|
2585
|
+
env=child_env,
|
|
2586
|
+
reporter=reporter,
|
|
2587
|
+
runner=runner,
|
|
2588
|
+
strict=sys.exc_info()[0] is None,
|
|
2589
|
+
)
|
|
2441
2590
|
reporter.done()
|
|
2442
2591
|
|
|
2443
2592
|
identity = _ensure_base_content_identity(
|
|
@@ -2,8 +2,12 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import io
|
|
4
4
|
import os
|
|
5
|
+
import subprocess
|
|
5
6
|
import sys
|
|
6
7
|
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from railmux import windows_msys2
|
|
7
11
|
from railmux.windows_install_log import InstallReporter, install_log_path
|
|
8
12
|
from railmux.windows_msys2 import _run_checked
|
|
9
13
|
|
|
@@ -60,6 +64,22 @@ def test_verbose_reporter_streams_the_same_sanitized_output(tmp_path):
|
|
|
60
64
|
assert "package 中文\nprogress\n" in path.read_text(encoding="utf-8")
|
|
61
65
|
|
|
62
66
|
|
|
67
|
+
def test_reporter_recognizes_only_the_explicit_msys2_core_restart(tmp_path):
|
|
68
|
+
path = tmp_path / "install.log"
|
|
69
|
+
with InstallReporter(path, verbose=False, stream=io.StringIO()) as reporter:
|
|
70
|
+
reporter.command_started("first update")
|
|
71
|
+
reporter.command_output(
|
|
72
|
+
":: To complete this update all MSYS2 processes including this "
|
|
73
|
+
"terminal will be closed. Confirm to proceed [Y/n]"
|
|
74
|
+
)
|
|
75
|
+
reporter.command_output_finished()
|
|
76
|
+
assert reporter.command_requested_msys2_restart
|
|
77
|
+
|
|
78
|
+
reporter.command_started("second update")
|
|
79
|
+
reporter.command_output("warning: terminate other MSYS2 programs\n")
|
|
80
|
+
assert not reporter.command_requested_msys2_restart
|
|
81
|
+
|
|
82
|
+
|
|
63
83
|
def test_verbose_reporter_never_fails_on_a_legacy_windows_console_codec(tmp_path):
|
|
64
84
|
stream = Cp1252Stream()
|
|
65
85
|
path = tmp_path / "install-0.4.0.dev7-20260803T000000Z-5.log"
|
|
@@ -194,6 +214,27 @@ def test_default_command_runner_uses_no_stdin_and_captures_utf8(tmp_path):
|
|
|
194
214
|
assert "captured 中文" in path.read_text(encoding="utf-8")
|
|
195
215
|
|
|
196
216
|
|
|
217
|
+
def test_windows_missing_dll_status_is_explained(tmp_path, monkeypatch):
|
|
218
|
+
stream = io.StringIO()
|
|
219
|
+
path = tmp_path / "install.log"
|
|
220
|
+
monkeypatch.setattr(windows_msys2, "_NATIVE_WINDOWS", True)
|
|
221
|
+
|
|
222
|
+
with InstallReporter(path, verbose=False, stream=stream) as reporter:
|
|
223
|
+
with pytest.raises(windows_msys2.RuntimeInstallError, match="3221225781"):
|
|
224
|
+
_run_checked(
|
|
225
|
+
["fixture.exe"],
|
|
226
|
+
env={},
|
|
227
|
+
reporter=reporter,
|
|
228
|
+
runner=lambda *_args, **_kwargs: subprocess.CompletedProcess(
|
|
229
|
+
[], 0xC0000135
|
|
230
|
+
),
|
|
231
|
+
label="MSYS2 base update",
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
assert "STATUS_DLL_NOT_FOUND" in stream.getvalue()
|
|
235
|
+
assert "security software" in path.read_text(encoding="utf-8")
|
|
236
|
+
|
|
237
|
+
|
|
197
238
|
def test_compact_reporter_surfaces_extraction_and_package_progress(tmp_path):
|
|
198
239
|
stream = io.StringIO()
|
|
199
240
|
path = tmp_path / "install-0.4.0.dev8-20260803T000000Z-7.log"
|
|
@@ -3,7 +3,9 @@ from __future__ import annotations
|
|
|
3
3
|
import hashlib
|
|
4
4
|
import io
|
|
5
5
|
import json
|
|
6
|
+
import os
|
|
6
7
|
import re
|
|
8
|
+
import stat
|
|
7
9
|
import subprocess
|
|
8
10
|
import tarfile
|
|
9
11
|
from contextlib import contextmanager
|
|
@@ -795,6 +797,8 @@ def test_install_is_staged_and_activated_only_after_exact_probe(tmp_path):
|
|
|
795
797
|
).read_text(encoding="utf-8")
|
|
796
798
|
)["railmux"] == VERSION
|
|
797
799
|
joined = [" ".join(command) for command in commands]
|
|
800
|
+
base_updates = [command for command in joined if "-Syuu --noconfirm" in command]
|
|
801
|
+
assert len(base_updates) == 2
|
|
798
802
|
assert any("-Syu --noconfirm" in command for command in joined)
|
|
799
803
|
assert any("--needed tmux python python-pip" in command for command in joined)
|
|
800
804
|
assert any('python -m venv "$1/venv"' in command for command in joined)
|
|
@@ -804,7 +808,8 @@ def test_install_is_staged_and_activated_only_after_exact_probe(tmp_path):
|
|
|
804
808
|
assert len(logs) == 1
|
|
805
809
|
log = logs[0].read_text(encoding="utf-8")
|
|
806
810
|
assert "[1/7] Preparing verified MSYS2" in log
|
|
807
|
-
assert "--- MSYS2 base update ---" in log
|
|
811
|
+
assert "--- MSYS2 base update pass 1 ---" in log
|
|
812
|
+
assert "--- MSYS2 base update pass 2 ---" in log
|
|
808
813
|
assert "Installation completed successfully" in log
|
|
809
814
|
config = runtime.root / "etc" / "railmux-pacman.conf"
|
|
810
815
|
assert "[msys]" in config.read_text(encoding="utf-8")
|
|
@@ -1291,12 +1296,16 @@ def test_tar_xz_extraction_is_internal_bounded_and_progress_visible(
|
|
|
1291
1296
|
_tar_directory("msys64"),
|
|
1292
1297
|
_tar_directory("msys64/usr"),
|
|
1293
1298
|
_tar_file("msys64/usr/bash.exe", b"verified bash"),
|
|
1299
|
+
_tar_file("msys64/usr/read-only-upstream", b"must stay writable"),
|
|
1294
1300
|
]
|
|
1301
|
+
entries[-1][0].mode = 0o444
|
|
1295
1302
|
archive = tmp_path / MSYS2_ARCHIVE_NAME
|
|
1296
1303
|
_write_test_tar(archive, entries)
|
|
1297
|
-
monkeypatch.setattr(windows_msys2, "MSYS2_ARCHIVE_MEMBER_COUNT",
|
|
1304
|
+
monkeypatch.setattr(windows_msys2, "MSYS2_ARCHIVE_MEMBER_COUNT", 4)
|
|
1298
1305
|
monkeypatch.setattr(
|
|
1299
|
-
windows_msys2,
|
|
1306
|
+
windows_msys2,
|
|
1307
|
+
"MSYS2_ARCHIVE_UNPACKED_SIZE",
|
|
1308
|
+
len(b"verified bash") + len(b"must stay writable"),
|
|
1300
1309
|
)
|
|
1301
1310
|
destination = tmp_path / "stage"
|
|
1302
1311
|
destination.mkdir()
|
|
@@ -1309,12 +1318,34 @@ def test_tar_xz_extraction_is_internal_bounded_and_progress_visible(
|
|
|
1309
1318
|
assert (destination / "msys64" / "usr" / "bash.exe").read_bytes() == (
|
|
1310
1319
|
b"verified bash"
|
|
1311
1320
|
)
|
|
1321
|
+
package_owned = destination / "msys64" / "usr" / "read-only-upstream"
|
|
1322
|
+
if os.name == "nt":
|
|
1323
|
+
readonly = getattr(stat, "FILE_ATTRIBUTE_READONLY", 0x1)
|
|
1324
|
+
assert not getattr(package_owned.stat(), "st_file_attributes", 0) & readonly
|
|
1325
|
+
else:
|
|
1326
|
+
assert stat.S_IMODE(package_owned.stat().st_mode) == 0o444
|
|
1312
1327
|
rendered = output.getvalue()
|
|
1313
|
-
assert "Extracting private runtime: 1/
|
|
1314
|
-
assert "Extracting private runtime:
|
|
1328
|
+
assert "Extracting private runtime: 1/4 files (25%)" in rendered
|
|
1329
|
+
assert "Extracting private runtime: 4/4 files (100%)" in rendered
|
|
1315
1330
|
assert "MSYS2 archive extraction" in log.read_text(encoding="utf-8")
|
|
1316
1331
|
|
|
1317
1332
|
|
|
1333
|
+
def test_native_windows_extraction_clears_posix_read_only_modes(
|
|
1334
|
+
tmp_path, monkeypatch,
|
|
1335
|
+
):
|
|
1336
|
+
path = tmp_path / "package-owned"
|
|
1337
|
+
path.write_bytes(b"fixture")
|
|
1338
|
+
applied = []
|
|
1339
|
+
monkeypatch.setattr(windows_msys2, "_NATIVE_WINDOWS", True)
|
|
1340
|
+
monkeypatch.setattr(
|
|
1341
|
+
windows_msys2.os, "chmod", lambda target, mode: applied.append((target, mode))
|
|
1342
|
+
)
|
|
1343
|
+
|
|
1344
|
+
windows_msys2._apply_archive_mode(path, 0o444)
|
|
1345
|
+
|
|
1346
|
+
assert applied == [(path, stat.S_IWRITE)]
|
|
1347
|
+
|
|
1348
|
+
|
|
1318
1349
|
@pytest.mark.parametrize(
|
|
1319
1350
|
("member", "message"),
|
|
1320
1351
|
[
|
|
@@ -1444,6 +1475,112 @@ def test_pacman_network_failure_retries_with_cache_and_relaxed_timeout(tmp_path)
|
|
|
1444
1475
|
assert f"Server = {repo}" in rendered
|
|
1445
1476
|
|
|
1446
1477
|
|
|
1478
|
+
def test_base_update_restarts_after_confirmed_core_shutdown(tmp_path):
|
|
1479
|
+
attempts = []
|
|
1480
|
+
restart = (
|
|
1481
|
+
b"upgrading msys2-runtime...\n"
|
|
1482
|
+
b":: To complete this update all MSYS2 processes including this "
|
|
1483
|
+
b"terminal will be closed. Confirm to proceed [Y/n]\n"
|
|
1484
|
+
)
|
|
1485
|
+
|
|
1486
|
+
def runner(argv, *, env, check):
|
|
1487
|
+
attempts.append(argv)
|
|
1488
|
+
update_attempts = [item for item in attempts if "-Syuu" in item[4]]
|
|
1489
|
+
if len(update_attempts) == 1 and "-Syuu" in argv[4]:
|
|
1490
|
+
return completed(argv, returncode=0xC0000135, stdout=restart)
|
|
1491
|
+
return completed(argv, stdout=b"there is nothing to do\n")
|
|
1492
|
+
|
|
1493
|
+
stream = io.StringIO()
|
|
1494
|
+
with InstallReporter(
|
|
1495
|
+
tmp_path / "install.log", verbose=False, stream=stream
|
|
1496
|
+
) as reporter:
|
|
1497
|
+
windows_msys2._run_base_update_with_restarts(
|
|
1498
|
+
tmp_path / "msys64",
|
|
1499
|
+
cache=tmp_path / "cache",
|
|
1500
|
+
env={},
|
|
1501
|
+
reporter=reporter,
|
|
1502
|
+
runner=runner,
|
|
1503
|
+
mirror_optimizer=lambda _root: PacmanMirrorDecision(
|
|
1504
|
+
None, None, False, (), ()
|
|
1505
|
+
),
|
|
1506
|
+
)
|
|
1507
|
+
|
|
1508
|
+
updates = [argv for argv in attempts if "-Syuu --noconfirm" in argv[4]]
|
|
1509
|
+
assert len(updates) == 2
|
|
1510
|
+
assert updates[0][0] == updates[1][0]
|
|
1511
|
+
assert windows_msys2._PRIVATE_GPG_SHUTDOWN_COMMAND in attempts[-1][4]
|
|
1512
|
+
rendered = stream.getvalue()
|
|
1513
|
+
assert "closed its own process as expected" in rendered
|
|
1514
|
+
assert "failed with exit code" not in rendered
|
|
1515
|
+
|
|
1516
|
+
|
|
1517
|
+
def test_base_update_does_not_mask_unconfirmed_missing_dll(tmp_path, monkeypatch):
|
|
1518
|
+
monkeypatch.setattr(windows_msys2, "_NATIVE_WINDOWS", True)
|
|
1519
|
+
attempts = []
|
|
1520
|
+
|
|
1521
|
+
def runner(argv, *, env, check):
|
|
1522
|
+
attempts.append(argv)
|
|
1523
|
+
if windows_msys2._PRIVATE_GPG_SHUTDOWN_COMMAND in argv[4]:
|
|
1524
|
+
return completed(argv)
|
|
1525
|
+
return completed(
|
|
1526
|
+
argv,
|
|
1527
|
+
returncode=0xC0000135,
|
|
1528
|
+
stdout=b"upgrading msys2-runtime...\n",
|
|
1529
|
+
)
|
|
1530
|
+
|
|
1531
|
+
stream = io.StringIO()
|
|
1532
|
+
with InstallReporter(
|
|
1533
|
+
tmp_path / "install.log", verbose=False, stream=stream
|
|
1534
|
+
) as reporter:
|
|
1535
|
+
with pytest.raises(RuntimeInstallError, match="3221225781"):
|
|
1536
|
+
windows_msys2._run_base_update_with_restarts(
|
|
1537
|
+
tmp_path / "msys64",
|
|
1538
|
+
cache=tmp_path / "cache",
|
|
1539
|
+
env={},
|
|
1540
|
+
reporter=reporter,
|
|
1541
|
+
runner=runner,
|
|
1542
|
+
mirror_optimizer=lambda _root: PacmanMirrorDecision(
|
|
1543
|
+
None, None, False, (), ()
|
|
1544
|
+
),
|
|
1545
|
+
)
|
|
1546
|
+
|
|
1547
|
+
updates = [argv for argv in attempts if "-Syuu --noconfirm" in argv[4]]
|
|
1548
|
+
assert len(updates) == 1
|
|
1549
|
+
assert "outside a confirmed MSYS2 core-update restart" in stream.getvalue()
|
|
1550
|
+
|
|
1551
|
+
|
|
1552
|
+
def test_base_update_restart_loop_is_bounded(tmp_path):
|
|
1553
|
+
attempts = []
|
|
1554
|
+
restart = (
|
|
1555
|
+
b":: To complete this update all MSYS2 processes including this "
|
|
1556
|
+
b"terminal will be closed. Confirm to proceed [Y/n]\n"
|
|
1557
|
+
)
|
|
1558
|
+
|
|
1559
|
+
def runner(argv, *, env, check):
|
|
1560
|
+
attempts.append(argv)
|
|
1561
|
+
if windows_msys2._PRIVATE_GPG_SHUTDOWN_COMMAND in argv[4]:
|
|
1562
|
+
return completed(argv)
|
|
1563
|
+
return completed(argv, stdout=restart)
|
|
1564
|
+
|
|
1565
|
+
with InstallReporter(
|
|
1566
|
+
tmp_path / "install.log", verbose=False, stream=io.StringIO()
|
|
1567
|
+
) as reporter:
|
|
1568
|
+
with pytest.raises(RuntimeInstallError, match="repeatedly requested"):
|
|
1569
|
+
windows_msys2._run_base_update_with_restarts(
|
|
1570
|
+
tmp_path / "msys64",
|
|
1571
|
+
cache=tmp_path / "cache",
|
|
1572
|
+
env={},
|
|
1573
|
+
reporter=reporter,
|
|
1574
|
+
runner=runner,
|
|
1575
|
+
mirror_optimizer=lambda _root: PacmanMirrorDecision(
|
|
1576
|
+
None, None, False, (), ()
|
|
1577
|
+
),
|
|
1578
|
+
)
|
|
1579
|
+
|
|
1580
|
+
updates = [argv for argv in attempts if "-Syuu --noconfirm" in argv[4]]
|
|
1581
|
+
assert len(updates) == windows_msys2._MAX_BASE_UPDATE_PASSES
|
|
1582
|
+
|
|
1583
|
+
|
|
1447
1584
|
def test_completed_package_cache_count_ignores_signatures_partials_and_links(tmp_path):
|
|
1448
1585
|
(tmp_path / "python.pkg.tar.zst").write_bytes(b"package")
|
|
1449
1586
|
(tmp_path / "python.pkg.tar.zst.sig").write_bytes(b"signature")
|