railmux 0.4.0.dev5__tar.gz → 0.4.0.dev7__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.dev5/src/railmux.egg-info → railmux-0.4.0.dev7}/PKG-INFO +1 -1
- railmux-0.4.0.dev7/src/railmux/__init__.py +1 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/windows_bootstrap.py +30 -7
- railmux-0.4.0.dev7/src/railmux/windows_install_log.py +189 -0
- railmux-0.4.0.dev7/src/railmux/windows_msys2.py +1099 -0
- railmux-0.4.0.dev7/src/railmux/windows_pacman.py +259 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7/src/railmux.egg-info}/PKG-INFO +1 -1
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux.egg-info/SOURCES.txt +4 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_windows_bootstrap.py +52 -0
- railmux-0.4.0.dev7/tests/test_windows_install_log.py +160 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_windows_msys2.py +272 -10
- railmux-0.4.0.dev7/tests/test_windows_pacman.py +223 -0
- railmux-0.4.0.dev5/src/railmux/__init__.py +0 -1
- railmux-0.4.0.dev5/src/railmux/windows_msys2.py +0 -580
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/LICENSE +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/MANIFEST.in +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/README.md +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/pyproject.toml +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/setup.cfg +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/__main__.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/atomic_file.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/background_index.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/cli.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/codex_index.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/config.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/config_cli.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/diagnostics.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/discovery.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/display_transport.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/entrypoint.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/fast_display_client.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/fast_display_history.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/fast_display_input.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/fast_display_protocol.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/fast_display_server.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/favorites.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/fuzzy.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/help_workspace.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/launcher.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/legacy_sessions.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/local_clipboard.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/local_open.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/models.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/modes.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/mouse_manager.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/orphan_marker.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/pane_surface.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/path_codec.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/provider_paths.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/remote_config.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/renames.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/restart_state.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/runtime_config.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/scroll_agent.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/scroll_manager.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/selection_isolation.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/self_update.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/session_cache.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/session_index.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/setting_contracts.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/settings.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/ssh_args.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/ssh_compat.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/ssh_display_diagnostics.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/ssh_doctor.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/system_deps.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/terminal_status.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/tmux_binding_manager.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/tmux_ctl.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/tmux_health.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/tmux_server.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/tool_panes.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/transcript.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/ui/__init__.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/ui/_widgets.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/ui/app.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/ui/keymap.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/ui/modals.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/ui/projects_pane.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/ui/running_pane.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/ui/sessions_pane.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/ui/sidebar.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/ui/statusbar.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/ui/workspace.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux.egg-info/dependency_links.txt +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux.egg-info/entry_points.txt +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux.egg-info/requires.txt +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux.egg-info/top_level.txt +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_agent_workspace.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_atomic_file.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_background_index.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_cli.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_codex_delete.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_codex_index.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_codex_lineage.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_codex_transcript.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_config.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_config_cli.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_denested_transport_benchmark.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_diagnostics.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_discovery.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_display_transport.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_fast_full_window.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_focus_highlight.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_fullscreen_binding.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_fuzzy.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_help_agent.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_help_workspace.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_history_cleanup.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_keymap.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_launcher.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_layout_profile.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_legacy_sessions.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_local_clipboard.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_local_open.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_modals.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_mode_selection.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_modes.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_mouse_manager.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_new_project.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_options.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_orphan_recovery.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_pane_surface.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_path_codec.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_projects_pane.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_provider_paths.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_release_notes.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_release_workflow.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_remote_config.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_renames.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_restart_state.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_running_pane.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_running_sort.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_runtime_config.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_scroll_agent.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_scroll_manager.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_selection_isolation.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_self_update.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_session_cache.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_session_index.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_sessions_pane.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_setting_contracts.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_settings.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_sidebar.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_soft_quit.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_ssh_args.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_ssh_compat.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_ssh_display_benchmark.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_ssh_display_diagnostics.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_ssh_doctor.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_status.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_statusbar.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_system_deps.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_terminal_size.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_terminal_status.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_termux_local_touch.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_tmux_binding_manager.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_tmux_ctl.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_tmux_health.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_tmux_integration.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_tmux_server.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_tmux_status.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_tool_panes.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_transcript.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_utf8_state.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_web_demo_recorder.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_wheel_smoke.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_widgets.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_windows_wrapper_contract.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_yolo_prompt.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tools/denested_transport_benchmark.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tools/release_notes.py +0 -0
- {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tools/wheel_smoke.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.0.dev7"
|
|
@@ -26,7 +26,7 @@ def _print_help() -> None:
|
|
|
26
26
|
" railmux ssh HOST [OPTIONS]\n"
|
|
27
27
|
" railmux config [--remote HOST] [OPTIONS]\n"
|
|
28
28
|
" railmux doctor [--remote HOST] [OPTIONS]\n"
|
|
29
|
-
" railmux runtime {status,install} [--yes]\n\n"
|
|
29
|
+
" railmux runtime {status,install} [--yes] [--verbose]\n\n"
|
|
30
30
|
"Windows preview: Railmux runs in a private managed MSYS2/tmux "
|
|
31
31
|
"runtime while Codex and Claude Code remain Windows-native and use "
|
|
32
32
|
"the existing Windows user session directories."
|
|
@@ -56,7 +56,8 @@ def _confirm_install(*, input_fn: Callable[[str], str] = input) -> bool:
|
|
|
56
56
|
try:
|
|
57
57
|
answer = input_fn(
|
|
58
58
|
"Install the private MSYS2/tmux runtime now? "
|
|
59
|
-
"This downloads
|
|
59
|
+
"This downloads a 50 MB base plus required updates and packages, "
|
|
60
|
+
"and uses about 300 MB or more of private disk space [y/N] "
|
|
60
61
|
)
|
|
61
62
|
except (EOFError, KeyboardInterrupt):
|
|
62
63
|
print()
|
|
@@ -68,13 +69,18 @@ def _install(
|
|
|
68
69
|
*,
|
|
69
70
|
environ: Mapping[str, str],
|
|
70
71
|
assume_yes: bool,
|
|
72
|
+
verbose: bool = False,
|
|
71
73
|
input_fn: Callable[[str], str] = input,
|
|
72
74
|
) -> Msys2Runtime | None:
|
|
73
75
|
if not assume_yes and not _confirm_install(input_fn=input_fn):
|
|
74
76
|
print("MSYS2 runtime installation cancelled.", file=sys.stderr)
|
|
75
77
|
return None
|
|
76
78
|
try:
|
|
77
|
-
runtime = install_managed_runtime(
|
|
79
|
+
runtime = install_managed_runtime(
|
|
80
|
+
version=__version__,
|
|
81
|
+
environ=environ,
|
|
82
|
+
verbose=verbose,
|
|
83
|
+
)
|
|
78
84
|
except RuntimeInstallError as exc:
|
|
79
85
|
print(f"error: {exc}", file=sys.stderr)
|
|
80
86
|
return None
|
|
@@ -135,12 +141,24 @@ def main(
|
|
|
135
141
|
if arguments[:2] == ["runtime", "status"] and len(arguments) == 2:
|
|
136
142
|
return _runtime_status(runtime, environ=environ)
|
|
137
143
|
if arguments and arguments[0] == "runtime":
|
|
138
|
-
|
|
139
|
-
|
|
144
|
+
install_arguments = arguments[1:]
|
|
145
|
+
install_flags = install_arguments[1:] if install_arguments[:1] == ["install"] else []
|
|
146
|
+
if (
|
|
147
|
+
not install_arguments
|
|
148
|
+
or install_arguments[0] != "install"
|
|
149
|
+
or len(install_flags) != len(set(install_flags))
|
|
150
|
+
or any(flag not in {"--yes", "--verbose"} for flag in install_flags)
|
|
151
|
+
):
|
|
152
|
+
print(
|
|
153
|
+
"error: usage: railmux runtime {status,install} "
|
|
154
|
+
"[--yes] [--verbose]",
|
|
155
|
+
file=sys.stderr,
|
|
156
|
+
)
|
|
140
157
|
return 2
|
|
141
158
|
runtime = runtime or _install(
|
|
142
159
|
environ=environ,
|
|
143
|
-
assume_yes=
|
|
160
|
+
assume_yes="--yes" in install_flags,
|
|
161
|
+
verbose="--verbose" in install_flags,
|
|
144
162
|
input_fn=input_fn,
|
|
145
163
|
)
|
|
146
164
|
return 0 if runtime is not None else 2
|
|
@@ -162,7 +180,12 @@ def main(
|
|
|
162
180
|
file=sys.stderr,
|
|
163
181
|
)
|
|
164
182
|
return 2
|
|
165
|
-
runtime = _install(
|
|
183
|
+
runtime = _install(
|
|
184
|
+
environ=environ,
|
|
185
|
+
assume_yes=False,
|
|
186
|
+
verbose=False,
|
|
187
|
+
input_fn=input_fn,
|
|
188
|
+
)
|
|
166
189
|
if runtime is None:
|
|
167
190
|
return 2
|
|
168
191
|
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""Bounded user output and complete UTF-8 logs for Windows runtime setup."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
import sys
|
|
7
|
+
import time
|
|
8
|
+
from collections import deque
|
|
9
|
+
from collections.abc import Callable, Mapping
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import BinaryIO, TextIO
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
_LOG_KEEP_COUNT = 5
|
|
15
|
+
_FAILURE_LINE_LIMIT = 500
|
|
16
|
+
_LOG_NAME_RE = re.compile(
|
|
17
|
+
r"install-[0-9A-Za-z.+-]+-[0-9]{8}T[0-9]{6}Z-[0-9]+\.log\Z"
|
|
18
|
+
)
|
|
19
|
+
_URL_CREDENTIAL_RE = re.compile(r"(https?://)[^/@\s]+@", re.IGNORECASE)
|
|
20
|
+
_SECRET_QUERY_RE = re.compile(
|
|
21
|
+
r"(?i)([?&](?:access_token|password|token)=)[^&\s]+"
|
|
22
|
+
)
|
|
23
|
+
_MIRROR_WARNING_MARKERS = (
|
|
24
|
+
"too many errors from ",
|
|
25
|
+
"failed retrieving file ",
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _redact_line(line: str) -> str:
|
|
30
|
+
line = _URL_CREDENTIAL_RE.sub(r"\1<redacted>@", line)
|
|
31
|
+
return _SECRET_QUERY_RE.sub(r"\1<redacted>", line)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def install_log_path(
|
|
35
|
+
environ: Mapping[str, str],
|
|
36
|
+
*,
|
|
37
|
+
version: str,
|
|
38
|
+
clock: Callable[[], float] = time.time,
|
|
39
|
+
) -> Path:
|
|
40
|
+
local_app_data = environ.get("LOCALAPPDATA", "").strip()
|
|
41
|
+
if not local_app_data:
|
|
42
|
+
raise OSError("LOCALAPPDATA is unavailable")
|
|
43
|
+
stamp = time.strftime("%Y%m%dT%H%M%SZ", time.gmtime(clock()))
|
|
44
|
+
return (
|
|
45
|
+
Path(local_app_data)
|
|
46
|
+
/ "Railmux"
|
|
47
|
+
/ "logs"
|
|
48
|
+
/ f"install-{version}-{stamp}-{os.getpid()}.log"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _prune_install_logs(directory: Path, *, keep: int = _LOG_KEEP_COUNT) -> None:
|
|
53
|
+
candidates: list[Path] = []
|
|
54
|
+
try:
|
|
55
|
+
entries = list(directory.iterdir())
|
|
56
|
+
except OSError:
|
|
57
|
+
return
|
|
58
|
+
for entry in entries:
|
|
59
|
+
if entry.is_file() and _LOG_NAME_RE.fullmatch(entry.name):
|
|
60
|
+
candidates.append(entry)
|
|
61
|
+
try:
|
|
62
|
+
candidates.sort(
|
|
63
|
+
key=lambda path: (path.stat().st_mtime_ns, path.name),
|
|
64
|
+
reverse=True,
|
|
65
|
+
)
|
|
66
|
+
except OSError:
|
|
67
|
+
return
|
|
68
|
+
for stale in candidates[max(keep, 1) :]:
|
|
69
|
+
try:
|
|
70
|
+
stale.unlink()
|
|
71
|
+
except OSError:
|
|
72
|
+
continue
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class InstallReporter:
|
|
76
|
+
"""Show stable phases while retaining complete command output."""
|
|
77
|
+
|
|
78
|
+
def __init__(
|
|
79
|
+
self,
|
|
80
|
+
path: Path,
|
|
81
|
+
*,
|
|
82
|
+
verbose: bool,
|
|
83
|
+
stream: TextIO | None = None,
|
|
84
|
+
) -> None:
|
|
85
|
+
self.path = path
|
|
86
|
+
self.verbose = verbose
|
|
87
|
+
self.stream = sys.stdout if stream is None else stream
|
|
88
|
+
self._log: TextIO | None = None
|
|
89
|
+
self._tail: deque[str] = deque(maxlen=20)
|
|
90
|
+
self._reported_mirror_warning = False
|
|
91
|
+
|
|
92
|
+
def __enter__(self) -> InstallReporter:
|
|
93
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
94
|
+
self._log = self.path.open("x", encoding="utf-8", newline="\n")
|
|
95
|
+
self._write_log("Railmux Windows runtime installation\n")
|
|
96
|
+
_prune_install_logs(self.path.parent)
|
|
97
|
+
return self
|
|
98
|
+
|
|
99
|
+
def __exit__(self, _exc_type, _exc, _traceback) -> None:
|
|
100
|
+
if self._log is not None:
|
|
101
|
+
self._log.close()
|
|
102
|
+
self._log = None
|
|
103
|
+
|
|
104
|
+
def _write_log(self, text: str) -> None:
|
|
105
|
+
if self._log is None:
|
|
106
|
+
return
|
|
107
|
+
self._log.write(text)
|
|
108
|
+
self._log.flush()
|
|
109
|
+
|
|
110
|
+
def _console(self, text: str, *, flush: bool = True) -> None:
|
|
111
|
+
try:
|
|
112
|
+
print(text, file=self.stream, flush=flush)
|
|
113
|
+
except UnicodeEncodeError:
|
|
114
|
+
encoding = getattr(self.stream, "encoding", None) or "utf-8"
|
|
115
|
+
safe = text.encode(encoding, errors="replace").decode(encoding)
|
|
116
|
+
print(safe, file=self.stream, flush=flush)
|
|
117
|
+
|
|
118
|
+
def phase(self, number: int, total: int, message: str) -> None:
|
|
119
|
+
rendered = f"[{number}/{total}] {message}"
|
|
120
|
+
self._console(rendered)
|
|
121
|
+
self._write_log(f"\n{rendered}\n")
|
|
122
|
+
|
|
123
|
+
def done(self, detail: str | None = None) -> None:
|
|
124
|
+
rendered = " done" + (f" · {detail}" if detail else "")
|
|
125
|
+
self._console(rendered)
|
|
126
|
+
self._write_log(f"{rendered}\n")
|
|
127
|
+
|
|
128
|
+
def note(self, message: str) -> None:
|
|
129
|
+
rendered = f" {message}"
|
|
130
|
+
self._console(rendered)
|
|
131
|
+
self._write_log(f"{rendered}\n")
|
|
132
|
+
|
|
133
|
+
def command_started(self, label: str) -> None:
|
|
134
|
+
self._tail.clear()
|
|
135
|
+
self._reported_mirror_warning = False
|
|
136
|
+
self._write_log(f"\n--- {label} ---\n")
|
|
137
|
+
|
|
138
|
+
def command_output(self, output: bytes | str | None) -> None:
|
|
139
|
+
if not output:
|
|
140
|
+
return
|
|
141
|
+
if isinstance(output, bytes):
|
|
142
|
+
text = output.decode("utf-8", errors="replace")
|
|
143
|
+
else:
|
|
144
|
+
text = output
|
|
145
|
+
normalized = text.replace("\r\n", "\n").replace("\r", "\n")
|
|
146
|
+
for raw_line in normalized.splitlines():
|
|
147
|
+
line = _redact_line(raw_line)
|
|
148
|
+
self._write_log(f"{line}\n")
|
|
149
|
+
if line.strip():
|
|
150
|
+
self._tail.append(line)
|
|
151
|
+
if self.verbose:
|
|
152
|
+
self._console(line)
|
|
153
|
+
elif (
|
|
154
|
+
not self._reported_mirror_warning
|
|
155
|
+
and any(marker in line.lower() for marker in _MIRROR_WARNING_MARKERS)
|
|
156
|
+
):
|
|
157
|
+
self.note(
|
|
158
|
+
"A package mirror failed; pacman is trying the next "
|
|
159
|
+
"approved source…"
|
|
160
|
+
)
|
|
161
|
+
self._reported_mirror_warning = True
|
|
162
|
+
|
|
163
|
+
def command_failed(self, label: str, returncode: int) -> None:
|
|
164
|
+
self.note(f"{label} failed with exit code {returncode}.")
|
|
165
|
+
if self._tail:
|
|
166
|
+
self.note("Last output:")
|
|
167
|
+
for line in list(self._tail)[-8:]:
|
|
168
|
+
if len(line) > _FAILURE_LINE_LIMIT:
|
|
169
|
+
line = f"{line[:_FAILURE_LINE_LIMIT]}… [truncated; see log]"
|
|
170
|
+
self._console(f" {line}")
|
|
171
|
+
self.note(f"Full UTF-8 log: {self.path}")
|
|
172
|
+
|
|
173
|
+
def finish(self) -> None:
|
|
174
|
+
self._write_log("\nInstallation completed successfully.\n")
|
|
175
|
+
self._console(f"Installation log: {self.path}")
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def stream_process_output(
|
|
179
|
+
process: object,
|
|
180
|
+
reporter: InstallReporter,
|
|
181
|
+
) -> int:
|
|
182
|
+
stdout: BinaryIO | None = getattr(process, "stdout", None)
|
|
183
|
+
if stdout is not None:
|
|
184
|
+
while True:
|
|
185
|
+
chunk = stdout.readline()
|
|
186
|
+
if not chunk:
|
|
187
|
+
break
|
|
188
|
+
reporter.command_output(chunk)
|
|
189
|
+
return process.wait()
|