railmux 0.4.0.dev4__tar.gz → 0.4.0.dev5__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.dev4/src/railmux.egg-info → railmux-0.4.0.dev5}/PKG-INFO +1 -1
- railmux-0.4.0.dev5/src/railmux/__init__.py +1 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/src/railmux/windows_msys2.py +158 -9
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5/src/railmux.egg-info}/PKG-INFO +1 -1
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_windows_msys2.py +173 -6
- railmux-0.4.0.dev4/src/railmux/__init__.py +0 -1
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/LICENSE +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/MANIFEST.in +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/README.md +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/pyproject.toml +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/setup.cfg +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/src/railmux/__main__.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/src/railmux/atomic_file.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/src/railmux/background_index.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/src/railmux/cli.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/src/railmux/codex_index.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/src/railmux/config.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/src/railmux/config_cli.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/src/railmux/diagnostics.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/src/railmux/discovery.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/src/railmux/display_transport.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/src/railmux/entrypoint.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/src/railmux/fast_display_client.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/src/railmux/fast_display_history.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/src/railmux/fast_display_input.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/src/railmux/fast_display_protocol.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/src/railmux/fast_display_server.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/src/railmux/favorites.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/src/railmux/fuzzy.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/src/railmux/help_workspace.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/src/railmux/launcher.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/src/railmux/legacy_sessions.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/src/railmux/local_clipboard.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/src/railmux/local_open.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/src/railmux/models.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/src/railmux/modes.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/src/railmux/mouse_manager.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/src/railmux/orphan_marker.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/src/railmux/pane_surface.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/src/railmux/path_codec.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/src/railmux/provider_paths.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/src/railmux/remote_config.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/src/railmux/renames.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/src/railmux/restart_state.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/src/railmux/runtime_config.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/src/railmux/scroll_agent.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/src/railmux/scroll_manager.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/src/railmux/selection_isolation.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/src/railmux/self_update.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/src/railmux/session_cache.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/src/railmux/session_index.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/src/railmux/setting_contracts.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/src/railmux/settings.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/src/railmux/ssh_args.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/src/railmux/ssh_compat.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/src/railmux/ssh_display_diagnostics.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/src/railmux/ssh_doctor.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/src/railmux/system_deps.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/src/railmux/terminal_status.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/src/railmux/tmux_binding_manager.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/src/railmux/tmux_ctl.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/src/railmux/tmux_health.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/src/railmux/tmux_server.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/src/railmux/tool_panes.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/src/railmux/transcript.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/src/railmux/ui/__init__.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/src/railmux/ui/_widgets.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/src/railmux/ui/app.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/src/railmux/ui/keymap.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/src/railmux/ui/modals.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/src/railmux/ui/projects_pane.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/src/railmux/ui/running_pane.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/src/railmux/ui/sessions_pane.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/src/railmux/ui/sidebar.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/src/railmux/ui/statusbar.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/src/railmux/ui/workspace.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/src/railmux/windows_bootstrap.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/src/railmux.egg-info/SOURCES.txt +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/src/railmux.egg-info/dependency_links.txt +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/src/railmux.egg-info/entry_points.txt +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/src/railmux.egg-info/requires.txt +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/src/railmux.egg-info/top_level.txt +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_agent_workspace.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_atomic_file.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_background_index.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_cli.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_codex_delete.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_codex_index.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_codex_lineage.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_codex_transcript.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_config.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_config_cli.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_denested_transport_benchmark.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_diagnostics.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_discovery.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_display_transport.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_fast_full_window.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_focus_highlight.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_fullscreen_binding.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_fuzzy.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_help_agent.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_help_workspace.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_history_cleanup.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_keymap.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_launcher.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_layout_profile.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_legacy_sessions.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_local_clipboard.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_local_open.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_modals.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_mode_selection.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_modes.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_mouse_manager.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_new_project.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_options.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_orphan_recovery.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_pane_surface.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_path_codec.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_projects_pane.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_provider_paths.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_release_notes.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_release_workflow.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_remote_config.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_renames.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_restart_state.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_running_pane.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_running_sort.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_runtime_config.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_scroll_agent.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_scroll_manager.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_selection_isolation.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_self_update.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_session_cache.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_session_index.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_sessions_pane.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_setting_contracts.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_settings.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_sidebar.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_soft_quit.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_ssh_args.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_ssh_compat.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_ssh_display_benchmark.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_ssh_display_diagnostics.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_ssh_doctor.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_status.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_statusbar.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_system_deps.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_terminal_size.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_terminal_status.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_termux_local_touch.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_tmux_binding_manager.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_tmux_ctl.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_tmux_health.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_tmux_integration.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_tmux_server.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_tmux_status.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_tool_panes.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_transcript.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_utf8_state.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_web_demo_recorder.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_wheel_smoke.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_widgets.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_windows_bootstrap.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_windows_wrapper_contract.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tests/test_yolo_prompt.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tools/denested_transport_benchmark.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tools/release_notes.py +0 -0
- {railmux-0.4.0.dev4 → railmux-0.4.0.dev5}/tools/wheel_smoke.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.0.dev5"
|
|
@@ -7,13 +7,16 @@ remain Windows-native.
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
9
|
import hashlib
|
|
10
|
+
import http.client
|
|
10
11
|
import json
|
|
11
12
|
import os
|
|
12
13
|
import re
|
|
13
14
|
import subprocess
|
|
15
|
+
import sys
|
|
14
16
|
import tempfile
|
|
15
17
|
import time
|
|
16
18
|
import urllib.error
|
|
19
|
+
import urllib.parse
|
|
17
20
|
import urllib.request
|
|
18
21
|
from collections.abc import Callable, Iterator, Mapping, Sequence
|
|
19
22
|
from contextlib import contextmanager
|
|
@@ -23,10 +26,27 @@ from pathlib import Path
|
|
|
23
26
|
|
|
24
27
|
MSYS2_RELEASE = "2026-03-22"
|
|
25
28
|
MSYS2_ARCHIVE_NAME = f"msys2-base-x86_64-{MSYS2_RELEASE.replace('-', '')}.sfx.exe"
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
+
MSYS2_ARCHIVE_SOURCES = (
|
|
30
|
+
(
|
|
31
|
+
"GitHub",
|
|
32
|
+
"https://github.com/msys2/msys2-installer/releases/download/"
|
|
33
|
+
f"{MSYS2_RELEASE}/{MSYS2_ARCHIVE_NAME}",
|
|
34
|
+
),
|
|
35
|
+
(
|
|
36
|
+
"MSYS2 repository",
|
|
37
|
+
f"https://repo.msys2.org/distrib/x86_64/{MSYS2_ARCHIVE_NAME}",
|
|
38
|
+
),
|
|
39
|
+
(
|
|
40
|
+
"TUNA mirror",
|
|
41
|
+
f"https://mirrors.tuna.tsinghua.edu.cn/msys2/distrib/x86_64/"
|
|
42
|
+
f"{MSYS2_ARCHIVE_NAME}",
|
|
43
|
+
),
|
|
44
|
+
(
|
|
45
|
+
"NJU mirror",
|
|
46
|
+
f"https://mirror.nju.edu.cn/msys2/distrib/x86_64/{MSYS2_ARCHIVE_NAME}",
|
|
47
|
+
),
|
|
29
48
|
)
|
|
49
|
+
MSYS2_ARCHIVE_SIZE = 52_820_994
|
|
30
50
|
MSYS2_ARCHIVE_SHA256 = (
|
|
31
51
|
"6fe0cc8154132040e034ff4daface2a4163a9d1f6ebaaa1133394bff460bd5cf"
|
|
32
52
|
)
|
|
@@ -42,6 +62,7 @@ _HANDOFF_COMMAND = (
|
|
|
42
62
|
)
|
|
43
63
|
_PROBE_TIMEOUT_SECONDS = 15.0
|
|
44
64
|
_DOWNLOAD_LIMIT = 128 * 1024 * 1024
|
|
65
|
+
_DOWNLOAD_LOG_STEP = 8 * 1024 * 1024
|
|
45
66
|
_VERSION_RE = re.compile(r"[0-9]+(?:\.[0-9]+)*(?:\.dev[0-9]+)?\Z")
|
|
46
67
|
|
|
47
68
|
_INITIAL_UPDATE_COMMAND = (
|
|
@@ -71,6 +92,61 @@ Runner = Callable[..., subprocess.CompletedProcess]
|
|
|
71
92
|
Downloader = Callable[[str, Path, str], None]
|
|
72
93
|
|
|
73
94
|
|
|
95
|
+
def _format_download_size(size: int) -> str:
|
|
96
|
+
if size >= 1024 * 1024:
|
|
97
|
+
return f"{size / (1024 * 1024):.1f} MiB"
|
|
98
|
+
if size >= 1024:
|
|
99
|
+
return f"{size / 1024:.1f} KiB"
|
|
100
|
+
return f"{size} B"
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class _DownloadProgress:
|
|
104
|
+
"""Render bounded progress without flooding redirected logs."""
|
|
105
|
+
|
|
106
|
+
def __init__(self, url: str) -> None:
|
|
107
|
+
self._stream = sys.stderr
|
|
108
|
+
self._source = urllib.parse.urlsplit(url).hostname or "approved source"
|
|
109
|
+
self._interactive = bool(getattr(self._stream, "isatty", lambda: False)())
|
|
110
|
+
self._last_logged = 0
|
|
111
|
+
self._active_line = False
|
|
112
|
+
|
|
113
|
+
def _message(self, downloaded: int, expected: int | None) -> str:
|
|
114
|
+
current = _format_download_size(downloaded)
|
|
115
|
+
if expected:
|
|
116
|
+
percent = min(100.0, downloaded * 100.0 / expected)
|
|
117
|
+
return (
|
|
118
|
+
f" {self._source}: {current} / "
|
|
119
|
+
f"{_format_download_size(expected)} ({percent:.1f}%)"
|
|
120
|
+
)
|
|
121
|
+
return f" {self._source}: {current} downloaded"
|
|
122
|
+
|
|
123
|
+
def update(
|
|
124
|
+
self,
|
|
125
|
+
downloaded: int,
|
|
126
|
+
expected: int | None,
|
|
127
|
+
*,
|
|
128
|
+
final: bool = False,
|
|
129
|
+
) -> None:
|
|
130
|
+
message = self._message(downloaded, expected)
|
|
131
|
+
if self._interactive:
|
|
132
|
+
self._stream.write(f"\r{message}")
|
|
133
|
+
self._active_line = not final
|
|
134
|
+
if final:
|
|
135
|
+
self._stream.write("\n")
|
|
136
|
+
self._stream.flush()
|
|
137
|
+
return
|
|
138
|
+
if final or downloaded - self._last_logged >= _DOWNLOAD_LOG_STEP:
|
|
139
|
+
self._stream.write(f"{message}\n")
|
|
140
|
+
self._stream.flush()
|
|
141
|
+
self._last_logged = downloaded
|
|
142
|
+
|
|
143
|
+
def abort(self) -> None:
|
|
144
|
+
if self._interactive and self._active_line:
|
|
145
|
+
self._stream.write("\n")
|
|
146
|
+
self._stream.flush()
|
|
147
|
+
self._active_line = False
|
|
148
|
+
|
|
149
|
+
|
|
74
150
|
@dataclass(frozen=True)
|
|
75
151
|
class Msys2Runtime:
|
|
76
152
|
root: Path
|
|
@@ -227,10 +303,30 @@ def find_runtime(
|
|
|
227
303
|
|
|
228
304
|
def download_verified(url: str, destination: Path, sha256: str) -> None:
|
|
229
305
|
"""Download one bounded artifact and verify it before execution."""
|
|
306
|
+
if urllib.parse.urlsplit(url).scheme.lower() != "https":
|
|
307
|
+
raise RuntimeInstallError("the MSYS2 runtime source did not use HTTPS")
|
|
230
308
|
digest = hashlib.sha256()
|
|
231
309
|
total = 0
|
|
310
|
+
expected: int | None = None
|
|
311
|
+
progress = _DownloadProgress(url)
|
|
232
312
|
try:
|
|
233
313
|
with urllib.request.urlopen(url, timeout=60) as response:
|
|
314
|
+
final_url = getattr(response, "geturl", lambda: url)()
|
|
315
|
+
if urllib.parse.urlsplit(final_url).scheme.lower() != "https":
|
|
316
|
+
raise RuntimeInstallError(
|
|
317
|
+
"the MSYS2 runtime source redirected outside HTTPS"
|
|
318
|
+
)
|
|
319
|
+
raw_length = response.headers.get("Content-Length")
|
|
320
|
+
if raw_length is not None:
|
|
321
|
+
try:
|
|
322
|
+
parsed_length = int(raw_length)
|
|
323
|
+
except (TypeError, ValueError):
|
|
324
|
+
parsed_length = 0
|
|
325
|
+
if parsed_length > 0:
|
|
326
|
+
expected = parsed_length
|
|
327
|
+
if expected is not None and expected > _DOWNLOAD_LIMIT:
|
|
328
|
+
raise RuntimeInstallError("MSYS2 download exceeded its size limit")
|
|
329
|
+
progress.update(0, expected)
|
|
234
330
|
with destination.open("xb") as output:
|
|
235
331
|
while True:
|
|
236
332
|
chunk = response.read(1024 * 1024)
|
|
@@ -241,12 +337,61 @@ def download_verified(url: str, destination: Path, sha256: str) -> None:
|
|
|
241
337
|
raise RuntimeInstallError("MSYS2 download exceeded its size limit")
|
|
242
338
|
digest.update(chunk)
|
|
243
339
|
output.write(chunk)
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
340
|
+
progress.update(total, expected)
|
|
341
|
+
if digest.hexdigest().lower() != sha256.lower():
|
|
342
|
+
raise RuntimeInstallError(
|
|
343
|
+
"the downloaded MSYS2 archive failed SHA-256 verification"
|
|
344
|
+
)
|
|
345
|
+
except (
|
|
346
|
+
RuntimeInstallError,
|
|
347
|
+
OSError,
|
|
348
|
+
urllib.error.URLError,
|
|
349
|
+
http.client.HTTPException,
|
|
350
|
+
) as exc:
|
|
351
|
+
progress.abort()
|
|
352
|
+
try:
|
|
353
|
+
destination.unlink(missing_ok=True)
|
|
354
|
+
except OSError as cleanup_error:
|
|
355
|
+
raise RuntimeInstallError(
|
|
356
|
+
"could not remove an incomplete MSYS2 download"
|
|
357
|
+
) from cleanup_error
|
|
358
|
+
if isinstance(exc, RuntimeInstallError):
|
|
359
|
+
raise
|
|
247
360
|
raise RuntimeInstallError("could not download the pinned MSYS2 runtime") from exc
|
|
248
|
-
|
|
249
|
-
|
|
361
|
+
progress.update(total, expected, final=True)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def download_from_sources(
|
|
365
|
+
destination: Path,
|
|
366
|
+
sha256: str,
|
|
367
|
+
*,
|
|
368
|
+
sources: Sequence[tuple[str, str]] = MSYS2_ARCHIVE_SOURCES,
|
|
369
|
+
downloader: Downloader = download_verified,
|
|
370
|
+
) -> str:
|
|
371
|
+
"""Try approved transports in order; every source uses the same pinned hash."""
|
|
372
|
+
last_error: RuntimeInstallError | None = None
|
|
373
|
+
for index, (label, url) in enumerate(sources, start=1):
|
|
374
|
+
print(f"Downloading from {label} ({index}/{len(sources)})…")
|
|
375
|
+
try:
|
|
376
|
+
downloader(url, destination, sha256)
|
|
377
|
+
except RuntimeInstallError as exc:
|
|
378
|
+
last_error = exc
|
|
379
|
+
try:
|
|
380
|
+
destination.unlink(missing_ok=True)
|
|
381
|
+
except OSError as cleanup_error:
|
|
382
|
+
raise RuntimeInstallError(
|
|
383
|
+
"could not remove an incomplete MSYS2 download"
|
|
384
|
+
) from cleanup_error
|
|
385
|
+
if index < len(sources):
|
|
386
|
+
print(
|
|
387
|
+
f"{label} failed ({exc}); trying the next approved source…",
|
|
388
|
+
file=sys.stderr,
|
|
389
|
+
)
|
|
390
|
+
continue
|
|
391
|
+
return label
|
|
392
|
+
raise RuntimeInstallError(
|
|
393
|
+
"could not download the pinned MSYS2 runtime from any approved source"
|
|
394
|
+
) from last_error
|
|
250
395
|
|
|
251
396
|
|
|
252
397
|
def _run_checked(
|
|
@@ -366,7 +511,11 @@ def install_managed_runtime(
|
|
|
366
511
|
stage = Path(raw_stage)
|
|
367
512
|
archive = stage / MSYS2_ARCHIVE_NAME
|
|
368
513
|
print(f"Downloading verified MSYS2 {MSYS2_RELEASE} runtime…")
|
|
369
|
-
|
|
514
|
+
download_from_sources(
|
|
515
|
+
archive,
|
|
516
|
+
MSYS2_ARCHIVE_SHA256,
|
|
517
|
+
downloader=downloader,
|
|
518
|
+
)
|
|
370
519
|
|
|
371
520
|
_run_checked(
|
|
372
521
|
[str(archive), "-y", f"-o{stage}"],
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import hashlib
|
|
4
|
+
import io
|
|
3
5
|
import json
|
|
4
6
|
import subprocess
|
|
5
7
|
from contextlib import contextmanager
|
|
@@ -8,11 +10,17 @@ from unittest.mock import MagicMock
|
|
|
8
10
|
|
|
9
11
|
import pytest
|
|
10
12
|
|
|
13
|
+
from railmux import windows_msys2
|
|
11
14
|
from railmux.windows_msys2 import (
|
|
12
15
|
MSYS2_ARCHIVE_NAME,
|
|
16
|
+
MSYS2_ARCHIVE_SHA256,
|
|
17
|
+
MSYS2_ARCHIVE_SIZE,
|
|
18
|
+
MSYS2_ARCHIVE_SOURCES,
|
|
13
19
|
MSYS2_RUNTIME_ID,
|
|
14
20
|
Msys2Runtime,
|
|
15
21
|
RuntimeInstallError,
|
|
22
|
+
download_from_sources,
|
|
23
|
+
download_verified,
|
|
16
24
|
find_runtime,
|
|
17
25
|
install_managed_runtime,
|
|
18
26
|
managed_root,
|
|
@@ -20,7 +28,36 @@ from railmux.windows_msys2 import (
|
|
|
20
28
|
)
|
|
21
29
|
|
|
22
30
|
|
|
23
|
-
VERSION = "0.4.0.
|
|
31
|
+
VERSION = "0.4.0.dev5"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class TtyBuffer(io.StringIO):
|
|
35
|
+
def isatty(self):
|
|
36
|
+
return True
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class FakeResponse(io.BytesIO):
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
payload: bytes,
|
|
43
|
+
*,
|
|
44
|
+
content_length: str | None = None,
|
|
45
|
+
final_url: str = "https://example.invalid/archive.exe",
|
|
46
|
+
):
|
|
47
|
+
super().__init__(payload)
|
|
48
|
+
self._final_url = final_url
|
|
49
|
+
self.headers = {}
|
|
50
|
+
if content_length is not None:
|
|
51
|
+
self.headers["Content-Length"] = content_length
|
|
52
|
+
|
|
53
|
+
def __enter__(self):
|
|
54
|
+
return self
|
|
55
|
+
|
|
56
|
+
def __exit__(self, _exc_type, _exc, _traceback):
|
|
57
|
+
self.close()
|
|
58
|
+
|
|
59
|
+
def geturl(self):
|
|
60
|
+
return self._final_url
|
|
24
61
|
|
|
25
62
|
|
|
26
63
|
def completed(argv, returncode=0, stdout=b"", stderr=b""):
|
|
@@ -41,6 +78,136 @@ def make_runtime(root: Path, *, managed: bool) -> Msys2Runtime:
|
|
|
41
78
|
return Msys2Runtime(root, managed=managed)
|
|
42
79
|
|
|
43
80
|
|
|
81
|
+
def test_approved_archive_sources_are_https_and_share_one_pinned_artifact():
|
|
82
|
+
assert MSYS2_ARCHIVE_SIZE == 52_820_994
|
|
83
|
+
assert len(MSYS2_ARCHIVE_SOURCES) == 4
|
|
84
|
+
assert len({url for _label, url in MSYS2_ARCHIVE_SOURCES}) == 4
|
|
85
|
+
for label, url in MSYS2_ARCHIVE_SOURCES:
|
|
86
|
+
assert label
|
|
87
|
+
assert url.startswith("https://")
|
|
88
|
+
assert url.endswith(f"/{MSYS2_ARCHIVE_NAME}")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_download_reports_bytes_and_percentage_on_a_terminal(tmp_path, monkeypatch):
|
|
92
|
+
payload = b"a" * (2 * 1024 * 1024)
|
|
93
|
+
progress = TtyBuffer()
|
|
94
|
+
monkeypatch.setattr(windows_msys2.sys, "stderr", progress)
|
|
95
|
+
monkeypatch.setattr(
|
|
96
|
+
windows_msys2.urllib.request,
|
|
97
|
+
"urlopen",
|
|
98
|
+
lambda _url, timeout: FakeResponse(
|
|
99
|
+
payload,
|
|
100
|
+
content_length=str(len(payload)),
|
|
101
|
+
),
|
|
102
|
+
)
|
|
103
|
+
destination = tmp_path / "archive.exe"
|
|
104
|
+
|
|
105
|
+
download_verified(
|
|
106
|
+
"https://example.invalid/archive.exe",
|
|
107
|
+
destination,
|
|
108
|
+
hashlib.sha256(payload).hexdigest(),
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
rendered = progress.getvalue()
|
|
112
|
+
assert "\r example.invalid: 1.0 MiB / 2.0 MiB (50.0%)" in rendered
|
|
113
|
+
assert "\r example.invalid: 2.0 MiB / 2.0 MiB (100.0%)\n" in rendered
|
|
114
|
+
assert destination.read_bytes() == payload
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def test_download_hash_failure_removes_the_untrusted_archive(tmp_path, monkeypatch):
|
|
118
|
+
payload = b"not the pinned archive"
|
|
119
|
+
monkeypatch.setattr(
|
|
120
|
+
windows_msys2.urllib.request,
|
|
121
|
+
"urlopen",
|
|
122
|
+
lambda _url, timeout: FakeResponse(
|
|
123
|
+
payload,
|
|
124
|
+
content_length=str(len(payload)),
|
|
125
|
+
),
|
|
126
|
+
)
|
|
127
|
+
destination = tmp_path / "archive.exe"
|
|
128
|
+
|
|
129
|
+
with pytest.raises(RuntimeInstallError, match="SHA-256 verification"):
|
|
130
|
+
download_verified(
|
|
131
|
+
"https://example.invalid/archive.exe",
|
|
132
|
+
destination,
|
|
133
|
+
MSYS2_ARCHIVE_SHA256,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
assert not destination.exists()
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def test_download_rejects_an_https_downgrade_before_writing(tmp_path, monkeypatch):
|
|
140
|
+
monkeypatch.setattr(
|
|
141
|
+
windows_msys2.urllib.request,
|
|
142
|
+
"urlopen",
|
|
143
|
+
lambda _url, timeout: FakeResponse(
|
|
144
|
+
b"untrusted",
|
|
145
|
+
final_url="http://mirror.invalid/archive.exe",
|
|
146
|
+
),
|
|
147
|
+
)
|
|
148
|
+
destination = tmp_path / "archive.exe"
|
|
149
|
+
|
|
150
|
+
with pytest.raises(RuntimeInstallError, match="redirected outside HTTPS"):
|
|
151
|
+
download_verified(
|
|
152
|
+
"https://example.invalid/archive.exe",
|
|
153
|
+
destination,
|
|
154
|
+
MSYS2_ARCHIVE_SHA256,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
assert not destination.exists()
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def test_download_falls_back_after_removing_a_partial_archive(tmp_path):
|
|
161
|
+
destination = tmp_path / "archive.exe"
|
|
162
|
+
sources = (
|
|
163
|
+
("first", "https://first.invalid/a"),
|
|
164
|
+
("second", "https://second.invalid/a"),
|
|
165
|
+
)
|
|
166
|
+
attempted = []
|
|
167
|
+
|
|
168
|
+
def downloader(url, target, sha256):
|
|
169
|
+
attempted.append((url, sha256))
|
|
170
|
+
if "first" in url:
|
|
171
|
+
target.write_bytes(b"partial")
|
|
172
|
+
raise RuntimeInstallError("could not download the pinned MSYS2 runtime")
|
|
173
|
+
assert not target.exists()
|
|
174
|
+
target.write_bytes(b"verified")
|
|
175
|
+
|
|
176
|
+
selected = download_from_sources(
|
|
177
|
+
destination,
|
|
178
|
+
MSYS2_ARCHIVE_SHA256,
|
|
179
|
+
sources=sources,
|
|
180
|
+
downloader=downloader,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
assert selected == "second"
|
|
184
|
+
assert [url for url, _sha256 in attempted] == [url for _label, url in sources]
|
|
185
|
+
assert {sha256 for _url, sha256 in attempted} == {MSYS2_ARCHIVE_SHA256}
|
|
186
|
+
assert destination.read_bytes() == b"verified"
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def test_download_fails_closed_after_every_approved_source(tmp_path):
|
|
190
|
+
destination = tmp_path / "archive.exe"
|
|
191
|
+
sources = (
|
|
192
|
+
("first", "https://first.invalid/a"),
|
|
193
|
+
("second", "https://second.invalid/a"),
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
def downloader(_url, target, _sha256):
|
|
197
|
+
target.write_bytes(b"untrusted")
|
|
198
|
+
raise RuntimeInstallError("archive failed verification")
|
|
199
|
+
|
|
200
|
+
with pytest.raises(RuntimeInstallError, match="any approved source"):
|
|
201
|
+
download_from_sources(
|
|
202
|
+
destination,
|
|
203
|
+
MSYS2_ARCHIVE_SHA256,
|
|
204
|
+
sources=sources,
|
|
205
|
+
downloader=downloader,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
assert not destination.exists()
|
|
209
|
+
|
|
210
|
+
|
|
44
211
|
def test_handoff_preserves_argv_and_uses_child_only_msys_environment(tmp_path):
|
|
45
212
|
runtime = make_runtime(tmp_path / "msys", managed=False)
|
|
46
213
|
arguments = ["ssh", "user@example", "--ssh-args=-J jump host", "开发"]
|
|
@@ -82,10 +249,10 @@ def test_managed_runtime_requires_utf8_marker_and_exact_package_version(tmp_path
|
|
|
82
249
|
root = managed_root(environ, version=VERSION)
|
|
83
250
|
assert root is not None
|
|
84
251
|
runtime = make_runtime(root, managed=True)
|
|
85
|
-
probe = MagicMock(return_value=completed([], stdout=b"railmux 0.4.0.
|
|
252
|
+
probe = MagicMock(return_value=completed([], stdout=b"railmux 0.4.0.dev5\n"))
|
|
86
253
|
|
|
87
254
|
assert probe_runtime(runtime, version=VERSION, environ=environ, probe=probe)
|
|
88
|
-
assert not probe_runtime(runtime, version="0.4.0.
|
|
255
|
+
assert not probe_runtime(runtime, version="0.4.0.dev6", environ=environ, probe=probe)
|
|
89
256
|
|
|
90
257
|
|
|
91
258
|
def test_runtime_probe_retries_one_transient_cold_start_failure(tmp_path):
|
|
@@ -93,7 +260,7 @@ def test_runtime_probe_retries_one_transient_cold_start_failure(tmp_path):
|
|
|
93
260
|
probe = MagicMock(
|
|
94
261
|
side_effect=[
|
|
95
262
|
completed([], returncode=1),
|
|
96
|
-
completed([], stdout=b"railmux 0.4.0.
|
|
263
|
+
completed([], stdout=b"railmux 0.4.0.dev5\n"),
|
|
97
264
|
]
|
|
98
265
|
)
|
|
99
266
|
|
|
@@ -114,7 +281,7 @@ def test_each_preview_version_uses_a_separate_runtime_generation(tmp_path):
|
|
|
114
281
|
def test_explicit_user_runtime_is_probed_but_never_requires_managed_marker(tmp_path):
|
|
115
282
|
root = tmp_path / "用户-owned-msys"
|
|
116
283
|
runtime = make_runtime(root, managed=False)
|
|
117
|
-
probe = MagicMock(return_value=completed([], stdout=b"railmux 0.4.0.
|
|
284
|
+
probe = MagicMock(return_value=completed([], stdout=b"railmux 0.4.0.dev5\n"))
|
|
118
285
|
environ = {"RAILMUX_MSYS2_ROOT": str(root), "USERPROFILE": r"C:\Users\u"}
|
|
119
286
|
|
|
120
287
|
found = find_runtime(version=VERSION, environ=environ, probe=probe)
|
|
@@ -186,7 +353,7 @@ def test_install_is_staged_and_activated_only_after_exact_probe(tmp_path):
|
|
|
186
353
|
return completed(argv)
|
|
187
354
|
|
|
188
355
|
def probe(argv, *, env, timeout):
|
|
189
|
-
return completed(argv, stdout=b"railmux 0.4.0.
|
|
356
|
+
return completed(argv, stdout=b"railmux 0.4.0.dev5\n")
|
|
190
357
|
|
|
191
358
|
@contextmanager
|
|
192
359
|
def unlocked(_base):
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.4.0.dev4"
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|