diff --git a/.flake8 b/.flake8 new file mode 100644 index 00000000..a8948ef1 --- /dev/null +++ b/.flake8 @@ -0,0 +1,7 @@ +[flake8] +filename = + *.py +max-line-length = 120 +extend-exclude = + .venv/ + venv/ diff --git a/.github/workflows/ci-macos-broker.yml b/.github/workflows/ci-macos-broker.yml index 15fcb65b..344b9ccc 100644 --- a/.github/workflows/ci-macos-broker.yml +++ b/.github/workflows/ci-macos-broker.yml @@ -45,6 +45,9 @@ jobs: - name: Install dependencies run: brew install cmake + - name: Test macOS installer quarantine handling + run: python3 tests/scripts/test-macos-install.py scripts/macos/install.command + - name: Configure universal build env: BUILD_VERSION: ${{ inputs.release_version }} diff --git a/scripts/macos/install.command b/scripts/macos/install.command index 81054dee..5bd109eb 100755 --- a/scripts/macos/install.command +++ b/scripts/macos/install.command @@ -35,6 +35,7 @@ fi /usr/bin/sudo /bin/rm -rf "${legacy_broker_app}" "${installed_broker_app}" "${installed_control_app}" /usr/bin/sudo /usr/bin/ditto "${broker_app}" "${installed_broker_app}" /usr/bin/sudo /usr/bin/ditto "${control_app}" "${installed_control_app}" +/usr/bin/sudo /usr/bin/xattr -dr com.apple.quarantine "${installed_broker_app}" "${installed_control_app}" /usr/bin/sudo /usr/sbin/chown -R root:wheel "${installed_broker_app}" "${installed_control_app}" /usr/bin/sudo /bin/chmod -R a+rX "${installed_broker_app}" "${installed_control_app}" /usr/bin/sudo /bin/chmod -R go-w "${installed_broker_app}" "${installed_control_app}" @@ -43,6 +44,7 @@ fi /usr/bin/sudo /usr/bin/install -m 0644 \ "${image_root}/${launchd_plist}" "${service_plist}" /usr/bin/sudo /usr/sbin/chown root:wheel "${service_plist}" +/usr/bin/sudo /usr/bin/xattr -dr com.apple.quarantine "${service_plist}" if [[ -d "${image_root}/usr/local/include/libvirtualhid" ]]; then /usr/bin/sudo /usr/bin/ditto "${image_root}/usr/local/include/libvirtualhid" /usr/local/include/libvirtualhid @@ -62,6 +64,14 @@ if [[ -d "${image_root}/usr/local/share/licenses/libvirtualhid" ]]; then /usr/bin/sudo /usr/bin/ditto "${image_root}/usr/local/share/licenses/libvirtualhid" /usr/local/share/licenses/libvirtualhid fi +for installed_path in /usr/local/include/libvirtualhid /usr/local/lib/libvirtualhid.a \ + /usr/local/bin/libvirtualhid-license /usr/local/lib/cmake/libvirtualhid \ + /usr/local/share/licenses/libvirtualhid; do + if [[ -e "${installed_path}" ]]; then + /usr/bin/sudo /usr/bin/xattr -dr com.apple.quarantine "${installed_path}" + fi +done + /usr/bin/sudo /bin/launchctl bootstrap system "${service_plist}" /usr/bin/sudo /bin/launchctl kickstart -k "system/${bundle_id}" echo 'Virtual HID Broker and Virtual HID Control installed in /Applications.' diff --git a/tests/scripts/test-macos-install.py b/tests/scripts/test-macos-install.py new file mode 100644 index 00000000..dc5e6216 --- /dev/null +++ b/tests/scripts/test-macos-install.py @@ -0,0 +1,78 @@ +import os +from pathlib import Path +import re +import subprocess +import sys +import tempfile + + +def run(*args): + return subprocess.run(args, check=True, capture_output=True, text=True).stdout + + +installer = Path(sys.argv[1]).read_text() +for quarantined in (False, True): + for optional in (False, True): + with tempfile.TemporaryDirectory(prefix="libvirtualhid-install-", dir="/private/tmp") as temp: + root = Path(temp) + image = root / "image with spaces" + destination = root / "installed" + for name in ("VirtualHIDBroker.app", "VirtualHIDControl.app"): + executable = image / name / "Contents/MacOS/example" + executable.parent.mkdir(parents=True) + executable.write_text("fixture") + (image / "dev.lizardbyte.app.libvirtualhid.plist").write_text("fixture") + if optional: + for name in ( + "include/libvirtualhid/gamepad.hpp", + "lib/libvirtualhid.a", + "bin/libvirtualhid-license", + "lib/cmake/libvirtualhid/libvirtualhid-config.cmake", + "share/licenses/libvirtualhid/LICENSE.md", + ): + path = image / "usr/local" / name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text("fixture") + run("/usr/bin/xattr", "-wr", "user.review_marker", "preserve", str(image)) + if quarantined: + run("/usr/bin/xattr", "-wr", "com.apple.quarantine", "0083;12345678;Safari;test", str(image)) + (destination / "Applications").mkdir(parents=True) + (destination / "Library/LaunchDaemons").mkdir(parents=True) + # Redirect fixed installation paths, leaving image-relative payload paths intact. + source = re.sub( + r"(?&2 + return 1 + fi + /usr/bin/touch "$TEST_BOOTSTRAPPED" + fi + return 0 ;; + esac + "$@" +} +''' + script = image / "Install libvirtualhid.command" + script.write_text(stubs + source) + marker = root / "bootstrapped" + env = dict(os.environ, TEST_DESTINATION=str(destination), TEST_BOOTSTRAPPED=str(marker)) + result = subprocess.run(["/bin/bash", str(script)], env=env, capture_output=True, text=True) + assert result.returncode == 0, (quarantined, optional, result.stderr) + assert marker.exists(), "bootstrap was skipped" + assert "user.review_marker: preserve" in run("/usr/bin/xattr", "-lr", str(destination)) + if quarantined: + assert "com.apple.quarantine" in run("/usr/bin/xattr", "-lr", str(image)) + print(f"PASS: quarantined={quarantined}, optional_payload={optional}")