railmux 0.4.0.dev35__tar.gz → 0.4.0.dev36__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.dev35/src/railmux.egg-info → railmux-0.4.0.dev36}/PKG-INFO +1 -1
- railmux-0.4.0.dev36/src/railmux/__init__.py +1 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux/provider_paths.py +3 -1
- railmux-0.4.0.dev36/src/railmux/release_version.py +111 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux/windows_msys2.py +32 -17
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux/windows_ui_transition.py +4 -3
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36/src/railmux.egg-info}/PKG-INFO +1 -1
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux.egg-info/SOURCES.txt +2 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_release_notes.py +26 -1
- railmux-0.4.0.dev36/tests/test_release_version.py +128 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_windows_msys2.py +83 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_windows_ui_transition.py +1 -1
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tools/release_notes.py +5 -2
- railmux-0.4.0.dev35/src/railmux/__init__.py +0 -1
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/LICENSE +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/MANIFEST.in +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/README.md +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/pyproject.toml +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/setup.cfg +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux/__main__.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux/atomic_file.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux/background_index.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux/cli.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux/codex_index.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux/config.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux/config_cli.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux/diagnostics.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux/discovery.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux/display_transport.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux/entrypoint.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux/fast_display_client.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux/fast_display_history.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux/fast_display_input.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux/fast_display_protocol.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux/fast_display_server.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux/favorites.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux/fuzzy.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux/help_workspace.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux/launcher.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux/legacy_sessions.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux/local_clipboard.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux/local_open.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux/models.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux/modes.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux/mouse_manager.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux/orphan_marker.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux/pane_surface.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux/path_codec.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux/preview_pager.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux/project_favorites.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux/remote_config.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux/renames.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux/restart_state.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux/runtime_config.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux/scroll_agent.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux/scroll_manager.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux/selection_isolation.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux/self_update.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux/session_cache.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux/session_index.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux/session_lease.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux/setting_contracts.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux/settings.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux/ssh_args.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux/ssh_compat.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux/ssh_display_diagnostics.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux/ssh_doctor.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux/system_deps.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux/terminal_status.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux/tmux_binding_manager.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux/tmux_ctl.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux/tmux_health.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux/tmux_server.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux/tool_panes.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux/transcript.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux/ui/__init__.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux/ui/_widgets.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux/ui/app.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux/ui/keymap.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux/ui/modals.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux/ui/projects_pane.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux/ui/running_pane.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux/ui/sessions_pane.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux/ui/sidebar.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux/ui/statusbar.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux/ui/workspace.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux/windows_attach_relay.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux/windows_bootstrap.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux/windows_install_log.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux/windows_pacman.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux/windows_paths.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux/windows_tmux_lifecycle.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux.egg-info/dependency_links.txt +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux.egg-info/entry_points.txt +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux.egg-info/requires.txt +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/src/railmux.egg-info/top_level.txt +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_agent_workspace.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_atomic_file.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_background_index.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_cli.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_codex_delete.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_codex_index.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_codex_lineage.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_codex_transcript.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_config.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_config_cli.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_denested_transport_benchmark.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_diagnostics.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_discovery.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_display_transport.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_fast_full_window.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_focus_highlight.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_fullscreen_binding.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_fuzzy.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_help_agent.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_help_workspace.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_history_cleanup.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_keymap.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_launcher.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_layout_profile.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_legacy_sessions.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_local_clipboard.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_local_open.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_modals.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_mode_selection.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_modes.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_mouse_manager.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_new_project.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_options.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_orphan_recovery.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_pane_surface.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_path_codec.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_preview_pager.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_project_favorites.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_projects_pane.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_provider_paths.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_release_workflow.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_remote_config.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_renames.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_restart_state.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_running_pane.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_running_sort.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_runtime_config.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_scroll_agent.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_scroll_manager.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_selection_isolation.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_self_update.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_session_cache.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_session_index.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_session_lease.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_sessions_pane.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_setting_contracts.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_settings.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_sidebar.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_soft_quit.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_ssh_args.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_ssh_compat.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_ssh_display_benchmark.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_ssh_display_diagnostics.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_ssh_doctor.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_status.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_statusbar.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_system_deps.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_terminal_size.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_terminal_status.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_termux_local_touch.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_tmux_binding_manager.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_tmux_ctl.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_tmux_health.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_tmux_integration.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_tmux_server.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_tmux_status.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_tool_panes.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_transcript.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_utf8_state.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_web_demo_recorder.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_wheel_smoke.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_widgets.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_windows_attach_relay.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_windows_bootstrap.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_windows_install_log.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_windows_pacman.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_windows_paths.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_windows_tmux_lifecycle.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_windows_wrapper_contract.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tests/test_yolo_prompt.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tools/denested_transport_benchmark.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tools/wheel_smoke.py +0 -0
- {railmux-0.4.0.dev35 → railmux-0.4.0.dev36}/tools/windows_archive_smoke.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.0.dev36"
|
|
@@ -9,13 +9,15 @@ import sys
|
|
|
9
9
|
from collections.abc import Mapping
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
|
|
12
|
+
from railmux.release_version import PROJECT_VERSION_PATTERN
|
|
13
|
+
|
|
12
14
|
|
|
13
15
|
_DRIVE_PATH = re.compile(r"^(?:\\\\\?\\)?([A-Za-z]):[\\/](.*)\Z")
|
|
14
16
|
_MANAGED_BASE_MARKER = Path("/railmux-base.json")
|
|
15
17
|
_MANAGED_BASE_CONTENT_MARKER = Path("/railmux-base-content-v1.json")
|
|
16
18
|
_MANAGED_APP_ROOT = Path("/opt/railmux/apps")
|
|
17
19
|
_MANAGED_APP_ID = re.compile(
|
|
18
|
-
|
|
20
|
+
rf"railmux-{PROJECT_VERSION_PATTERN}\Z"
|
|
19
21
|
)
|
|
20
22
|
|
|
21
23
|
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Narrow version grammar shared by managed Windows app-layer identities."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import re
|
|
5
|
+
from functools import total_ordering
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class InvalidVersion(ValueError):
|
|
10
|
+
"""A version is outside Railmux's canonical persisted grammar."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@total_ordering
|
|
14
|
+
class ProjectVersion:
|
|
15
|
+
"""Small standard-library ordering type for native bootstrap identities."""
|
|
16
|
+
|
|
17
|
+
__slots__ = ("_key", "_value")
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
value: str,
|
|
22
|
+
release: tuple[int, ...],
|
|
23
|
+
stage: int,
|
|
24
|
+
stage_number: int,
|
|
25
|
+
) -> None:
|
|
26
|
+
normalized = release
|
|
27
|
+
while len(normalized) > 1 and normalized[-1] == 0:
|
|
28
|
+
normalized = normalized[:-1]
|
|
29
|
+
self._value = value
|
|
30
|
+
self._key = (normalized, stage, stage_number)
|
|
31
|
+
|
|
32
|
+
def __str__(self) -> str:
|
|
33
|
+
return self._value
|
|
34
|
+
|
|
35
|
+
def __repr__(self) -> str:
|
|
36
|
+
return f"ProjectVersion({self._value!r})"
|
|
37
|
+
|
|
38
|
+
def __hash__(self) -> int:
|
|
39
|
+
return hash(self._key)
|
|
40
|
+
|
|
41
|
+
def __eq__(self, other: Any) -> bool:
|
|
42
|
+
if not isinstance(other, ProjectVersion):
|
|
43
|
+
return NotImplemented
|
|
44
|
+
return self._key == other._key
|
|
45
|
+
|
|
46
|
+
def __lt__(self, other: Any) -> bool:
|
|
47
|
+
if not isinstance(other, ProjectVersion):
|
|
48
|
+
return NotImplemented
|
|
49
|
+
return self._key < other._key
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# Railmux app directories are security-sensitive persisted identities. Accept
|
|
53
|
+
# only the spellings that this project publishes: a final, an RC, or a
|
|
54
|
+
# development release. PEP 440 aliases, epochs, post releases, and local
|
|
55
|
+
# versions are deliberately excluded so equal versions have one path spelling.
|
|
56
|
+
_CANONICAL_INTEGER_PATTERN = r"(?:0|[1-9][0-9]*)"
|
|
57
|
+
PROJECT_VERSION_PATTERN = (
|
|
58
|
+
rf"{_CANONICAL_INTEGER_PATTERN}"
|
|
59
|
+
rf"(?:\.{_CANONICAL_INTEGER_PATTERN})*"
|
|
60
|
+
rf"(?:rc{_CANONICAL_INTEGER_PATTERN}|\.dev{_CANONICAL_INTEGER_PATTERN})?"
|
|
61
|
+
)
|
|
62
|
+
PROJECT_VERSION_RE = re.compile(PROJECT_VERSION_PATTERN + r"\Z")
|
|
63
|
+
_PROJECT_VERSION_PARTS_RE = re.compile(
|
|
64
|
+
rf"(?P<release>{_CANONICAL_INTEGER_PATTERN}"
|
|
65
|
+
rf"(?:\.{_CANONICAL_INTEGER_PATTERN})*)"
|
|
66
|
+
rf"(?:(?:rc(?P<rc>{_CANONICAL_INTEGER_PATTERN}))|"
|
|
67
|
+
rf"(?:\.dev(?P<dev>{_CANONICAL_INTEGER_PATTERN})))?\Z"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def parse_project_version(value: str) -> ProjectVersion:
|
|
72
|
+
"""Return the canonical supported Railmux version or raise InvalidVersion."""
|
|
73
|
+
match = _PROJECT_VERSION_PARTS_RE.fullmatch(value)
|
|
74
|
+
if match is None:
|
|
75
|
+
raise InvalidVersion(value)
|
|
76
|
+
release_parts = match.group("release").split(".")
|
|
77
|
+
stage_text = match.group("rc") or match.group("dev")
|
|
78
|
+
numeric_parts = release_parts + ([stage_text] if stage_text else [])
|
|
79
|
+
if any(
|
|
80
|
+
len(part) > 1 and part.startswith("0")
|
|
81
|
+
for part in numeric_parts
|
|
82
|
+
):
|
|
83
|
+
raise InvalidVersion(
|
|
84
|
+
f"non-canonical Railmux version spelling: {value!r}"
|
|
85
|
+
)
|
|
86
|
+
if match.group("dev") is not None:
|
|
87
|
+
stage = 0
|
|
88
|
+
stage_number = int(match.group("dev"))
|
|
89
|
+
elif match.group("rc") is not None:
|
|
90
|
+
stage = 1
|
|
91
|
+
stage_number = int(match.group("rc"))
|
|
92
|
+
else:
|
|
93
|
+
stage = 2
|
|
94
|
+
stage_number = 0
|
|
95
|
+
return ProjectVersion(
|
|
96
|
+
value,
|
|
97
|
+
tuple(int(part) for part in release_parts),
|
|
98
|
+
stage,
|
|
99
|
+
stage_number,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def is_project_version(value: object) -> bool:
|
|
104
|
+
"""Return whether *value* is one canonical published-version spelling."""
|
|
105
|
+
if not isinstance(value, str):
|
|
106
|
+
return False
|
|
107
|
+
try:
|
|
108
|
+
parse_project_version(value)
|
|
109
|
+
except InvalidVersion:
|
|
110
|
+
return False
|
|
111
|
+
return True
|
|
@@ -47,6 +47,13 @@ from railmux.windows_paths import (
|
|
|
47
47
|
legacy_local_app_data_root,
|
|
48
48
|
managed_windows_data_root,
|
|
49
49
|
)
|
|
50
|
+
from railmux.release_version import (
|
|
51
|
+
PROJECT_VERSION_PATTERN,
|
|
52
|
+
PROJECT_VERSION_RE,
|
|
53
|
+
ProjectVersion,
|
|
54
|
+
is_project_version,
|
|
55
|
+
parse_project_version,
|
|
56
|
+
)
|
|
50
57
|
from railmux.terminal_status import STYLE_ACCENT, STYLE_MUTED, styled
|
|
51
58
|
|
|
52
59
|
|
|
@@ -108,7 +115,7 @@ _DOWNLOAD_PROBE_TIMEOUT = 10.0
|
|
|
108
115
|
_DOWNLOAD_SLOW_REMAINING_SECONDS = 60.0
|
|
109
116
|
_DOWNLOAD_SWITCH_RATIO = 1.25
|
|
110
117
|
_CONTENT_RANGE_RE = re.compile(r"bytes (\d+)-(\d+)/(\d+)\Z")
|
|
111
|
-
_VERSION_RE =
|
|
118
|
+
_VERSION_RE = PROJECT_VERSION_RE
|
|
112
119
|
|
|
113
120
|
_PACMAN_CONFIG = "/etc/railmux-pacman.conf"
|
|
114
121
|
_PACMAN_PACKAGES = "tmux python python-pip"
|
|
@@ -1077,10 +1084,8 @@ def _app_marker_matches(root: Path, *, app_name: str, version: str) -> bool:
|
|
|
1077
1084
|
)
|
|
1078
1085
|
|
|
1079
1086
|
|
|
1080
|
-
def _version_key(version: str) ->
|
|
1081
|
-
|
|
1082
|
-
numbers = tuple(int(part) for part in base.split("."))
|
|
1083
|
-
return numbers, int(dev) if separator else sys.maxsize
|
|
1087
|
+
def _version_key(version: str) -> ProjectVersion:
|
|
1088
|
+
return parse_project_version(version)
|
|
1084
1089
|
|
|
1085
1090
|
|
|
1086
1091
|
def _marked_app_versions(root: Path) -> dict[str, Path] | None:
|
|
@@ -1113,7 +1118,7 @@ def _marked_app_versions(root: Path) -> dict[str, Path] | None:
|
|
|
1113
1118
|
continue
|
|
1114
1119
|
version = candidate.name[len(prefix):]
|
|
1115
1120
|
if (
|
|
1116
|
-
|
|
1121
|
+
is_project_version(version)
|
|
1117
1122
|
and _app_marker_matches(
|
|
1118
1123
|
root, app_name=candidate.name, version=version)
|
|
1119
1124
|
):
|
|
@@ -1144,8 +1149,8 @@ def _in_use_app_names(
|
|
|
1144
1149
|
except UnicodeDecodeError:
|
|
1145
1150
|
return None
|
|
1146
1151
|
pattern = re.compile(
|
|
1147
|
-
|
|
1148
|
-
|
|
1152
|
+
rf"(?:^|/)opt/railmux/apps/"
|
|
1153
|
+
rf"(railmux-{PROJECT_VERSION_PATTERN})(?:/|$)"
|
|
1149
1154
|
)
|
|
1150
1155
|
return frozenset(match.group(1) for match in pattern.finditer(text))
|
|
1151
1156
|
|
|
@@ -1158,7 +1163,7 @@ def plan_managed_runtime_prune(
|
|
|
1158
1163
|
probe: Probe = _probe,
|
|
1159
1164
|
) -> RuntimePrunePlan:
|
|
1160
1165
|
"""Build a fail-closed plan without deleting any file."""
|
|
1161
|
-
if not
|
|
1166
|
+
if not is_project_version(version):
|
|
1162
1167
|
raise RuntimeInstallError("the Railmux package version is invalid")
|
|
1163
1168
|
if environ.get(_RUNTIME_OVERRIDE, "").strip():
|
|
1164
1169
|
raise RuntimeInstallError(
|
|
@@ -1277,7 +1282,7 @@ def _find_shared_root(base: Path) -> Path | None:
|
|
|
1277
1282
|
if _base_marker_matches(canonical):
|
|
1278
1283
|
return canonical
|
|
1279
1284
|
legacy_parent = base / MSYS2_RUNTIME_ID
|
|
1280
|
-
candidates: list[tuple[
|
|
1285
|
+
candidates: list[tuple[ProjectVersion, Path]] = []
|
|
1281
1286
|
try:
|
|
1282
1287
|
paths = list(legacy_parent.glob("railmux-*"))
|
|
1283
1288
|
except OSError:
|
|
@@ -1288,7 +1293,7 @@ def _find_shared_root(base: Path) -> Path | None:
|
|
|
1288
1293
|
if (
|
|
1289
1294
|
not candidate.is_symlink()
|
|
1290
1295
|
and _base_marker_matches(candidate)
|
|
1291
|
-
and
|
|
1296
|
+
and is_project_version(version)
|
|
1292
1297
|
):
|
|
1293
1298
|
candidates.append((_version_key(version), candidate))
|
|
1294
1299
|
if candidates:
|
|
@@ -2332,7 +2337,7 @@ def _write_app_marker(app_root: Path, *, version: str) -> None:
|
|
|
2332
2337
|
|
|
2333
2338
|
def _legacy_candidates(base: Path) -> list[tuple[Path, str]]:
|
|
2334
2339
|
parent = base / MSYS2_RUNTIME_ID
|
|
2335
|
-
candidates: list[tuple[
|
|
2340
|
+
candidates: list[tuple[ProjectVersion, Path, str]] = []
|
|
2336
2341
|
try:
|
|
2337
2342
|
paths = list(parent.glob("railmux-*"))
|
|
2338
2343
|
except OSError:
|
|
@@ -2346,7 +2351,7 @@ def _legacy_candidates(base: Path) -> list[tuple[Path, str]]:
|
|
|
2346
2351
|
version = payload.get("railmux")
|
|
2347
2352
|
if (
|
|
2348
2353
|
not isinstance(version, str)
|
|
2349
|
-
or not
|
|
2354
|
+
or not is_project_version(version)
|
|
2350
2355
|
or payload
|
|
2351
2356
|
!= {
|
|
2352
2357
|
"schema": RUNTIME_SCHEMA,
|
|
@@ -2492,11 +2497,21 @@ def _venv_site_packages(application: Path) -> Path | None:
|
|
|
2492
2497
|
def _native_application_payload(version: str) -> tuple[Path, Path] | None:
|
|
2493
2498
|
"""Locate the currently executing pure-Python package and dist-info."""
|
|
2494
2499
|
try:
|
|
2495
|
-
|
|
2496
|
-
|
|
2500
|
+
matches = [
|
|
2501
|
+
distribution
|
|
2502
|
+
for distribution in importlib_metadata.distributions(name="railmux")
|
|
2503
|
+
if distribution.version == version
|
|
2504
|
+
]
|
|
2505
|
+
except (OSError, ValueError):
|
|
2497
2506
|
return None
|
|
2498
|
-
|
|
2507
|
+
# A long-lived Windows test/user environment can retain an old dist-info
|
|
2508
|
+
# directory even though pip installed the current package code. Selecting
|
|
2509
|
+
# ``distribution("railmux")`` would then be order-dependent and could
|
|
2510
|
+
# force an unnecessary network fallback. Never delete metadata here; use
|
|
2511
|
+
# the unique distribution whose version exactly matches the imported app.
|
|
2512
|
+
if len(matches) != 1:
|
|
2499
2513
|
return None
|
|
2514
|
+
distribution = matches[0]
|
|
2500
2515
|
package = Path(__file__).resolve().parent
|
|
2501
2516
|
if not package.is_dir():
|
|
2502
2517
|
return None
|
|
@@ -2965,7 +2980,7 @@ def install_managed_runtime(
|
|
|
2965
2980
|
reuse_only: bool = False,
|
|
2966
2981
|
) -> Msys2Runtime:
|
|
2967
2982
|
"""Install a fresh private runtime and activate it only after verification."""
|
|
2968
|
-
if not
|
|
2983
|
+
if not is_project_version(version):
|
|
2969
2984
|
raise RuntimeInstallError("the Railmux package version is invalid")
|
|
2970
2985
|
if environ.get(_RUNTIME_OVERRIDE, "").strip():
|
|
2971
2986
|
raise RuntimeInstallError(
|
|
@@ -23,6 +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.release_version import PROJECT_VERSION_PATTERN
|
|
26
27
|
from railmux.windows_msys2 import MSYS2_BASE_LINEAGE_SHA256
|
|
27
28
|
|
|
28
29
|
|
|
@@ -31,7 +32,7 @@ REQUESTED_APP_OPTION = "@railmux_requested_app_v1"
|
|
|
31
32
|
TRANSITION_STATUS_OPTION = "@railmux_app_transition_status_v1"
|
|
32
33
|
UPGRADE_WAKE_SEQUENCE = "\x1b[33~"
|
|
33
34
|
_APP_RE = re.compile(
|
|
34
|
-
|
|
35
|
+
rf"railmux-({PROJECT_VERSION_PATTERN})\Z"
|
|
35
36
|
)
|
|
36
37
|
_CONTENT_RE = re.compile(r"[0-9a-f]{64}\Z")
|
|
37
38
|
_PANE_RE = re.compile(r"%[0-9]+\Z")
|
|
@@ -557,7 +558,7 @@ def _pane_app_version(
|
|
|
557
558
|
pass
|
|
558
559
|
pattern = re.compile(
|
|
559
560
|
r"/opt/railmux/apps/railmux-"
|
|
560
|
-
|
|
561
|
+
rf"({PROJECT_VERSION_PATTERN})/venv/bin/railmux"
|
|
561
562
|
)
|
|
562
563
|
for candidate in candidates:
|
|
563
564
|
match = pattern.search(candidate)
|
|
@@ -663,7 +664,7 @@ def ensure_current_ui(
|
|
|
663
664
|
runtime: str,
|
|
664
665
|
target_app: str,
|
|
665
666
|
target_version: str,
|
|
666
|
-
timeout: float =
|
|
667
|
+
timeout: float = 15.0,
|
|
667
668
|
) -> TransitionOutcome:
|
|
668
669
|
"""Converge a detached managed outer UI without touching providers."""
|
|
669
670
|
content_id = _base_identity(runtime)
|
|
@@ -35,6 +35,7 @@ src/railmux/path_codec.py
|
|
|
35
35
|
src/railmux/preview_pager.py
|
|
36
36
|
src/railmux/project_favorites.py
|
|
37
37
|
src/railmux/provider_paths.py
|
|
38
|
+
src/railmux/release_version.py
|
|
38
39
|
src/railmux/remote_config.py
|
|
39
40
|
src/railmux/renames.py
|
|
40
41
|
src/railmux/restart_state.py
|
|
@@ -126,6 +127,7 @@ tests/test_project_favorites.py
|
|
|
126
127
|
tests/test_projects_pane.py
|
|
127
128
|
tests/test_provider_paths.py
|
|
128
129
|
tests/test_release_notes.py
|
|
130
|
+
tests/test_release_version.py
|
|
129
131
|
tests/test_release_workflow.py
|
|
130
132
|
tests/test_remote_config.py
|
|
131
133
|
tests/test_renames.py
|
|
@@ -79,6 +79,21 @@ def test_renders_development_release_section():
|
|
|
79
79
|
)
|
|
80
80
|
|
|
81
81
|
|
|
82
|
+
def test_renders_release_candidate_section():
|
|
83
|
+
changelog = (
|
|
84
|
+
"## [0.4.0rc1] - 2026-08-07\n\n"
|
|
85
|
+
"- Release candidate.\n\n"
|
|
86
|
+
"[0.4.0rc1]: "
|
|
87
|
+
"https://example.test/compare/v0.4.0.dev36...v0.4.0rc1\n"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
assert render_release_notes(changelog, "v0.4.0rc1") == (
|
|
91
|
+
"- Release candidate.\n\n"
|
|
92
|
+
"**Full Changelog**: "
|
|
93
|
+
"https://example.test/compare/v0.4.0.dev36...v0.4.0rc1\n"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
82
97
|
def test_last_section_does_not_include_changelog_link_definitions():
|
|
83
98
|
notes = render_release_notes(CHANGELOG, "0.2.30")
|
|
84
99
|
|
|
@@ -89,7 +104,17 @@ def test_last_section_does_not_include_changelog_link_definitions():
|
|
|
89
104
|
|
|
90
105
|
@pytest.mark.parametrize(
|
|
91
106
|
"version",
|
|
92
|
-
[
|
|
107
|
+
[
|
|
108
|
+
"0.2.4",
|
|
109
|
+
"Unreleased",
|
|
110
|
+
"v0.2",
|
|
111
|
+
"0.2.3-dev1",
|
|
112
|
+
"0.2.3.dev",
|
|
113
|
+
"00.2.3",
|
|
114
|
+
"0.02.3",
|
|
115
|
+
"0.2.03",
|
|
116
|
+
"0.2.3rc01",
|
|
117
|
+
],
|
|
93
118
|
)
|
|
94
119
|
def test_rejects_missing_or_invalid_release(version):
|
|
95
120
|
with pytest.raises(ValueError):
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import sys
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from railmux import __version__, provider_paths, windows_msys2
|
|
8
|
+
from railmux import windows_ui_transition
|
|
9
|
+
from railmux.release_version import (
|
|
10
|
+
InvalidVersion,
|
|
11
|
+
is_project_version,
|
|
12
|
+
parse_project_version,
|
|
13
|
+
)
|
|
14
|
+
from tools.release_notes import normalize_version
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
ROOT = Path(__file__).parents[1]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@pytest.mark.parametrize(
|
|
21
|
+
"version",
|
|
22
|
+
["0.4.0.dev35", "0.4.0.dev36", "0.4.0rc1", "0.4.0", "1.2.3"],
|
|
23
|
+
)
|
|
24
|
+
def test_every_persisted_version_consumer_accepts_release_spellings(version):
|
|
25
|
+
app = f"railmux-{version}"
|
|
26
|
+
|
|
27
|
+
assert is_project_version(version)
|
|
28
|
+
assert windows_msys2._VERSION_RE.fullmatch(version)
|
|
29
|
+
assert windows_ui_transition._APP_RE.fullmatch(app)
|
|
30
|
+
assert provider_paths._MANAGED_APP_ID.fullmatch(app)
|
|
31
|
+
assert normalize_version(f"v{version}") == version
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@pytest.mark.parametrize(
|
|
35
|
+
"version",
|
|
36
|
+
[
|
|
37
|
+
"0.4.0-rc1",
|
|
38
|
+
"0.4.0RC1",
|
|
39
|
+
"0.4.0rc01",
|
|
40
|
+
"0.4.0.dev01",
|
|
41
|
+
"00.4.0",
|
|
42
|
+
"0.04.0",
|
|
43
|
+
"0.4.00",
|
|
44
|
+
"0.4.0a1",
|
|
45
|
+
"0.4.0b1",
|
|
46
|
+
"0.4.0.post1",
|
|
47
|
+
"0.4.0+local",
|
|
48
|
+
"1!0.4.0",
|
|
49
|
+
],
|
|
50
|
+
)
|
|
51
|
+
def test_persisted_version_grammar_rejects_aliases_and_unsupported_forms(version):
|
|
52
|
+
with pytest.raises(InvalidVersion):
|
|
53
|
+
parse_project_version(version)
|
|
54
|
+
app = f"railmux-{version}"
|
|
55
|
+
assert windows_msys2._VERSION_RE.fullmatch(version) is None
|
|
56
|
+
assert windows_ui_transition._APP_RE.fullmatch(app) is None
|
|
57
|
+
assert provider_paths._MANAGED_APP_ID.fullmatch(app) is None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_release_order_matches_dev_to_rc_to_final():
|
|
61
|
+
versions = ["0.4.0", "0.4.0.dev35", "0.4.0rc1", "0.4.0.dev36"]
|
|
62
|
+
|
|
63
|
+
assert sorted(versions, key=windows_msys2._version_key) == [
|
|
64
|
+
"0.4.0.dev35",
|
|
65
|
+
"0.4.0.dev36",
|
|
66
|
+
"0.4.0rc1",
|
|
67
|
+
"0.4.0",
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_release_order_crosses_the_next_development_base():
|
|
72
|
+
versions = ["0.4.1.dev1", "0.4.0", "0.4.0rc2", "0.4.0.dev99"]
|
|
73
|
+
|
|
74
|
+
assert sorted(versions, key=parse_project_version) == [
|
|
75
|
+
"0.4.0.dev99",
|
|
76
|
+
"0.4.0rc2",
|
|
77
|
+
"0.4.0",
|
|
78
|
+
"0.4.1.dev1",
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_package_version_uses_its_canonical_persisted_spelling():
|
|
83
|
+
assert str(parse_project_version(__version__)) == __version__
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_native_bootstrap_version_command_does_not_import_packaging():
|
|
87
|
+
code = f"""
|
|
88
|
+
import importlib.abc
|
|
89
|
+
import sys
|
|
90
|
+
sys.path.insert(0, {str(ROOT / 'src')!r})
|
|
91
|
+
class BlockPackaging(importlib.abc.MetaPathFinder):
|
|
92
|
+
def find_spec(self, fullname, path=None, target=None):
|
|
93
|
+
if fullname == 'packaging' or fullname.startswith('packaging.'):
|
|
94
|
+
raise ModuleNotFoundError(fullname)
|
|
95
|
+
return None
|
|
96
|
+
sys.meta_path.insert(0, BlockPackaging())
|
|
97
|
+
from railmux.windows_bootstrap import main
|
|
98
|
+
raise SystemExit(main(['--version'], version_info=(3, 10)))
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
result = subprocess.run(
|
|
102
|
+
[sys.executable, "-c", code],
|
|
103
|
+
capture_output=True,
|
|
104
|
+
text=True,
|
|
105
|
+
check=False,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
assert result.returncode == 0, result.stderr
|
|
109
|
+
assert result.stdout.startswith("railmux 0.4.0")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_in_use_inventory_retains_an_rc_app_layer(tmp_path):
|
|
113
|
+
output = (
|
|
114
|
+
b"/opt/railmux/apps/railmux-0.4.0rc1/venv/bin/railmux\0"
|
|
115
|
+
b"/opt/railmux/apps/railmux-0.4.0.dev36/venv/bin/railmux\0"
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
result = windows_msys2._in_use_app_names(
|
|
119
|
+
tmp_path,
|
|
120
|
+
environ={},
|
|
121
|
+
probe=lambda argv, **kwargs: subprocess.CompletedProcess(
|
|
122
|
+
argv, 0, stdout=output, stderr=b""
|
|
123
|
+
),
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
assert result == frozenset(
|
|
127
|
+
{"railmux-0.4.0rc1", "railmux-0.4.0.dev36"}
|
|
128
|
+
)
|
|
@@ -2200,6 +2200,49 @@ def test_runtime_status_distinguishes_reusable_base_from_current_app(tmp_path):
|
|
|
2200
2200
|
assert snapshot["current_app"] is False
|
|
2201
2201
|
|
|
2202
2202
|
|
|
2203
|
+
def test_native_payload_selects_exact_version_among_stale_dist_info(
|
|
2204
|
+
tmp_path, monkeypatch,
|
|
2205
|
+
):
|
|
2206
|
+
current_info = tmp_path / "railmux-0.4.0rc1.dist-info"
|
|
2207
|
+
stale_info = tmp_path / "railmux-0.4.0.dev33.dist-info"
|
|
2208
|
+
current_info.mkdir()
|
|
2209
|
+
stale_info.mkdir()
|
|
2210
|
+
|
|
2211
|
+
def distribution(version, info):
|
|
2212
|
+
return SimpleNamespace(
|
|
2213
|
+
version=version,
|
|
2214
|
+
files=(Path(info.name) / "METADATA",),
|
|
2215
|
+
locate_file=lambda relative: tmp_path / relative,
|
|
2216
|
+
)
|
|
2217
|
+
|
|
2218
|
+
monkeypatch.setattr(
|
|
2219
|
+
windows_msys2.importlib_metadata,
|
|
2220
|
+
"distributions",
|
|
2221
|
+
lambda **_kwargs: iter((
|
|
2222
|
+
distribution("0.4.0.dev33", stale_info),
|
|
2223
|
+
distribution("0.4.0rc1", current_info),
|
|
2224
|
+
)),
|
|
2225
|
+
)
|
|
2226
|
+
|
|
2227
|
+
payload = windows_msys2._native_application_payload("0.4.0rc1")
|
|
2228
|
+
|
|
2229
|
+
assert payload is not None
|
|
2230
|
+
assert payload[1] == current_info
|
|
2231
|
+
|
|
2232
|
+
|
|
2233
|
+
def test_native_payload_rejects_duplicate_exact_metadata(tmp_path, monkeypatch):
|
|
2234
|
+
info = tmp_path / "railmux-0.4.0rc1.dist-info"
|
|
2235
|
+
info.mkdir()
|
|
2236
|
+
distribution = SimpleNamespace(version="0.4.0rc1")
|
|
2237
|
+
monkeypatch.setattr(
|
|
2238
|
+
windows_msys2.importlib_metadata,
|
|
2239
|
+
"distributions",
|
|
2240
|
+
lambda **_kwargs: iter((distribution, distribution)),
|
|
2241
|
+
)
|
|
2242
|
+
|
|
2243
|
+
assert windows_msys2._native_application_payload("0.4.0rc1") is None
|
|
2244
|
+
|
|
2245
|
+
|
|
2203
2246
|
def test_prune_retains_current_previous_and_process_proven_layers(tmp_path):
|
|
2204
2247
|
environ = {"LOCALAPPDATA": str(tmp_path)}
|
|
2205
2248
|
root = managed_root(environ)
|
|
@@ -2222,6 +2265,29 @@ def test_prune_retains_current_previous_and_process_proven_layers(tmp_path):
|
|
|
2222
2265
|
assert f"railmux-{VERSION}" in plan.retained_apps
|
|
2223
2266
|
|
|
2224
2267
|
|
|
2268
|
+
def test_prune_retains_running_rc_beyond_current_and_previous_layers(tmp_path):
|
|
2269
|
+
environ = {"LOCALAPPDATA": str(tmp_path)}
|
|
2270
|
+
root = managed_root(environ)
|
|
2271
|
+
assert root is not None
|
|
2272
|
+
make_runtime(root, managed=True, shared=True)
|
|
2273
|
+
old_dev = add_marked_app(root, "0.4.0.dev36")
|
|
2274
|
+
running_rc = add_marked_app(root, "0.4.0rc1")
|
|
2275
|
+
final = add_marked_app(root, "0.4.0")
|
|
2276
|
+
current = add_marked_app(root, "0.4.1.dev1")
|
|
2277
|
+
in_use = b"/opt/railmux/apps/railmux-0.4.0rc1/venv/bin/railmux\0"
|
|
2278
|
+
|
|
2279
|
+
plan = windows_msys2.plan_managed_runtime_prune(
|
|
2280
|
+
version="0.4.1.dev1",
|
|
2281
|
+
environ=environ,
|
|
2282
|
+
probe=lambda argv, **_kwargs: completed(argv, stdout=in_use),
|
|
2283
|
+
)
|
|
2284
|
+
|
|
2285
|
+
assert plan.remove_apps == (old_dev,)
|
|
2286
|
+
assert running_rc.name in plan.retained_apps
|
|
2287
|
+
assert final.name in plan.retained_apps
|
|
2288
|
+
assert current.name in plan.retained_apps
|
|
2289
|
+
|
|
2290
|
+
|
|
2225
2291
|
def test_prune_ignores_unmarked_directories_and_fails_closed_on_process_probe(
|
|
2226
2292
|
tmp_path,
|
|
2227
2293
|
):
|
|
@@ -2243,6 +2309,23 @@ def test_prune_ignores_unmarked_directories_and_fails_closed_on_process_probe(
|
|
|
2243
2309
|
assert unmarked.is_dir()
|
|
2244
2310
|
|
|
2245
2311
|
|
|
2312
|
+
def test_prune_ignores_marked_noncanonical_version_directory(tmp_path):
|
|
2313
|
+
environ = {"LOCALAPPDATA": str(tmp_path)}
|
|
2314
|
+
root = managed_root(environ)
|
|
2315
|
+
assert root is not None
|
|
2316
|
+
make_runtime(root, managed=True, shared=True)
|
|
2317
|
+
noncanonical = add_marked_app(root, "0.04.0")
|
|
2318
|
+
|
|
2319
|
+
plan = windows_msys2.plan_managed_runtime_prune(
|
|
2320
|
+
version=VERSION,
|
|
2321
|
+
environ=environ,
|
|
2322
|
+
probe=lambda argv, **_kwargs: completed(argv, stdout=b""),
|
|
2323
|
+
)
|
|
2324
|
+
|
|
2325
|
+
assert noncanonical not in plan.remove_apps
|
|
2326
|
+
assert noncanonical.is_dir()
|
|
2327
|
+
|
|
2328
|
+
|
|
2246
2329
|
def test_prune_refuses_linked_application_parent(tmp_path):
|
|
2247
2330
|
environ = {"LOCALAPPDATA": str(tmp_path)}
|
|
2248
2331
|
root = managed_root(environ)
|
|
@@ -9,7 +9,10 @@ import sys
|
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
_VERSION_RE = re.compile(
|
|
12
|
+
_VERSION_RE = re.compile(
|
|
13
|
+
r"(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)"
|
|
14
|
+
r"(?:rc(?:0|[1-9]\d*)|\.dev(?:0|[1-9]\d*))?"
|
|
15
|
+
)
|
|
13
16
|
_SECTION_RE = re.compile(r"^## \[([^]]+)](?:\s+-\s+.+)?\s*$")
|
|
14
17
|
_LINK_RE = re.compile(r"^\[([^]]+)]\s*:\s*(\S+)\s*$")
|
|
15
18
|
|
|
@@ -66,7 +69,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
66
69
|
"version",
|
|
67
70
|
help=(
|
|
68
71
|
"release version or tag, for example 0.2.3, v0.2.3, or "
|
|
69
|
-
"v0.2.4.dev1"
|
|
72
|
+
"v0.2.4.dev1, or v0.4.0rc1"
|
|
70
73
|
),
|
|
71
74
|
)
|
|
72
75
|
parser.add_argument(
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.4.0.dev35"
|
|
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
|