diff --git a/.github/workflows/build_windows_macos.yml b/.github/workflows/build_windows_macos.yml index 412e4a5da..54f4eb8b8 100644 --- a/.github/workflows/build_windows_macos.yml +++ b/.github/workflows/build_windows_macos.yml @@ -60,11 +60,11 @@ jobs: uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: installer.exe - key: innosetup-6.7.1-exe + key: innosetup-6.7.4-exe - name: Download Inno Setup installer if: steps.cache-innosetup.outputs.cache-hit != 'true' - run: curl -L -o installer.exe https://github.com/jrsoftware/issrc/releases/download/is-6_7_1/innosetup-6.7.1.exe + run: curl -L -o installer.exe https://github.com/jrsoftware/issrc/releases/download/is-6_7_4/innosetup-6.7.4.exe - name: Install Inno Setup run: | diff --git a/INSTALL.md b/INSTALL.md index 9ff0055ad..957eabb35 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -184,12 +184,20 @@ For PowerShell (MS Windows) autocompletion, run this command in PowerShell: notepad $PROFILE ``` -And add this line to the file: +For the default per-user installation, add this line to the file: ```powershell -Import-Module "C:\Program Files (x86)\ardupilot_methodic_configurator\ardupilot_methodic_configurator_command_line_completion.psm1" +Import-Module "$env:LOCALAPPDATA\Programs\ardupilot_methodic_configurator\ardupilot_methodic_configurator_command_line_completion.psm1" ``` +For an all-users installation on 64-bit Windows, use: + +```powershell +Import-Module "${env:ProgramFiles(x86)}\ardupilot_methodic_configurator\ardupilot_methodic_configurator_command_line_completion.psm1" +``` + +If you selected a different installation directory, use the module path in that directory instead. + ## Verify Installer Security and Integrity **Why verify?** The ArduPilot Methodic Configurator installer is cryptographically signed and includes SLSA Level 3 provenance attestations. diff --git a/ardupilot_methodic_configurator/backend_filesystem_program_settings.py b/ardupilot_methodic_configurator/backend_filesystem_program_settings.py index 603fe1dfc..d876f2911 100644 --- a/ardupilot_methodic_configurator/backend_filesystem_program_settings.py +++ b/ardupilot_methodic_configurator/backend_filesystem_program_settings.py @@ -505,12 +505,31 @@ def get_templates_base_dir() -> str: package_path = importlib_files("ardupilot_methodic_configurator") logging_debug("current script directory1: %s", package_path) if platform_system() == "Windows": - package_path = Path(ProgramSettings._site_config_dir()) + package_path = Path(ProgramSettings._user_config_dir()) + # Uninstall can leave personal presets behind; require an installer-owned file. + if not (package_path / "vehicle_templates" / "system_vehicle_components_template.json").is_file(): + package_path = Path(ProgramSettings._site_config_dir()) logging_debug("current script directory2: %s", package_path) logging_debug(_("site_directory: %s"), package_path) return str(package_path / "vehicle_templates") + @staticmethod + def get_other_windows_templates_base_dir(templates_dir: str) -> str | None: + """Find the other installation scope without creating its configuration directory.""" + if platform_system() != "Windows" or os.environ.get("AMC_COMPONENT_TEMPLATES_DIR_TEST"): + return None + user_dir = ( + Path(user_config_dir(".ardupilot_methodic_configurator", appauthor=False, roaming=True)) / "vehicle_templates" + ) + site_dir = Path(site_config_dir(".ardupilot_methodic_configurator", appauthor=False)) / "vehicle_templates" + selected = os_path.normcase(os_path.abspath(templates_dir)) + if selected == os_path.normcase(os_path.abspath(user_dir)): + return str(site_dir) + if selected == os_path.normcase(os_path.abspath(site_dir)): + return str(user_dir) + return None + @staticmethod def get_vehicles_default_dir() -> Path: settings_directory = Path(ProgramSettings._user_config_dir()) diff --git a/ardupilot_methodic_configurator/backend_filesystem_vehicle_components.py b/ardupilot_methodic_configurator/backend_filesystem_vehicle_components.py index c92e68095..ad52f5eb5 100644 --- a/ardupilot_methodic_configurator/backend_filesystem_vehicle_components.py +++ b/ardupilot_methodic_configurator/backend_filesystem_vehicle_components.py @@ -123,6 +123,12 @@ def _load_user_templates(self) -> dict[str, list[dict]]: templates_filename = "user_vehicle_components_template.json" templates_dir = ProgramSettings.get_templates_base_dir() filepath = os_path.join(templates_dir, templates_filename) + if not os_path.exists(filepath): + other_templates_dir = ProgramSettings.get_other_windows_templates_base_dir(templates_dir) + if other_templates_dir: + other_filepath = os_path.join(other_templates_dir, templates_filename) + if os_path.isfile(other_filepath): + filepath = other_filepath templates = {} try: diff --git a/tests/test_backend_filesystem_program_settings.py b/tests/test_backend_filesystem_program_settings.py index 53e959b11..58d80a52f 100755 --- a/tests/test_backend_filesystem_program_settings.py +++ b/tests/test_backend_filesystem_program_settings.py @@ -13,11 +13,13 @@ import json import platform from os import path as os_path +from pathlib import Path from unittest.mock import MagicMock, mock_open, patch import pytest from ardupilot_methodic_configurator.backend_filesystem_program_settings import ProgramSettings, normalize_path +from ardupilot_methodic_configurator.backend_filesystem_vehicle_components import VehicleComponents # pylint: disable=too-many-lines,protected-access,redefined-outer-name,too-few-public-methods @@ -1321,29 +1323,199 @@ def test_get_templates_base_dir_uses_script_dir_on_macos(self) -> None: assert result == "/mock/package/path/vehicle_templates" mock_importlib.assert_called_once_with("ardupilot_methodic_configurator") - def test_get_templates_base_dir_uses_site_config_on_windows(self) -> None: + @pytest.mark.parametrize("user_templates_state", ["absent", "empty", "user-only", "marker-directory", "installed"]) + def test_get_templates_base_dir_selects_installed_templates_on_windows( + self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch, user_templates_state: str + ) -> None: """ - Templates base directory uses site config directory on Windows. + Select an installed template set without relying on the machine's configuration. - GIVEN: A Windows platform environment + GIVEN: An all-users template installation and possibly a per-user template directory WHEN: The templates base directory is requested - THEN: The site configuration directory should be used as the base - """ - # Arrange: Mock Windows platform and site config + THEN: Per-user templates take precedence only when their installer-owned file exists + """ + monkeypatch.delenv("AMC_COMPONENT_TEMPLATES_DIR_TEST", raising=False) + user_config = tmp_path / "user" + site_config = tmp_path / "site" + user_templates = user_config / "vehicle_templates" + site_templates = site_config / "vehicle_templates" + site_templates.mkdir(parents=True) + (site_templates / "system_vehicle_components_template.json").write_text("{}", encoding="utf-8") + if user_templates_state != "absent": + user_templates.mkdir(parents=True) + if user_templates_state == "user-only": + (user_templates / "user_vehicle_components_template.json").write_text("{}", encoding="utf-8") + elif user_templates_state == "marker-directory": + (user_templates / "system_vehicle_components_template.json").mkdir() + elif user_templates_state == "installed": + (user_templates / "system_vehicle_components_template.json").write_text("{}", encoding="utf-8") + with ( - patch("ardupilot_methodic_configurator.backend_filesystem_program_settings.platform_system") as mock_platform, patch( - "ardupilot_methodic_configurator.backend_filesystem_program_settings.ProgramSettings._site_config_dir" - ) as mock_site_config, + "ardupilot_methodic_configurator.backend_filesystem_program_settings.platform_system", return_value="Windows" + ), + patch.object(ProgramSettings, "_user_config_dir", return_value=str(user_config)) as mock_user_config, + patch.object(ProgramSettings, "_site_config_dir", return_value=str(site_config)) as mock_site_config, ): - mock_platform.return_value = "Windows" - mock_site_config.return_value = "C:\\Program Files\\App" - - # Act: Get templates base directory result = ProgramSettings.get_templates_base_dir() - # Assert: Windows path logic is used correctly - mock_site_config.assert_called_once() - # Use os.path.join to handle platform-specific path separators - expected_path = os_path.join("C:\\Program Files\\App", "vehicle_templates") - assert result == expected_path + mock_user_config.assert_called_once_with() + if user_templates_state == "installed": + assert result == str(user_templates) + mock_site_config.assert_not_called() + else: + assert result == str(site_templates) + mock_site_config.assert_called_once_with() + + def test_scope_switch_finds_shared_templates_after_per_user_uninstall( + self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch + ) -> None: + """ + Find all-users templates when uninstall leaves personal presets behind. + + GIVEN: Per-user templates, personal presets, and an all-users installation + WHEN: Uninstall removes the per-user installer's template file + THEN: Lookup switches to the shared installation and leaves personal presets intact + """ + monkeypatch.delenv("AMC_COMPONENT_TEMPLATES_DIR_TEST", raising=False) + user_config = tmp_path / "user" + site_config = tmp_path / "site" + user_templates = user_config / "vehicle_templates" + site_templates = site_config / "vehicle_templates" + user_templates.mkdir(parents=True) + site_templates.mkdir(parents=True) + user_system_file = user_templates / "system_vehicle_components_template.json" + user_system_file.write_text("{}", encoding="utf-8") + (site_templates / user_system_file.name).write_text("{}", encoding="utf-8") + personal_presets = user_templates / "user_vehicle_components_template.json" + personal_presets.write_text('{"Flight Controller": [{"name": "Personal preset"}]}', encoding="utf-8") + with ( + patch( + "ardupilot_methodic_configurator.backend_filesystem_program_settings.platform_system", return_value="Windows" + ), + patch.object(ProgramSettings, "_user_config_dir", return_value=str(user_config)), + patch.object(ProgramSettings, "_site_config_dir", return_value=str(site_config)), + ): + assert ProgramSettings.get_templates_base_dir() == str(user_templates) + + user_system_file.unlink() + + assert ProgramSettings.get_templates_base_dir() == str(site_templates) + assert json.loads(personal_presets.read_text(encoding="utf-8")) == { + "Flight Controller": [{"name": "Personal preset"}] + } + + +class TestComponentPresetScope: + """Preserve personal component presets when Windows installation scope changes.""" + + @pytest.mark.parametrize("old_scope", ["user", "site"]) + def test_saved_presets_survive_scope_switch_and_subsequent_save( + self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch, old_scope: str + ) -> None: + """ + Load existing presets after switching either way, then retain them on the next save. + + GIVEN a personal preset saved in an installation scope that is later uninstalled + WHEN another scope is installed and the user loads and saves component templates + THEN the old preset remains available and the new scope owns the updated file + """ + monkeypatch.delenv("AMC_COMPONENT_TEMPLATES_DIR_TEST", raising=False) + user_config, site_config = tmp_path / "user", tmp_path / "site" + user_templates, site_templates = user_config / "vehicle_templates", site_config / "vehicle_templates" + user_templates.mkdir(parents=True) + site_templates.mkdir(parents=True) + marker = "system_vehicle_components_template.json" + (site_templates / marker).write_text("{}", encoding="utf-8") + if old_scope == "user": + (user_templates / marker).write_text("{}", encoding="utf-8") + old_templates = user_templates if old_scope == "user" else site_templates + new_templates = site_templates if old_scope == "user" else user_templates + preset = {"Flight Controller": [{"name": "Personal preset", "data": {"Manufacturer": "Example"}}]} + added = {"Battery": [{"name": "New preset", "data": {"Cells": 4}}]} + + with ( + patch( + "ardupilot_methodic_configurator.backend_filesystem_program_settings.platform_system", return_value="Windows" + ), + patch( + "ardupilot_methodic_configurator.backend_filesystem_program_settings.user_config_dir", + return_value=str(user_config), + ), + patch( + "ardupilot_methodic_configurator.backend_filesystem_program_settings.site_config_dir", + return_value=str(site_config), + ), + ): + components = VehicleComponents() + assert not components.save_component_templates_to_file(preset)[0] + assert ProgramSettings.get_templates_base_dir() == str(old_templates) + old_file = old_templates / "user_vehicle_components_template.json" + assert json.loads(old_file.read_text(encoding="utf-8")) == preset + + (old_templates / marker).unlink() + (new_templates / marker).write_text("{}", encoding="utf-8") + assert ProgramSettings.get_templates_base_dir() == str(new_templates) + assert components.load_component_templates()["Flight Controller"] == [ + {**preset["Flight Controller"][0], "is_user_modified": True} + ] + assert not components.save_component_templates_to_file(added)[0] + assert json.loads((new_templates / old_file.name).read_text(encoding="utf-8")) == {**preset, **added} + assert json.loads(old_file.read_text(encoding="utf-8")) == preset + + old_file.write_text('{"Ignored": [{"name": "Old"}]}', encoding="utf-8") + assert components.load_component_templates() == { + "Flight Controller": [{**preset["Flight Controller"][0], "is_user_modified": True}], + "Battery": [{**added["Battery"][0], "is_user_modified": True}], + } + + @pytest.mark.parametrize("selected_content", ["{}", "{invalid json"]) + def test_selected_scope_file_remains_authoritative( + self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch, selected_content: str + ) -> None: + """An empty or invalid selected file does not restore presets from the other scope.""" + monkeypatch.delenv("AMC_COMPONENT_TEMPLATES_DIR_TEST", raising=False) + user_config, site_config = tmp_path / "user", tmp_path / "site" + user_templates, site_templates = user_config / "vehicle_templates", site_config / "vehicle_templates" + user_templates.mkdir(parents=True) + site_templates.mkdir(parents=True) + (user_templates / "system_vehicle_components_template.json").write_text("{}", encoding="utf-8") + (user_templates / "user_vehicle_components_template.json").write_text(selected_content, encoding="utf-8") + (site_templates / "user_vehicle_components_template.json").write_text( + '{"Flight Controller": [{"name": "Old preset"}]}', encoding="utf-8" + ) + with ( + patch( + "ardupilot_methodic_configurator.backend_filesystem_program_settings.platform_system", return_value="Windows" + ), + patch( + "ardupilot_methodic_configurator.backend_filesystem_program_settings.user_config_dir", + return_value=str(user_config), + ), + patch( + "ardupilot_methodic_configurator.backend_filesystem_program_settings.site_config_dir", + return_value=str(site_config), + ), + ): + assert VehicleComponents().load_component_templates() == {} + + @pytest.mark.parametrize(("platform_name", "override"), [("Linux", False), ("Windows", True)]) + def test_alternate_scope_is_disabled_outside_normal_windows_installs( + self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch, platform_name: str, override: bool + ) -> None: + """Test overrides and non-Windows installations never read another scope's presets.""" + if override: + monkeypatch.setenv("AMC_COMPONENT_TEMPLATES_DIR_TEST", str(tmp_path)) + else: + monkeypatch.delenv("AMC_COMPONENT_TEMPLATES_DIR_TEST", raising=False) + with ( + patch( + "ardupilot_methodic_configurator.backend_filesystem_program_settings.platform_system", + return_value=platform_name, + ), + patch("ardupilot_methodic_configurator.backend_filesystem_program_settings.user_config_dir") as user_dir, + patch("ardupilot_methodic_configurator.backend_filesystem_program_settings.site_config_dir") as site_dir, + ): + assert ProgramSettings.get_other_windows_templates_base_dir(str(tmp_path)) is None + user_dir.assert_not_called() + site_dir.assert_not_called() diff --git a/windows/ardupilot_methodic_configurator.iss b/windows/ardupilot_methodic_configurator.iss index 3aac983a7..be3f4c578 100644 --- a/windows/ardupilot_methodic_configurator.iss +++ b/windows/ardupilot_methodic_configurator.iss @@ -21,7 +21,9 @@ AppPublisher={#MyAppPublisher} AppPublisherURL={#MyAppURL} AppSupportURL={#MyAppURL} AppUpdatesURL={#MyAppURL} -DefaultDirName={commonpf}\{#MyAppName} +PrivilegesRequired=lowest +PrivilegesRequiredOverridesAllowed=dialog +DefaultDirName={autopf}\{#MyAppName} ; DefaultDirName={code:GetDefaultDirName} DefaultGroupName={#MyAppName} LicenseFile=..\LICENSE.md @@ -31,52 +33,52 @@ SolidCompression=yes ChangesEnvironment=yes [InstallDelete] -; Remove main application files -Type: filesandordirs; Name: {commonpf}\{#MyAppName} +; Remove stale application files from the default directory for this scope. +Type: filesandordirs; Name: "{autopf}\{#MyAppName}" ; Remove ArduCopter template files -Type: filesandordirs; Name: "{commonappdata}\.ardupilot_methodic_configurator\vehicle_templates\ArduCopter\diatone_taycan_mxc\4.3.8-params" -Type: filesandordirs; Name: "{commonappdata}\.ardupilot_methodic_configurator\vehicle_templates\ArduCopter\diatone_taycan_mxc\4.4.4-params" -Type: filesandordirs; Name: "{commonappdata}\.ardupilot_methodic_configurator\vehicle_templates\ArduCopter\diatone_taycan_mxc\4.5.1-params" -Type: filesandordirs; Name: "{commonappdata}\.ardupilot_methodic_configurator\vehicle_templates\ArduCopter\diatone_taycan_mxc\4.5.2-params" -Type: filesandordirs; Name: "{commonappdata}\.ardupilot_methodic_configurator\vehicle_templates\ArduCopter\diatone_taycan_mxc\4.5.3-params" -Type: filesandordirs; Name: "{commonappdata}\.ardupilot_methodic_configurator\vehicle_templates\ArduCopter\diatone_taycan_mxc\4.6.0-DEV-params" -Type: filesandordirs; Name: "{commonappdata}\.ardupilot_methodic_configurator\vehicle_templates\ArduCopter\diatone_taycan_mxc\4.5.x-params" -Type: filesandordirs; Name: "{commonappdata}\.ardupilot_methodic_configurator\vehicle_templates\ArduCopter\diatone_taycan_mxc\4.6.x-params" -Type: filesandordirs; Name: "{commonappdata}\.ardupilot_methodic_configurator\vehicle_templates\ArduCopter\empty_4.4.x" -Type: filesandordirs; Name: "{commonappdata}\.ardupilot_methodic_configurator\vehicle_templates\ArduCopter\empty_4.5.x" -Type: filesandordirs; Name: "{commonappdata}\.ardupilot_methodic_configurator\vehicle_templates\ArduCopter\empty_4.6.x" -Type: filesandordirs; Name: "{commonappdata}\.ardupilot_methodic_configurator\vehicle_templates\ArduCopter\empty_4.7.x" -Type: filesandordirs; Name: "{commonappdata}\.ardupilot_methodic_configurator\vehicle_templates\ArduCopter\empty_4.8.x" -Type: filesandordirs; Name: "{commonappdata}\.ardupilot_methodic_configurator\vehicle_templates\ArduCopter\X11_plus"; -Type: filesandordirs; Name: "{commonappdata}\.ardupilot_methodic_configurator\vehicle_templates\ArduCopter\Marmotte5v2"; -Type: filesandordirs; Name: "{commonappdata}\.ardupilot_methodic_configurator\vehicle_templates\ArduCopter\Chimera7"; -Type: filesandordirs; Name: "{commonappdata}\.ardupilot_methodic_configurator\vehicle_templates\ArduCopter\ReadyToSkyZD550"; -Type: filesandordirs; Name: "{commonappdata}\.ardupilot_methodic_configurator\vehicle_templates\ArduCopter\FETtec-5"; -Type: filesandordirs; Name: "{commonappdata}\.ardupilot_methodic_configurator\vehicle_templates\ArduCopter\Tarot_X4"; -Type: filesandordirs; Name: "{commonappdata}\.ardupilot_methodic_configurator\vehicle_templates\ArduCopter\TarotFY680Hexacopter"; -Type: filesandordirs; Name: "{commonappdata}\.ardupilot_methodic_configurator\vehicle_templates\ArduCopter\GazeboIrisWithTargetFollow"; -Type: filesandordirs; Name: "{commonappdata}\.ardupilot_methodic_configurator\vehicle_templates\ArduCopter\Hoverit_X11+"; -Type: filesandordirs; Name: "{commonappdata}\.ardupilot_methodic_configurator\vehicle_templates\ArduCopter\Hoverit_X13"; -Type: filesandordirs; Name: "{commonappdata}\.ardupilot_methodic_configurator\vehicle_templates\ArduCopter\Holybro_X500"; -Type: filesandordirs; Name: "{commonappdata}\.ardupilot_methodic_configurator\vehicle_templates\ArduCopter\Holybro_X500_V2"; -Type: filesandordirs; Name: "{commonappdata}\.ardupilot_methodic_configurator\vehicle_templates\ArduCopter\Holybro_X650_LTE"; -Type: filesandordirs; Name: "{commonappdata}\.ardupilot_methodic_configurator\vehicle_templates\ArduCopter\Demo32Motor_PeterHall"; -Type: filesandordirs; Name: "{commonappdata}\.ardupilot_methodic_configurator\vehicle_templates\ArduCopter\ESM450X-Des"; +Type: filesandordirs; Name: "{autoappdata}\.ardupilot_methodic_configurator\vehicle_templates\ArduCopter\diatone_taycan_mxc\4.3.8-params" +Type: filesandordirs; Name: "{autoappdata}\.ardupilot_methodic_configurator\vehicle_templates\ArduCopter\diatone_taycan_mxc\4.4.4-params" +Type: filesandordirs; Name: "{autoappdata}\.ardupilot_methodic_configurator\vehicle_templates\ArduCopter\diatone_taycan_mxc\4.5.1-params" +Type: filesandordirs; Name: "{autoappdata}\.ardupilot_methodic_configurator\vehicle_templates\ArduCopter\diatone_taycan_mxc\4.5.2-params" +Type: filesandordirs; Name: "{autoappdata}\.ardupilot_methodic_configurator\vehicle_templates\ArduCopter\diatone_taycan_mxc\4.5.3-params" +Type: filesandordirs; Name: "{autoappdata}\.ardupilot_methodic_configurator\vehicle_templates\ArduCopter\diatone_taycan_mxc\4.6.0-DEV-params" +Type: filesandordirs; Name: "{autoappdata}\.ardupilot_methodic_configurator\vehicle_templates\ArduCopter\diatone_taycan_mxc\4.5.x-params" +Type: filesandordirs; Name: "{autoappdata}\.ardupilot_methodic_configurator\vehicle_templates\ArduCopter\diatone_taycan_mxc\4.6.x-params" +Type: filesandordirs; Name: "{autoappdata}\.ardupilot_methodic_configurator\vehicle_templates\ArduCopter\empty_4.4.x" +Type: filesandordirs; Name: "{autoappdata}\.ardupilot_methodic_configurator\vehicle_templates\ArduCopter\empty_4.5.x" +Type: filesandordirs; Name: "{autoappdata}\.ardupilot_methodic_configurator\vehicle_templates\ArduCopter\empty_4.6.x" +Type: filesandordirs; Name: "{autoappdata}\.ardupilot_methodic_configurator\vehicle_templates\ArduCopter\empty_4.7.x" +Type: filesandordirs; Name: "{autoappdata}\.ardupilot_methodic_configurator\vehicle_templates\ArduCopter\empty_4.8.x" +Type: filesandordirs; Name: "{autoappdata}\.ardupilot_methodic_configurator\vehicle_templates\ArduCopter\X11_plus"; +Type: filesandordirs; Name: "{autoappdata}\.ardupilot_methodic_configurator\vehicle_templates\ArduCopter\Marmotte5v2"; +Type: filesandordirs; Name: "{autoappdata}\.ardupilot_methodic_configurator\vehicle_templates\ArduCopter\Chimera7"; +Type: filesandordirs; Name: "{autoappdata}\.ardupilot_methodic_configurator\vehicle_templates\ArduCopter\ReadyToSkyZD550"; +Type: filesandordirs; Name: "{autoappdata}\.ardupilot_methodic_configurator\vehicle_templates\ArduCopter\FETtec-5"; +Type: filesandordirs; Name: "{autoappdata}\.ardupilot_methodic_configurator\vehicle_templates\ArduCopter\Tarot_X4"; +Type: filesandordirs; Name: "{autoappdata}\.ardupilot_methodic_configurator\vehicle_templates\ArduCopter\TarotFY680Hexacopter"; +Type: filesandordirs; Name: "{autoappdata}\.ardupilot_methodic_configurator\vehicle_templates\ArduCopter\GazeboIrisWithTargetFollow"; +Type: filesandordirs; Name: "{autoappdata}\.ardupilot_methodic_configurator\vehicle_templates\ArduCopter\Hoverit_X11+"; +Type: filesandordirs; Name: "{autoappdata}\.ardupilot_methodic_configurator\vehicle_templates\ArduCopter\Hoverit_X13"; +Type: filesandordirs; Name: "{autoappdata}\.ardupilot_methodic_configurator\vehicle_templates\ArduCopter\Holybro_X500"; +Type: filesandordirs; Name: "{autoappdata}\.ardupilot_methodic_configurator\vehicle_templates\ArduCopter\Holybro_X500_V2"; +Type: filesandordirs; Name: "{autoappdata}\.ardupilot_methodic_configurator\vehicle_templates\ArduCopter\Holybro_X650_LTE"; +Type: filesandordirs; Name: "{autoappdata}\.ardupilot_methodic_configurator\vehicle_templates\ArduCopter\Demo32Motor_PeterHall"; +Type: filesandordirs; Name: "{autoappdata}\.ardupilot_methodic_configurator\vehicle_templates\ArduCopter\ESM450X-Des"; ; Remove ArduPlane template files -Type: filesandordirs; Name: "{commonappdata}\.ardupilot_methodic_configurator\vehicle_templates\ArduPlane\empty_4.7.x"; -Type: filesandordirs; Name: "{commonappdata}\.ardupilot_methodic_configurator\vehicle_templates\ArduPlane\normal_plane"; +Type: filesandordirs; Name: "{autoappdata}\.ardupilot_methodic_configurator\vehicle_templates\ArduPlane\empty_4.7.x"; +Type: filesandordirs; Name: "{autoappdata}\.ardupilot_methodic_configurator\vehicle_templates\ArduPlane\normal_plane"; ; Remove Heli template files -Type: filesandordirs; Name: "{commonappdata}\.ardupilot_methodic_configurator\vehicle_templates\Heli\Allister"; -Type: filesandordirs; Name: "{commonappdata}\.ardupilot_methodic_configurator\vehicle_templates\Heli\OMP_M4"; -Type: filesandordirs; Name: "{commonappdata}\.ardupilot_methodic_configurator\vehicle_templates\Heli\empty_4.7.x"; +Type: filesandordirs; Name: "{autoappdata}\.ardupilot_methodic_configurator\vehicle_templates\Heli\Allister"; +Type: filesandordirs; Name: "{autoappdata}\.ardupilot_methodic_configurator\vehicle_templates\Heli\OMP_M4"; +Type: filesandordirs; Name: "{autoappdata}\.ardupilot_methodic_configurator\vehicle_templates\Heli\empty_4.7.x"; ; Remove Rover template files -Type: filesandordirs; Name: "{commonappdata}\.ardupilot_methodic_configurator\vehicle_templates\Rover\AION_R1"; -Type: filesandordirs; Name: "{commonappdata}\.ardupilot_methodic_configurator\vehicle_templates\Rover\Carisma_SCA-1E"; -Type: filesandordirs; Name: "{commonappdata}\.ardupilot_methodic_configurator\vehicle_templates\Rover\empty_4.7.x"; +Type: filesandordirs; Name: "{autoappdata}\.ardupilot_methodic_configurator\vehicle_templates\Rover\AION_R1"; +Type: filesandordirs; Name: "{autoappdata}\.ardupilot_methodic_configurator\vehicle_templates\Rover\Carisma_SCA-1E"; +Type: filesandordirs; Name: "{autoappdata}\.ardupilot_methodic_configurator\vehicle_templates\Rover\empty_4.7.x"; ; Remove Outdated files Type: filesandordirs; Name: "{app}\_internal\ardupilot_methodic_configurator\ArduCopter_configuration_steps.json"; Type: filesandordirs; Name: "{app}\_internal\ardupilot_methodic_configurator\ArduPlane_configuration_steps.json"; @@ -84,8 +86,8 @@ Type: filesandordirs; Name: "{app}\_internal\ardupilot_methodic_configurator\con Type: filesandordirs; Name: "{app}\_internal\ardupilot_methodic_configurator\configuration_steps_ArduPlane.json"; Type: filesandordirs; Name: "{app}\_internal\ardupilot_methodic_configurator\configuration_steps_Heli.json"; Type: filesandordirs; Name: "{app}\_internal\ardupilot_methodic_configurator\configuration_steps_Rover.json"; -Type: filesandordirs; Name: "{commonappdata}\.ardupilot_methodic_configurator\version.txt"; -Type: filesandordirs; Name: "{commonappdata}\.ardupilot_methodic_configurator\git_hash.txt"; +Type: filesandordirs; Name: "{autoappdata}\.ardupilot_methodic_configurator\version.txt"; +Type: filesandordirs; Name: "{autoappdata}\.ardupilot_methodic_configurator\git_hash.txt"; [Languages] Name: "en"; MessagesFile: "compiler:Default.isl" @@ -97,14 +99,13 @@ Name: "ja"; MessagesFile: "compiler:Languages\Japanese.isl" [Tasks] Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}" -Name: "modifypath"; Description: "&Add application directory to environmental path for current user. For command line use"; Flags: unchecked checkablealone -Name: "modifypath\system"; Description: Add application directory to environmental path for all users. For command line use; Flags: unchecked +Name: "modifypath"; Description: "&Add application directory to PATH for command line use"; Flags: unchecked checkablealone [Files] Source: "..\ardupilot_methodic_configurator\dist\ardupilot_methodic_configurator\ardupilot_methodic_configurator.exe"; DestDir: "{app}"; Flags: ignoreversion Source: "..\ardupilot_methodic_configurator\dist\ardupilot_methodic_configurator\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs ; NOTE: Don't use "Flags: ignoreversion" on any shared system files -Source: "..\ardupilot_methodic_configurator\vehicle_templates\*.*"; DestDir: "{commonappdata}\.ardupilot_methodic_configurator\vehicle_templates"; Flags: ignoreversion recursesubdirs createallsubdirs +Source: "..\ardupilot_methodic_configurator\vehicle_templates\*.*"; DestDir: "{autoappdata}\.ardupilot_methodic_configurator\vehicle_templates"; Flags: ignoreversion recursesubdirs createallsubdirs Source: "..\git_hash.txt"; DestDir: "{app}\_internal\ardupilot_methodic_configurator"; Flags: ignoreversion Source: "..\windows\ardupilot_methodic_configurator.ico"; DestDir: "{app}"; Flags: ignoreversion Source: "..\ardupilot_methodic_configurator\images\*.png"; DestDir: "{app}\_internal\ardupilot_methodic_configurator\images"; Flags: ignoreversion recursesubdirs createallsubdirs @@ -122,9 +123,9 @@ Source: "..\ardupilot_methodic_configurator_command_line_completion.psm1"; DestD Name: "{userappdata}\.ardupilot_methodic_configurator\vehicles"; Flags: uninsneveruninstall [Icons] -Name: "{commondesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; WorkingDir: "{userappdata}\.ardupilot_methodic_configurator"; Tasks: desktopicon; IconFilename: "{app}\ardupilot_methodic_configurator.ico"; Parameters: "--language {language}" +Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; WorkingDir: "{userappdata}\.ardupilot_methodic_configurator"; Tasks: desktopicon; IconFilename: "{app}\ardupilot_methodic_configurator.ico"; Parameters: "--language {language}" Name: "{group}\Documentation"; Filename: "https://github.com/ArduPilot/MethodicConfigurator/blob/master/USERMANUAL.md" -Name: "{group}\Vehicle Templates"; Filename: "{commonappdata}\.ardupilot_methodic_configurator\vehicle_templates" +Name: "{group}\Vehicle Templates"; Filename: "{autoappdata}\.ardupilot_methodic_configurator\vehicle_templates" Name: "{group}\ArduPilot Methodic Configurator Forum"; Filename: "https://discuss.ardupilot.org/t/new-ardupilot-methodic-configurator-gui/115038/" Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; WorkingDir: "{userappdata}\.ardupilot_methodic_configurator"; IconFilename: "{app}\ardupilot_methodic_configurator.ico"; Parameters: "--language {language}" @@ -153,11 +154,10 @@ end; procedure CurStepChanged(CurStep: TSetupStep); begin if CurStep = ssPostInstall then begin - if IsTaskSelected(ModPathName + '\system') then begin - ModPathType := 'system'; - end else begin + if IsAdminInstallMode then + ModPathType := 'system' + else ModPathType := 'user'; - end; if IsTaskSelected(ModPathName) then ModPath(); diff --git a/windows/modpath.iss b/windows/modpath.iss index a886a20fd..a0cd8a6a4 100644 --- a/windows/modpath.iss +++ b/windows/modpath.iss @@ -183,6 +183,8 @@ var i: Integer; taskname: String; regpath: String; + regroot: Integer; + installedPath: String; regstring: String; appid: String; begin @@ -192,8 +194,15 @@ begin appid := '{#emit SetupSetting("AppId")}'; if appid = '' then appid := '{#emit SetupSetting("AppName")}'; regpath := ExpandConstant('Software\Microsoft\Windows\CurrentVersion\Uninstall\'+appid+'_is1'); - RegQueryStringValue(HKLM, regpath, 'Inno Setup: Selected Tasks', regstring); - if regstring = '' then RegQueryStringValue(HKCU, regpath, 'Inno Setup: Selected Tasks', regstring); + regroot := HKCU; + ModPathType := 'user'; + // Match the installation directory rather than the uninstaller's elevation. + if RegQueryStringValue(HKLM, regpath, 'Inno Setup: App Path', installedPath) and + (CompareText(installedPath, ExpandConstant('{app}')) = 0) then begin + regroot := HKLM; + ModPathType := 'system'; + end; + RegQueryStringValue(regroot, regpath, 'Inno Setup: Selected Tasks', regstring); // check each task; if matches modpath taskname, trigger patch removal if regstring <> '' then begin @@ -201,8 +210,14 @@ begin MPExplode(aSelectedTasks, regstring, ','); if GetArrayLength(aSelectedTasks) > 0 then begin for i := 0 to GetArrayLength(aSelectedTasks)-1 do begin - if comparetext(aSelectedTasks[i], taskname) = 0 then + if comparetext(aSelectedTasks[i], taskname) = 0 then begin ModPath(); + // Older all-users installers added the directory to the user PATH. + if regroot = HKLM then begin + ModPathType := 'user'; + ModPath(); + end; + end; end; end; end;