railmux 0.4.0.dev25__tar.gz → 0.4.0.dev26__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.dev25/src/railmux.egg-info → railmux-0.4.0.dev26}/PKG-INFO +1 -1
- railmux-0.4.0.dev26/src/railmux/__init__.py +1 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux/windows_install_log.py +18 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux/windows_msys2.py +190 -14
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux/windows_ui_transition.py +2 -2
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26/src/railmux.egg-info}/PKG-INFO +1 -1
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_windows_msys2.py +151 -15
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_windows_ui_transition.py +3 -3
- railmux-0.4.0.dev25/src/railmux/__init__.py +0 -1
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/LICENSE +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/MANIFEST.in +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/README.md +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/pyproject.toml +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/setup.cfg +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux/__main__.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux/atomic_file.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux/background_index.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux/cli.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux/codex_index.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux/config.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux/config_cli.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux/diagnostics.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux/discovery.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux/display_transport.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux/entrypoint.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux/fast_display_client.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux/fast_display_history.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux/fast_display_input.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux/fast_display_protocol.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux/fast_display_server.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux/favorites.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux/fuzzy.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux/help_workspace.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux/launcher.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux/legacy_sessions.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux/local_clipboard.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux/local_open.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux/models.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux/modes.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux/mouse_manager.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux/orphan_marker.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux/pane_surface.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux/path_codec.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux/provider_paths.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux/remote_config.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux/renames.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux/restart_state.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux/runtime_config.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux/scroll_agent.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux/scroll_manager.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux/selection_isolation.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux/self_update.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux/session_cache.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux/session_index.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux/session_lease.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux/setting_contracts.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux/settings.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux/ssh_args.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux/ssh_compat.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux/ssh_display_diagnostics.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux/ssh_doctor.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux/system_deps.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux/terminal_status.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux/tmux_binding_manager.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux/tmux_ctl.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux/tmux_health.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux/tmux_server.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux/tool_panes.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux/transcript.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux/ui/__init__.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux/ui/_widgets.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux/ui/app.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux/ui/keymap.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux/ui/modals.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux/ui/projects_pane.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux/ui/running_pane.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux/ui/sessions_pane.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux/ui/sidebar.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux/ui/statusbar.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux/ui/workspace.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux/windows_attach_relay.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux/windows_bootstrap.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux/windows_pacman.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux/windows_tmux_lifecycle.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux.egg-info/SOURCES.txt +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux.egg-info/dependency_links.txt +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux.egg-info/entry_points.txt +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux.egg-info/requires.txt +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/src/railmux.egg-info/top_level.txt +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_agent_workspace.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_atomic_file.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_background_index.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_cli.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_codex_delete.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_codex_index.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_codex_lineage.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_codex_transcript.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_config.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_config_cli.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_denested_transport_benchmark.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_diagnostics.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_discovery.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_display_transport.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_fast_full_window.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_focus_highlight.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_fullscreen_binding.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_fuzzy.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_help_agent.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_help_workspace.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_history_cleanup.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_keymap.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_launcher.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_layout_profile.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_legacy_sessions.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_local_clipboard.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_local_open.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_modals.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_mode_selection.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_modes.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_mouse_manager.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_new_project.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_options.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_orphan_recovery.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_pane_surface.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_path_codec.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_projects_pane.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_provider_paths.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_release_notes.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_release_workflow.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_remote_config.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_renames.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_restart_state.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_running_pane.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_running_sort.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_runtime_config.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_scroll_agent.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_scroll_manager.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_selection_isolation.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_self_update.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_session_cache.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_session_index.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_session_lease.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_sessions_pane.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_setting_contracts.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_settings.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_sidebar.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_soft_quit.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_ssh_args.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_ssh_compat.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_ssh_display_benchmark.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_ssh_display_diagnostics.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_ssh_doctor.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_status.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_statusbar.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_system_deps.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_terminal_size.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_terminal_status.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_termux_local_touch.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_tmux_binding_manager.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_tmux_ctl.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_tmux_health.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_tmux_integration.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_tmux_server.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_tmux_status.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_tool_panes.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_transcript.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_utf8_state.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_web_demo_recorder.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_wheel_smoke.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_widgets.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_windows_attach_relay.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_windows_bootstrap.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_windows_install_log.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_windows_pacman.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_windows_tmux_lifecycle.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_windows_wrapper_contract.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tests/test_yolo_prompt.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tools/denested_transport_benchmark.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tools/release_notes.py +0 -0
- {railmux-0.4.0.dev25 → railmux-0.4.0.dev26}/tools/wheel_smoke.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.0.dev26"
|
|
@@ -235,6 +235,24 @@ class InstallReporter:
|
|
|
235
235
|
self._consume_output_line(self._pending_output)
|
|
236
236
|
self._pending_output = ""
|
|
237
237
|
|
|
238
|
+
def extraction_progress(self, completed: int, total: int) -> None:
|
|
239
|
+
"""Report bounded in-process archive extraction progress."""
|
|
240
|
+
if total <= 0:
|
|
241
|
+
return
|
|
242
|
+
completed = min(max(completed, 0), total)
|
|
243
|
+
percent = min(100, int(completed * 100 / total))
|
|
244
|
+
self._progress_detail = (
|
|
245
|
+
f"extracting private runtime: {completed}/{total} files "
|
|
246
|
+
f"({percent}%)"
|
|
247
|
+
)
|
|
248
|
+
if percent >= self._last_extraction_percent + 5 or percent == 100:
|
|
249
|
+
self.note(
|
|
250
|
+
f"Extracting private runtime: {completed}/{total} files "
|
|
251
|
+
f"({percent}%)"
|
|
252
|
+
)
|
|
253
|
+
self._last_extraction_percent = percent
|
|
254
|
+
self.heartbeat()
|
|
255
|
+
|
|
238
256
|
def _consume_output_line(self, raw_line: str) -> None:
|
|
239
257
|
line = _redact_line(raw_line)
|
|
240
258
|
self._write_log(f"{line}\n")
|
|
@@ -10,6 +10,7 @@ import concurrent.futures
|
|
|
10
10
|
import hashlib
|
|
11
11
|
import http.client
|
|
12
12
|
import json
|
|
13
|
+
import lzma
|
|
13
14
|
import os
|
|
14
15
|
import re
|
|
15
16
|
import secrets
|
|
@@ -17,6 +18,7 @@ import shutil
|
|
|
17
18
|
import stat
|
|
18
19
|
import subprocess
|
|
19
20
|
import sys
|
|
21
|
+
import tarfile
|
|
20
22
|
import tempfile
|
|
21
23
|
import time
|
|
22
24
|
import urllib.error
|
|
@@ -25,7 +27,7 @@ import urllib.request
|
|
|
25
27
|
from collections.abc import Callable, Iterator, Mapping, Sequence
|
|
26
28
|
from contextlib import contextmanager
|
|
27
29
|
from dataclasses import dataclass
|
|
28
|
-
from pathlib import Path
|
|
30
|
+
from pathlib import Path, PurePosixPath
|
|
29
31
|
|
|
30
32
|
from railmux.windows_install_log import (
|
|
31
33
|
InstallReporter,
|
|
@@ -44,7 +46,7 @@ from railmux.terminal_status import STYLE_ACCENT, STYLE_MUTED, styled
|
|
|
44
46
|
|
|
45
47
|
|
|
46
48
|
MSYS2_RELEASE = "2026-03-22"
|
|
47
|
-
MSYS2_ARCHIVE_NAME = f"msys2-base-x86_64-{MSYS2_RELEASE.replace('-', '')}.
|
|
49
|
+
MSYS2_ARCHIVE_NAME = f"msys2-base-x86_64-{MSYS2_RELEASE.replace('-', '')}.tar.xz"
|
|
48
50
|
MSYS2_ARCHIVE_SOURCES = (
|
|
49
51
|
(
|
|
50
52
|
"GitHub",
|
|
@@ -65,8 +67,16 @@ MSYS2_ARCHIVE_SOURCES = (
|
|
|
65
67
|
f"https://mirror.nju.edu.cn/msys2/distrib/x86_64/{MSYS2_ARCHIVE_NAME}",
|
|
66
68
|
),
|
|
67
69
|
)
|
|
68
|
-
MSYS2_ARCHIVE_SIZE =
|
|
70
|
+
MSYS2_ARCHIVE_SIZE = 53_466_096
|
|
69
71
|
MSYS2_ARCHIVE_SHA256 = (
|
|
72
|
+
"6b4a986a3ec4f1e40313bdf17903a6f5c854373d4230c40f14c5e35c4bac7fce"
|
|
73
|
+
)
|
|
74
|
+
MSYS2_ARCHIVE_MEMBER_COUNT = 16_485
|
|
75
|
+
MSYS2_ARCHIVE_UNPACKED_SIZE = 289_361_533
|
|
76
|
+
# Schema-1 base-content markers released in dev24/dev25 use the SFX digest as
|
|
77
|
+
# their pinned release-lineage token. Keep accepting and writing that durable
|
|
78
|
+
# value while the bootstrap consumes the equivalent official tar.xz artifact.
|
|
79
|
+
MSYS2_BASE_LINEAGE_SHA256 = (
|
|
70
80
|
"6fe0cc8154132040e034ff4daface2a4163a9d1f6ebaaa1133394bff460bd5cf"
|
|
71
81
|
)
|
|
72
82
|
MSYS2_RUNTIME_ID = f"msys2-{MSYS2_RELEASE}"
|
|
@@ -141,7 +151,7 @@ class BaseContentIdentity:
|
|
|
141
151
|
return {
|
|
142
152
|
"schema": 1,
|
|
143
153
|
"runtime": MSYS2_RUNTIME_ID,
|
|
144
|
-
"archive_sha256":
|
|
154
|
+
"archive_sha256": MSYS2_BASE_LINEAGE_SHA256,
|
|
145
155
|
"content_id": self.content_id,
|
|
146
156
|
"package_count": self.package_count,
|
|
147
157
|
"core_packages": dict(self.core_packages),
|
|
@@ -166,6 +176,7 @@ Probe = Callable[..., subprocess.CompletedProcess[bytes]]
|
|
|
166
176
|
Runner = Callable[..., subprocess.CompletedProcess]
|
|
167
177
|
Downloader = Callable[[str, Path, str], None]
|
|
168
178
|
MirrorOptimizer = Callable[[Path], PacmanMirrorDecision]
|
|
179
|
+
Extractor = Callable[..., None]
|
|
169
180
|
|
|
170
181
|
|
|
171
182
|
@dataclass(frozen=True)
|
|
@@ -519,9 +530,20 @@ def _managed_cache(environ: Mapping[str, str]) -> Path | None:
|
|
|
519
530
|
return Path(local_app_data) / "Railmux" / "cache"
|
|
520
531
|
|
|
521
532
|
|
|
533
|
+
def _path_is_link_or_reparse(path: Path) -> bool:
|
|
534
|
+
info = path.lstat()
|
|
535
|
+
reparse_flag = getattr(stat, "FILE_ATTRIBUTE_REPARSE_POINT", 0x400)
|
|
536
|
+
return stat.S_ISLNK(info.st_mode) or bool(
|
|
537
|
+
getattr(info, "st_file_attributes", 0) & reparse_flag
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
|
|
522
541
|
def _archive_cache_is_valid(path: Path) -> bool:
|
|
523
542
|
try:
|
|
524
|
-
if
|
|
543
|
+
if (
|
|
544
|
+
_path_is_link_or_reparse(path)
|
|
545
|
+
or path.stat().st_size != MSYS2_ARCHIVE_SIZE
|
|
546
|
+
):
|
|
525
547
|
return False
|
|
526
548
|
digest = hashlib.sha256()
|
|
527
549
|
with path.open("rb") as stream:
|
|
@@ -553,6 +575,166 @@ def _prepare_cached_archive(
|
|
|
553
575
|
return archive, source
|
|
554
576
|
|
|
555
577
|
|
|
578
|
+
def _safe_archive_parts(member: tarfile.TarInfo) -> tuple[str, ...]:
|
|
579
|
+
name = member.name
|
|
580
|
+
raw_parts = name.split("/")
|
|
581
|
+
pure = PurePosixPath(name)
|
|
582
|
+
if (
|
|
583
|
+
not name
|
|
584
|
+
or "\\" in name
|
|
585
|
+
or pure.is_absolute()
|
|
586
|
+
or any(part in {"", ".", ".."} for part in raw_parts)
|
|
587
|
+
or not raw_parts
|
|
588
|
+
or raw_parts[0] != "msys64"
|
|
589
|
+
):
|
|
590
|
+
raise RuntimeInstallError(
|
|
591
|
+
"the verified MSYS2 archive contained an unsafe path"
|
|
592
|
+
)
|
|
593
|
+
return tuple(raw_parts)
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
def _ensure_safe_archive_directory(root: Path, parts: Sequence[str]) -> Path:
|
|
597
|
+
current = root
|
|
598
|
+
for part in parts:
|
|
599
|
+
candidate = current / part
|
|
600
|
+
try:
|
|
601
|
+
if _path_is_link_or_reparse(candidate) or not candidate.is_dir():
|
|
602
|
+
raise RuntimeInstallError(
|
|
603
|
+
"the MSYS2 extraction staging directory became unsafe"
|
|
604
|
+
)
|
|
605
|
+
except FileNotFoundError:
|
|
606
|
+
try:
|
|
607
|
+
candidate.mkdir()
|
|
608
|
+
except OSError as exc:
|
|
609
|
+
raise RuntimeInstallError(
|
|
610
|
+
"could not create the MSYS2 extraction staging directory"
|
|
611
|
+
) from exc
|
|
612
|
+
if _path_is_link_or_reparse(candidate) or not candidate.is_dir():
|
|
613
|
+
raise RuntimeInstallError(
|
|
614
|
+
"the MSYS2 extraction staging directory became unsafe"
|
|
615
|
+
)
|
|
616
|
+
except OSError as exc:
|
|
617
|
+
raise RuntimeInstallError(
|
|
618
|
+
"could not inspect the MSYS2 extraction staging directory"
|
|
619
|
+
) from exc
|
|
620
|
+
current = candidate
|
|
621
|
+
return current
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
def _extract_msys2_archive(
|
|
625
|
+
archive: Path,
|
|
626
|
+
destination: Path,
|
|
627
|
+
*,
|
|
628
|
+
reporter: InstallReporter,
|
|
629
|
+
) -> None:
|
|
630
|
+
"""Extract the pinned tar.xz without executing downloaded code."""
|
|
631
|
+
reporter.command_started("MSYS2 archive extraction", progress="extract")
|
|
632
|
+
seen: set[tuple[str, ...]] = set()
|
|
633
|
+
directories: list[tuple[Path, int]] = []
|
|
634
|
+
member_count = 0
|
|
635
|
+
unpacked_size = 0
|
|
636
|
+
try:
|
|
637
|
+
if _path_is_link_or_reparse(destination) or not destination.is_dir():
|
|
638
|
+
raise RuntimeInstallError(
|
|
639
|
+
"the MSYS2 extraction staging directory is unsafe"
|
|
640
|
+
)
|
|
641
|
+
with tarfile.open(archive, mode="r|xz") as stream:
|
|
642
|
+
for member in stream:
|
|
643
|
+
member_count += 1
|
|
644
|
+
if member_count > MSYS2_ARCHIVE_MEMBER_COUNT:
|
|
645
|
+
raise RuntimeInstallError(
|
|
646
|
+
"the verified MSYS2 archive member count changed"
|
|
647
|
+
)
|
|
648
|
+
parts = _safe_archive_parts(member)
|
|
649
|
+
if parts in seen:
|
|
650
|
+
raise RuntimeInstallError(
|
|
651
|
+
"the verified MSYS2 archive contained a duplicate path"
|
|
652
|
+
)
|
|
653
|
+
seen.add(parts)
|
|
654
|
+
if not (member.isdir() or member.isreg()):
|
|
655
|
+
raise RuntimeInstallError(
|
|
656
|
+
"the verified MSYS2 archive contained an unsupported link "
|
|
657
|
+
"or special file"
|
|
658
|
+
)
|
|
659
|
+
parent = _ensure_safe_archive_directory(destination, parts[:-1])
|
|
660
|
+
target = parent / parts[-1]
|
|
661
|
+
if member.isdir():
|
|
662
|
+
directory = _ensure_safe_archive_directory(destination, parts)
|
|
663
|
+
directories.append((directory, member.mode & 0o777))
|
|
664
|
+
else:
|
|
665
|
+
if member.size < 0:
|
|
666
|
+
raise RuntimeInstallError(
|
|
667
|
+
"the verified MSYS2 archive contained an invalid file"
|
|
668
|
+
)
|
|
669
|
+
unpacked_size += member.size
|
|
670
|
+
if unpacked_size > MSYS2_ARCHIVE_UNPACKED_SIZE:
|
|
671
|
+
raise RuntimeInstallError(
|
|
672
|
+
"the verified MSYS2 archive expanded beyond its "
|
|
673
|
+
"pinned size"
|
|
674
|
+
)
|
|
675
|
+
try:
|
|
676
|
+
try:
|
|
677
|
+
target.lstat()
|
|
678
|
+
except FileNotFoundError:
|
|
679
|
+
pass
|
|
680
|
+
else:
|
|
681
|
+
raise RuntimeInstallError(
|
|
682
|
+
"the MSYS2 extraction staging path already exists"
|
|
683
|
+
)
|
|
684
|
+
source = stream.extractfile(member)
|
|
685
|
+
if source is None:
|
|
686
|
+
raise RuntimeInstallError(
|
|
687
|
+
"the verified MSYS2 archive file was unreadable"
|
|
688
|
+
)
|
|
689
|
+
remaining = member.size
|
|
690
|
+
with target.open("xb") as output:
|
|
691
|
+
while remaining:
|
|
692
|
+
chunk = source.read(min(1024 * 1024, remaining))
|
|
693
|
+
if not chunk:
|
|
694
|
+
raise RuntimeInstallError(
|
|
695
|
+
"the verified MSYS2 archive ended early"
|
|
696
|
+
)
|
|
697
|
+
output.write(chunk)
|
|
698
|
+
remaining -= len(chunk)
|
|
699
|
+
os.chmod(target, member.mode & 0o777)
|
|
700
|
+
except RuntimeInstallError:
|
|
701
|
+
raise
|
|
702
|
+
except OSError as exc:
|
|
703
|
+
raise RuntimeInstallError(
|
|
704
|
+
"could not write the MSYS2 extraction staging file"
|
|
705
|
+
) from exc
|
|
706
|
+
reporter.extraction_progress(
|
|
707
|
+
member_count, MSYS2_ARCHIVE_MEMBER_COUNT
|
|
708
|
+
)
|
|
709
|
+
if member_count != MSYS2_ARCHIVE_MEMBER_COUNT:
|
|
710
|
+
raise RuntimeInstallError(
|
|
711
|
+
"the verified MSYS2 archive member count changed"
|
|
712
|
+
)
|
|
713
|
+
if unpacked_size != MSYS2_ARCHIVE_UNPACKED_SIZE:
|
|
714
|
+
raise RuntimeInstallError(
|
|
715
|
+
"the verified MSYS2 archive expanded to an unexpected size"
|
|
716
|
+
)
|
|
717
|
+
for directory, mode in reversed(directories):
|
|
718
|
+
os.chmod(directory, mode)
|
|
719
|
+
except RuntimeInstallError:
|
|
720
|
+
reporter.note(
|
|
721
|
+
"MSYS2 archive extraction failed; the temporary base was not "
|
|
722
|
+
"published.",
|
|
723
|
+
level="error",
|
|
724
|
+
)
|
|
725
|
+
raise
|
|
726
|
+
except (OSError, lzma.LZMAError, tarfile.TarError, EOFError) as exc:
|
|
727
|
+
reporter.note(
|
|
728
|
+
"MSYS2 archive extraction failed; the temporary base was not "
|
|
729
|
+
"published.",
|
|
730
|
+
level="error",
|
|
731
|
+
)
|
|
732
|
+
raise RuntimeInstallError(
|
|
733
|
+
"the verified MSYS2 tar.xz archive could not be extracted"
|
|
734
|
+
) from exc
|
|
735
|
+
reporter.command_succeeded()
|
|
736
|
+
|
|
737
|
+
|
|
556
738
|
def managed_root(environ: Mapping[str, str]) -> Path | None:
|
|
557
739
|
base = _managed_base(environ)
|
|
558
740
|
if base is None:
|
|
@@ -690,7 +872,7 @@ def _decode_base_content_marker(root: Path) -> BaseContentIdentity | None:
|
|
|
690
872
|
if (
|
|
691
873
|
payload.get("schema") != 1
|
|
692
874
|
or payload.get("runtime") != MSYS2_RUNTIME_ID
|
|
693
|
-
or payload.get("archive_sha256") !=
|
|
875
|
+
or payload.get("archive_sha256") != MSYS2_BASE_LINEAGE_SHA256
|
|
694
876
|
or not isinstance(content_id, str)
|
|
695
877
|
or re.fullmatch(r"[0-9a-f]{64}", content_id) is None
|
|
696
878
|
or not isinstance(package_count, int)
|
|
@@ -2022,6 +2204,7 @@ def install_managed_runtime(
|
|
|
2022
2204
|
probe: Probe = _probe,
|
|
2023
2205
|
lock_factory: Callable[[Path], object] = install_lock,
|
|
2024
2206
|
mirror_optimizer: MirrorOptimizer = optimize_pacman_mirror,
|
|
2207
|
+
extractor: Extractor = _extract_msys2_archive,
|
|
2025
2208
|
verbose: bool = False,
|
|
2026
2209
|
reuse_only: bool = False,
|
|
2027
2210
|
) -> Msys2Runtime:
|
|
@@ -2160,14 +2343,7 @@ def install_managed_runtime(
|
|
|
2160
2343
|
reporter.done(source)
|
|
2161
2344
|
|
|
2162
2345
|
reporter.phase(2, 7, "Extracting the private MSYS2 base")
|
|
2163
|
-
|
|
2164
|
-
[str(archive), "-y", f"-o{stage}"],
|
|
2165
|
-
env=environ,
|
|
2166
|
-
reporter=reporter,
|
|
2167
|
-
runner=runner,
|
|
2168
|
-
label="MSYS2 extraction",
|
|
2169
|
-
progress="extract",
|
|
2170
|
-
)
|
|
2346
|
+
extractor(archive, stage, reporter=reporter)
|
|
2171
2347
|
root = stage / "msys64"
|
|
2172
2348
|
if not (root / "usr" / "bin" / "bash.exe").is_file():
|
|
2173
2349
|
raise RuntimeInstallError(
|
|
@@ -23,7 +23,7 @@ from typing import Iterator, Mapping, Sequence
|
|
|
23
23
|
from packaging.version import InvalidVersion, Version
|
|
24
24
|
|
|
25
25
|
from railmux import restart_state, tmux_server
|
|
26
|
-
from railmux.windows_msys2 import
|
|
26
|
+
from railmux.windows_msys2 import MSYS2_BASE_LINEAGE_SHA256
|
|
27
27
|
|
|
28
28
|
|
|
29
29
|
CURRENT_APP_OPTION = "@railmux_current_app_v1"
|
|
@@ -136,7 +136,7 @@ def _base_identity(runtime: str) -> str | None:
|
|
|
136
136
|
or not isinstance(content, dict)
|
|
137
137
|
or content.get("schema") != 1
|
|
138
138
|
or content.get("runtime") != runtime
|
|
139
|
-
or content.get("archive_sha256") !=
|
|
139
|
+
or content.get("archive_sha256") != MSYS2_BASE_LINEAGE_SHA256
|
|
140
140
|
or not isinstance(content_id, str)
|
|
141
141
|
or _CONTENT_RE.fullmatch(content_id) is None
|
|
142
142
|
or not isinstance(package_count, int)
|
|
@@ -5,6 +5,7 @@ import io
|
|
|
5
5
|
import json
|
|
6
6
|
import re
|
|
7
7
|
import subprocess
|
|
8
|
+
import tarfile
|
|
8
9
|
from contextlib import contextmanager
|
|
9
10
|
from pathlib import Path
|
|
10
11
|
from types import SimpleNamespace
|
|
@@ -20,6 +21,7 @@ from railmux.windows_msys2 import (
|
|
|
20
21
|
MSYS2_ARCHIVE_SHA256,
|
|
21
22
|
MSYS2_ARCHIVE_SIZE,
|
|
22
23
|
MSYS2_ARCHIVE_SOURCES,
|
|
24
|
+
MSYS2_BASE_LINEAGE_SHA256,
|
|
23
25
|
MSYS2_RUNTIME_ID,
|
|
24
26
|
Msys2Runtime,
|
|
25
27
|
RuntimeInstallError,
|
|
@@ -102,7 +104,7 @@ def make_runtime(
|
|
|
102
104
|
{
|
|
103
105
|
"schema": 1,
|
|
104
106
|
"runtime": MSYS2_RUNTIME_ID,
|
|
105
|
-
"archive_sha256":
|
|
107
|
+
"archive_sha256": MSYS2_BASE_LINEAGE_SHA256,
|
|
106
108
|
"content_id": _TEST_CONTENT_ID,
|
|
107
109
|
"package_count": 3,
|
|
108
110
|
"core_packages": {
|
|
@@ -156,7 +158,12 @@ def add_marked_app(root: Path, version: str) -> Path:
|
|
|
156
158
|
|
|
157
159
|
|
|
158
160
|
def test_approved_archive_sources_are_https_and_share_one_pinned_artifact():
|
|
159
|
-
assert
|
|
161
|
+
assert MSYS2_ARCHIVE_NAME.endswith(".tar.xz")
|
|
162
|
+
assert not MSYS2_ARCHIVE_NAME.endswith(".sfx.exe")
|
|
163
|
+
assert MSYS2_ARCHIVE_SIZE == 53_466_096
|
|
164
|
+
assert MSYS2_ARCHIVE_SHA256 == (
|
|
165
|
+
"6b4a986a3ec4f1e40313bdf17903a6f5c854373d4230c40f14c5e35c4bac7fce"
|
|
166
|
+
)
|
|
160
167
|
assert len(MSYS2_ARCHIVE_SOURCES) == 4
|
|
161
168
|
assert len({url for _label, url in MSYS2_ARCHIVE_SOURCES}) == 4
|
|
162
169
|
for label, url in MSYS2_ARCHIVE_SOURCES:
|
|
@@ -728,21 +735,22 @@ def test_install_is_staged_and_activated_only_after_exact_probe(tmp_path):
|
|
|
728
735
|
assert destination.name == MSYS2_ARCHIVE_NAME
|
|
729
736
|
destination.write_bytes(b"verified fixture")
|
|
730
737
|
|
|
738
|
+
def extractor(archive, output, *, reporter):
|
|
739
|
+
assert archive.name == MSYS2_ARCHIVE_NAME
|
|
740
|
+
bash = output / "msys64" / "usr" / "bin" / "bash.exe"
|
|
741
|
+
bash.parent.mkdir(parents=True)
|
|
742
|
+
bash.write_bytes(b"fixture")
|
|
743
|
+
config = output / "msys64" / "etc" / "pacman.conf"
|
|
744
|
+
config.parent.mkdir(parents=True, exist_ok=True)
|
|
745
|
+
config.write_text(
|
|
746
|
+
"[options]\nParallelDownloads = 5\n"
|
|
747
|
+
"[mingw64]\nInclude = /etc/pacman.d/mirrorlist.mingw\n"
|
|
748
|
+
"[msys]\nInclude = /etc/pacman.d/mirrorlist.msys\n",
|
|
749
|
+
encoding="utf-8",
|
|
750
|
+
)
|
|
751
|
+
|
|
731
752
|
def runner(argv, *, env, check):
|
|
732
753
|
commands.append(argv)
|
|
733
|
-
if argv[0].endswith(MSYS2_ARCHIVE_NAME):
|
|
734
|
-
output = Path(next(arg[2:] for arg in argv if arg.startswith("-o")))
|
|
735
|
-
bash = output / "msys64" / "usr" / "bin" / "bash.exe"
|
|
736
|
-
bash.parent.mkdir(parents=True)
|
|
737
|
-
bash.write_bytes(b"fixture")
|
|
738
|
-
config = output / "msys64" / "etc" / "pacman.conf"
|
|
739
|
-
config.parent.mkdir(parents=True, exist_ok=True)
|
|
740
|
-
config.write_text(
|
|
741
|
-
"[options]\nParallelDownloads = 5\n"
|
|
742
|
-
"[mingw64]\nInclude = /etc/pacman.d/mirrorlist.mingw\n"
|
|
743
|
-
"[msys]\nInclude = /etc/pacman.d/mirrorlist.msys\n",
|
|
744
|
-
encoding="utf-8",
|
|
745
|
-
)
|
|
746
754
|
return completed(argv)
|
|
747
755
|
|
|
748
756
|
def probe(argv, *, env, timeout):
|
|
@@ -759,6 +767,7 @@ def test_install_is_staged_and_activated_only_after_exact_probe(tmp_path):
|
|
|
759
767
|
environ=environ,
|
|
760
768
|
downloader=downloader,
|
|
761
769
|
runner=runner,
|
|
770
|
+
extractor=extractor,
|
|
762
771
|
probe=probe,
|
|
763
772
|
lock_factory=unlocked,
|
|
764
773
|
mirror_optimizer=lambda _root: PacmanMirrorDecision(
|
|
@@ -790,6 +799,7 @@ def test_install_is_staged_and_activated_only_after_exact_probe(tmp_path):
|
|
|
790
799
|
assert any("--needed tmux python python-pip" in command for command in joined)
|
|
791
800
|
assert any('python -m venv "$1/venv"' in command for command in joined)
|
|
792
801
|
assert any('railmux[ssh]==$2' in command for command in joined)
|
|
802
|
+
assert all(MSYS2_ARCHIVE_NAME not in command for command in joined)
|
|
793
803
|
logs = list((Path(environ["LOCALAPPDATA"]) / "Railmux" / "logs").glob("*.log"))
|
|
794
804
|
assert len(logs) == 1
|
|
795
805
|
log = logs[0].read_text(encoding="utf-8")
|
|
@@ -1253,6 +1263,132 @@ def test_verified_base_archive_cache_avoids_a_second_download(tmp_path, monkeypa
|
|
|
1253
1263
|
assert source == "verified local cache"
|
|
1254
1264
|
|
|
1255
1265
|
|
|
1266
|
+
def _write_test_tar(path: Path, entries: list[tuple[tarfile.TarInfo, bytes]]) -> None:
|
|
1267
|
+
with tarfile.open(path, mode="w:xz") as archive:
|
|
1268
|
+
for member, payload in entries:
|
|
1269
|
+
archive.addfile(member, io.BytesIO(payload) if member.isreg() else None)
|
|
1270
|
+
|
|
1271
|
+
|
|
1272
|
+
def _tar_directory(name: str) -> tuple[tarfile.TarInfo, bytes]:
|
|
1273
|
+
member = tarfile.TarInfo(name)
|
|
1274
|
+
member.type = tarfile.DIRTYPE
|
|
1275
|
+
member.mode = 0o755
|
|
1276
|
+
return member, b""
|
|
1277
|
+
|
|
1278
|
+
|
|
1279
|
+
def _tar_file(name: str, payload: bytes) -> tuple[tarfile.TarInfo, bytes]:
|
|
1280
|
+
member = tarfile.TarInfo(name)
|
|
1281
|
+
member.size = len(payload)
|
|
1282
|
+
member.mode = 0o755 if name.endswith(".exe") else 0o644
|
|
1283
|
+
return member, payload
|
|
1284
|
+
|
|
1285
|
+
|
|
1286
|
+
def test_tar_xz_extraction_is_internal_bounded_and_progress_visible(
|
|
1287
|
+
tmp_path,
|
|
1288
|
+
monkeypatch,
|
|
1289
|
+
):
|
|
1290
|
+
entries = [
|
|
1291
|
+
_tar_directory("msys64"),
|
|
1292
|
+
_tar_directory("msys64/usr"),
|
|
1293
|
+
_tar_file("msys64/usr/bash.exe", b"verified bash"),
|
|
1294
|
+
]
|
|
1295
|
+
archive = tmp_path / MSYS2_ARCHIVE_NAME
|
|
1296
|
+
_write_test_tar(archive, entries)
|
|
1297
|
+
monkeypatch.setattr(windows_msys2, "MSYS2_ARCHIVE_MEMBER_COUNT", 3)
|
|
1298
|
+
monkeypatch.setattr(
|
|
1299
|
+
windows_msys2, "MSYS2_ARCHIVE_UNPACKED_SIZE", len(b"verified bash")
|
|
1300
|
+
)
|
|
1301
|
+
destination = tmp_path / "stage"
|
|
1302
|
+
destination.mkdir()
|
|
1303
|
+
log = tmp_path / "install.log"
|
|
1304
|
+
output = io.StringIO()
|
|
1305
|
+
|
|
1306
|
+
with InstallReporter(log, verbose=False, stream=output) as reporter:
|
|
1307
|
+
windows_msys2._extract_msys2_archive(archive, destination, reporter=reporter)
|
|
1308
|
+
|
|
1309
|
+
assert (destination / "msys64" / "usr" / "bash.exe").read_bytes() == (
|
|
1310
|
+
b"verified bash"
|
|
1311
|
+
)
|
|
1312
|
+
rendered = output.getvalue()
|
|
1313
|
+
assert "Extracting private runtime: 1/3 files (33%)" in rendered
|
|
1314
|
+
assert "Extracting private runtime: 3/3 files (100%)" in rendered
|
|
1315
|
+
assert "MSYS2 archive extraction" in log.read_text(encoding="utf-8")
|
|
1316
|
+
|
|
1317
|
+
|
|
1318
|
+
@pytest.mark.parametrize(
|
|
1319
|
+
("member", "message"),
|
|
1320
|
+
[
|
|
1321
|
+
(_tar_file("../outside", b"bad"), "unsafe path"),
|
|
1322
|
+
(_tar_file("/absolute", b"bad"), "unsafe path"),
|
|
1323
|
+
(_tar_file(r"msys64\..\outside", b"bad"), "unsafe path"),
|
|
1324
|
+
],
|
|
1325
|
+
)
|
|
1326
|
+
def test_tar_xz_extraction_rejects_paths_outside_staging(
|
|
1327
|
+
tmp_path,
|
|
1328
|
+
monkeypatch,
|
|
1329
|
+
member,
|
|
1330
|
+
message,
|
|
1331
|
+
):
|
|
1332
|
+
archive = tmp_path / MSYS2_ARCHIVE_NAME
|
|
1333
|
+
_write_test_tar(archive, [member])
|
|
1334
|
+
monkeypatch.setattr(windows_msys2, "MSYS2_ARCHIVE_MEMBER_COUNT", 1)
|
|
1335
|
+
monkeypatch.setattr(windows_msys2, "MSYS2_ARCHIVE_UNPACKED_SIZE", 3)
|
|
1336
|
+
destination = tmp_path / "stage"
|
|
1337
|
+
destination.mkdir()
|
|
1338
|
+
|
|
1339
|
+
with InstallReporter(
|
|
1340
|
+
tmp_path / "install.log", verbose=False, stream=io.StringIO()
|
|
1341
|
+
) as reporter:
|
|
1342
|
+
with pytest.raises(RuntimeInstallError, match=message):
|
|
1343
|
+
windows_msys2._extract_msys2_archive(
|
|
1344
|
+
archive, destination, reporter=reporter
|
|
1345
|
+
)
|
|
1346
|
+
|
|
1347
|
+
assert not (tmp_path / "outside").exists()
|
|
1348
|
+
|
|
1349
|
+
|
|
1350
|
+
def test_tar_xz_extraction_rejects_links_and_special_files(tmp_path, monkeypatch):
|
|
1351
|
+
member = tarfile.TarInfo("msys64/link")
|
|
1352
|
+
member.type = tarfile.SYMTYPE
|
|
1353
|
+
member.linkname = "../outside"
|
|
1354
|
+
archive = tmp_path / MSYS2_ARCHIVE_NAME
|
|
1355
|
+
_write_test_tar(archive, [_tar_directory("msys64"), (member, b"")])
|
|
1356
|
+
monkeypatch.setattr(windows_msys2, "MSYS2_ARCHIVE_MEMBER_COUNT", 2)
|
|
1357
|
+
monkeypatch.setattr(windows_msys2, "MSYS2_ARCHIVE_UNPACKED_SIZE", 0)
|
|
1358
|
+
destination = tmp_path / "stage"
|
|
1359
|
+
destination.mkdir()
|
|
1360
|
+
|
|
1361
|
+
with InstallReporter(
|
|
1362
|
+
tmp_path / "install.log", verbose=False, stream=io.StringIO()
|
|
1363
|
+
) as reporter:
|
|
1364
|
+
with pytest.raises(RuntimeInstallError, match="unsupported link"):
|
|
1365
|
+
windows_msys2._extract_msys2_archive(
|
|
1366
|
+
archive, destination, reporter=reporter
|
|
1367
|
+
)
|
|
1368
|
+
|
|
1369
|
+
|
|
1370
|
+
def test_tar_xz_extraction_rejects_changed_inventory_without_publishing(
|
|
1371
|
+
tmp_path,
|
|
1372
|
+
monkeypatch,
|
|
1373
|
+
):
|
|
1374
|
+
archive = tmp_path / MSYS2_ARCHIVE_NAME
|
|
1375
|
+
_write_test_tar(archive, [_tar_directory("msys64")])
|
|
1376
|
+
monkeypatch.setattr(windows_msys2, "MSYS2_ARCHIVE_MEMBER_COUNT", 2)
|
|
1377
|
+
monkeypatch.setattr(windows_msys2, "MSYS2_ARCHIVE_UNPACKED_SIZE", 0)
|
|
1378
|
+
destination = tmp_path / "stage"
|
|
1379
|
+
destination.mkdir()
|
|
1380
|
+
|
|
1381
|
+
with InstallReporter(
|
|
1382
|
+
tmp_path / "install.log", verbose=False, stream=io.StringIO()
|
|
1383
|
+
) as reporter:
|
|
1384
|
+
with pytest.raises(RuntimeInstallError, match="member count changed"):
|
|
1385
|
+
windows_msys2._extract_msys2_archive(
|
|
1386
|
+
archive, destination, reporter=reporter
|
|
1387
|
+
)
|
|
1388
|
+
|
|
1389
|
+
assert (destination / "msys64").is_dir()
|
|
1390
|
+
|
|
1391
|
+
|
|
1256
1392
|
def test_pacman_network_failure_retries_with_cache_and_relaxed_timeout(tmp_path):
|
|
1257
1393
|
root = tmp_path / "msys64"
|
|
1258
1394
|
bash = root / "usr" / "bin" / "bash.exe"
|
|
@@ -8,7 +8,7 @@ import pytest
|
|
|
8
8
|
import urwid
|
|
9
9
|
|
|
10
10
|
from railmux import tmux_server, windows_ui_transition as transition
|
|
11
|
-
from railmux.windows_msys2 import
|
|
11
|
+
from railmux.windows_msys2 import MSYS2_BASE_LINEAGE_SHA256
|
|
12
12
|
from railmux.ui import app as app_module
|
|
13
13
|
from railmux.ui.app import App
|
|
14
14
|
|
|
@@ -46,7 +46,7 @@ def _validated_tree(monkeypatch, tmp_path: Path) -> Path:
|
|
|
46
46
|
json.dumps({
|
|
47
47
|
"schema": 1,
|
|
48
48
|
"runtime": RUNTIME,
|
|
49
|
-
"archive_sha256":
|
|
49
|
+
"archive_sha256": MSYS2_BASE_LINEAGE_SHA256,
|
|
50
50
|
"content_id": CONTENT,
|
|
51
51
|
"package_count": 3,
|
|
52
52
|
"core_packages": {
|
|
@@ -88,7 +88,7 @@ def test_upgrade_exec_requires_exact_content_bound_app(monkeypatch, tmp_path):
|
|
|
88
88
|
json.dumps({
|
|
89
89
|
"schema": 1,
|
|
90
90
|
"runtime": RUNTIME,
|
|
91
|
-
"archive_sha256":
|
|
91
|
+
"archive_sha256": MSYS2_BASE_LINEAGE_SHA256,
|
|
92
92
|
"content_id": "c" * 64,
|
|
93
93
|
"package_count": 3,
|
|
94
94
|
"core_packages": {
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.4.0.dev25"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|