From e9bfcac36deef7cc73a18b6dbc77c85d1c1bafe6 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Tue, 23 Apr 2024 14:46:54 +0200 Subject: [PATCH 01/12] Add support for visionOS --- Cargo.toml | 2 +- src/ios.rs | 4 +++- src/lib.rs | 34 ++++++++++++++++++++++++---------- 3 files changed, 28 insertions(+), 12 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 472148b..84a94eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,7 +35,7 @@ core-foundation = "0.9" jni = "0.21" ndk-context = "0.1" -[target.'cfg(target_os = "ios")'.dependencies] +[target.'cfg(any(target_os = "ios", target_os = "tvos", target_os = "visionos"))'.dependencies] raw-window-handle = "0.5.0" objc = "0.2.7" diff --git a/src/ios.rs b/src/ios.rs index 1767b14..2d56a4d 100644 --- a/src/ios.rs +++ b/src/ios.rs @@ -1,7 +1,9 @@ use crate::{Browser, BrowserOptions, Error, ErrorKind, Result, TargetType}; use objc::{class, msg_send, runtime::Object, sel, sel_impl}; -/// Deal with opening of browsers on iOS +/// Deal with opening of browsers on iOS/tvOS/visionOS. +/// +/// watchOS doesn't have a browser, so this won't work there. pub(super) fn open_browser_internal( _browser: Browser, target: &TargetType, diff --git a/src/lib.rs b/src/lib.rs index 5169f7c..046a35d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,14 +14,14 @@ //! //! ## Platform Support Status //! -//! | Platform | Supported | Browsers | Test status | -//! |----------|-----------|----------|-------------| -//! | macos | ✅ | default + [others](https://docs.rs/webbrowser/latest/webbrowser/enum.Browser.html) | ✅ | -//! | windows | ✅ | default only | ✅ | -//! | linux/wsl | ✅ | default only (respects $BROWSER env var, so can be used with other browsers) | ✅ | -//! | android | ✅ | default only | ✅ | -//! | ios | ✅ | default only | ✅ | -//! | wasm | ✅ | default only | ✅ | +//! | Platform | Supported | Browsers | Test status | +//! |-----------------------|-----------|----------|-------------| +//! | macOS | ✅ | default + [others](https://docs.rs/webbrowser/latest/webbrowser/enum.Browser.html) | ✅ | +//! | windows | ✅ | default only | ✅ | +//! | linux/wsl | ✅ | default only (respects $BROWSER env var, so can be used with other browsers) | ✅ | +//! | android | ✅ | default only | ✅ | +//! | iOS/tvOS/visionOS | ✅ | default only | ✅ | +//! | wasm | ✅ | default only | ✅ | //! | unix (*bsd, aix etc.) | ✅ | default only (respects $BROWSER env var, so can be used with other browsers) | Manual | //! //! ## Consistent Behaviour @@ -39,7 +39,10 @@ //! * `disable-wsl` - this disables WSL `file` implementation (`http` still works) //! * `wasm-console` - this enables logging to wasm console (valid only on wasm platform) -#[cfg_attr(any(target_os = "ios", target_os = "tvos"), path = "ios.rs")] +#[cfg_attr( + any(target_os = "ios", target_os = "tvos", target_os = "visionos"), + path = "ios.rs" +)] #[cfg_attr(target_os = "macos", path = "macos.rs")] #[cfg_attr(target_os = "android", path = "android.rs")] #[cfg_attr(target_family = "wasm", path = "wasm.rs")] @@ -50,6 +53,7 @@ not(any( target_os = "ios", target_os = "tvos", + target_os = "visionos", target_os = "macos", target_os = "android", target_family = "wasm", @@ -67,6 +71,7 @@ mod os; not(any( target_os = "ios", target_os = "tvos", + target_os = "visionos", target_os = "macos", target_os = "android", target_family = "wasm", @@ -316,6 +321,7 @@ pub fn open_browser_with_options( if cfg!(any( target_os = "ios", target_os = "tvos", + target_os = "visionos", target_os = "macos", target_os = "android", target_family = "wasm", @@ -338,6 +344,8 @@ impl TargetType { feature = "hardened", target_os = "android", target_os = "ios", + target_os = "tvos", + target_os = "visionos", target_family = "wasm" ))] fn is_http(&self) -> bool { @@ -346,7 +354,13 @@ impl TargetType { /// If `target` represents a valid http/https url, return the str corresponding to it /// else return `std::io::Error` of kind `std::io::ErrorKind::InvalidInput` - #[cfg(any(target_os = "android", target_os = "ios", target_family = "wasm"))] + #[cfg(any( + target_os = "android", + target_os = "ios", + target_os = "tvos", + target_os = "visionos", + target_family = "wasm" + ))] fn get_http_url(&self) -> Result<&str> { if self.is_http() { Ok(self.0.as_str()) From 25787f2b97369bacff0c832b10a03193325cd3a3 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Wed, 24 Apr 2024 02:48:18 +0200 Subject: [PATCH 02/12] iOS: Use `objc2` Concrete benefits to `webbrowser`: - Less error prone `msg_send!` macro invocations (I've refactored the rest into a separate function as recommended by the docs). - Prevents a leak of the `options` dictionary. - Catches errors when passing an invalid URL to `NSURL`. - Makes it easier to do something in the completion handler in the future. --- Cargo.toml | 9 +++++++-- src/ios.rs | 57 ++++++++++++++++++++++++++++++++---------------------- 2 files changed, 41 insertions(+), 25 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 84a94eb..bcdfb64 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,8 +36,13 @@ jni = "0.21" ndk-context = "0.1" [target.'cfg(any(target_os = "ios", target_os = "tvos", target_os = "visionos"))'.dependencies] -raw-window-handle = "0.5.0" -objc = "0.2.7" +block2 = "0.5.0" +objc2 = "0.5.1" +objc2-foundation = { version = "0.2.0", features = [ + "NSDictionary", + "NSString", + "NSURL", +] } [dev-dependencies] actix-web = "4" diff --git a/src/ios.rs b/src/ios.rs index 2d56a4d..ea926d4 100644 --- a/src/ios.rs +++ b/src/ios.rs @@ -1,5 +1,22 @@ use crate::{Browser, BrowserOptions, Error, ErrorKind, Result, TargetType}; -use objc::{class, msg_send, runtime::Object, sel, sel_impl}; +use block2::Block; +use objc2::rc::Id; +use objc2::runtime::Bool; +use objc2::{class, msg_send, msg_send_id}; +use objc2_foundation::{NSDictionary, NSObject, NSString, NSURL}; + +fn app() -> Option> { + unsafe { msg_send_id![class!(UIApplication), sharedApplication] } +} + +fn open_url( + app: &NSObject, + url: &NSURL, + options: &NSDictionary, + handler: Option<&Block>, +) { + unsafe { msg_send![app, openURL: url, options: options, completionHandler: handler] } +} /// Deal with opening of browsers on iOS/tvOS/visionOS. /// @@ -17,28 +34,22 @@ pub(super) fn open_browser_internal( return Ok(()); } - unsafe { - let app: *mut Object = msg_send![class!(UIApplication), sharedApplication]; - if app.is_null() { - return Err(Error::new( - ErrorKind::Other, - "UIApplication is null, can't open url", - )); - } - - let url_cstr = std::ffi::CString::new(url)?; + let app = app().ok_or(Error::new( + ErrorKind::Other, + "UIApplication is null, can't open url", + ))?; - // Create ns string class from our string - let url_string: *mut Object = msg_send![class!(NSString), stringWithUTF8String: url_cstr]; - // Create NSURL object with given string - let url_object: *mut Object = msg_send![class!(NSURL), URLWithString: url_string]; - // No completion handler - let nil: *mut Object = ::core::ptr::null_mut(); - // empty options dictionary - let no_options: *mut Object = msg_send![class!(NSDictionary), new]; + // Create ns string class from our string + let url_string = NSString::from_str(url); + // Create NSURL object with given string + let url_object = unsafe { NSURL::URLWithString(&url_string) }.ok_or(Error::new( + ErrorKind::Other, + "Failed creating NSURL; is the URL valid?", + ))?; + // empty options dictionary + let options = NSDictionary::new(); - // Open url - let () = msg_send![app, openURL:url_object options:no_options completionHandler:nil]; - Ok(()) - } + // Open url + open_url(&app, &url_object, &options, None); + Ok(()) } From 0a6fe550ba42614c2db5b89b5b3c430518048fd8 Mon Sep 17 00:00:00 2001 From: Amod Malviya Date: Sat, 4 May 2024 14:00:40 +0530 Subject: [PATCH 03/12] fix ios build #build-ios --- .github/workflows/ios.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ios.yaml b/.github/workflows/ios.yaml index 8973c67..dfe7f6a 100644 --- a/.github/workflows/ios.yaml +++ b/.github/workflows/ios.yaml @@ -10,7 +10,7 @@ env: CARGO_TERM_COLOR: always RUST_BACKTRACE: 1 RUST_LOG: webbrowser=TRACE - IOS_TARGET: x86_64-apple-ios + IOS_TARGET: aarch64-apple-ios-sim jobs: build: From 5dfb6ce2932a0cd5c54e6c3cdf85f798847834c0 Mon Sep 17 00:00:00 2001 From: Amod Malviya Date: Sat, 4 May 2024 14:23:13 +0530 Subject: [PATCH 04/12] macos: fix github test as macos-latest-arm64 does not include firefox --- tests/test_macos.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_macos.rs b/tests/test_macos.rs index 81972fe..d288fc3 100644 --- a/tests/test_macos.rs +++ b/tests/test_macos.rs @@ -18,11 +18,11 @@ mod tests { check_browser(Browser::Safari, TEST_PLATFORM).await; } - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - #[ignore] - async fn test_open_firefox() { - check_browser(Browser::Firefox, TEST_PLATFORM).await; - } + // #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + // #[ignore] + // async fn test_open_firefox() { + // check_browser(Browser::Firefox, TEST_PLATFORM).await; + // } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[ignore] From fbbf541855275c2a280f308a78ed46e38ab087ad Mon Sep 17 00:00:00 2001 From: Amod Malviya Date: Sat, 4 May 2024 14:32:52 +0530 Subject: [PATCH 05/12] android: use ubuntu for github tests instead of macos --- .github/workflows/android.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/android.yaml b/.github/workflows/android.yaml index d33b163..9df0edd 100644 --- a/.github/workflows/android.yaml +++ b/.github/workflows/android.yaml @@ -17,7 +17,7 @@ env: jobs: build: name: Build - runs-on: macos-latest + runs-on: ubuntu-latest if: ${{ !contains(github.event.head_commit.message, '#build-') || contains(github.event.head_commit.message, '#build-android') }} strategy: matrix: From a387ef097184b1e5952d6c6db5a67b7f2107323e Mon Sep 17 00:00:00 2001 From: Amod Malviya Date: Sat, 4 May 2024 16:56:40 +0530 Subject: [PATCH 06/12] ios: enable full backtrace and nocapture in tests --- .github/workflows/ios.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ios.yaml b/.github/workflows/ios.yaml index dfe7f6a..c4db43e 100644 --- a/.github/workflows/ios.yaml +++ b/.github/workflows/ios.yaml @@ -8,7 +8,7 @@ on: env: CARGO_TERM_COLOR: always - RUST_BACKTRACE: 1 + RUST_BACKTRACE: full RUST_LOG: webbrowser=TRACE IOS_TARGET: aarch64-apple-ios-sim @@ -40,12 +40,12 @@ jobs: DEVID=$(xcrun simctl create iphone-latest $IOSDEV $IOSRUNTIME) echo "==== using device $IOSDEV, $IOSRUNTIME ====" xcrun simctl boot $DEVID - sleep 5 + sleep 10 xcrun simctl list 2>&1 # Run tests - name: Run tests - run: cargo +${{ matrix.rust }} test --verbose --test test_ios -- --include-ignored + run: cargo +${{ matrix.rust }} test --verbose --test test_ios -- --include-ignored --nocapture # Code format, linting etc. - name: Check Code Formatting From a63f1aecb41a163fa0daf1971b9c4bf8d3d2485a Mon Sep 17 00:00:00 2001 From: Amod Malviya Date: Sat, 4 May 2024 17:26:06 +0530 Subject: [PATCH 07/12] ios: tests - have a compilation warm up to reduce the risk of overshooting wait time --- tests/test_ios.rs | 47 ++++++++++++++++++++++++++--------------------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/tests/test_ios.rs b/tests/test_ios.rs index 62c16cd..d1fb8e7 100644 --- a/tests/test_ios.rs +++ b/tests/test_ios.rs @@ -29,6 +29,31 @@ mod tests { glue_dir.push("testglue"); run_cmd(&glue_dir, &["./build"]).expect("glue code build failed"); + let compile_app = || { + run_cmd( + &app_dir, + &[ + "xcrun", + "xcodebuild", + "-project", + "test-ios-app.xcodeproj", + "-configuration", + "Debug", + "-sdk", + "iphonesimulator", + "-destination", + "platform=iOS Simulator,name=iphone-latest", + "-arch", + if cfg!(target_arch = "aarch64") { + "arm64" + } else { + "x86_64" + }, + ], + ) + }; + compile_app().expect("compilation warm up failed for the app"); + // invoke server check_request_received_using(uri, &ipv4, |url, _port| { // modify ios app code to use the correct url @@ -62,27 +87,7 @@ mod tests { }; // build app - let exec_result = run_cmd( - &app_dir, - &[ - "xcrun", - "xcodebuild", - "-project", - "test-ios-app.xcodeproj", - "-configuration", - "Debug", - "-sdk", - "iphonesimulator", - "-destination", - "platform=iOS Simulator,name=iphone-latest", - "-arch", - if cfg!(target_arch = "aarch64") { - "arm64" - } else { - "x86_64" - }, - ], - ); + let exec_result = compile_app(); handle_exec_result(exec_result, "failed to build ios app"); // launch app on simulator From bc70f6a7e2ac9755be8328ecdb0137933e8018fb Mon Sep 17 00:00:00 2001 From: Amod Malviya Date: Sat, 4 May 2024 17:54:26 +0530 Subject: [PATCH 08/12] ios: use xcode 15.3 in tests to see if slowdown issues are resolved --- .github/workflows/ios.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ios.yaml b/.github/workflows/ios.yaml index c4db43e..09c6a75 100644 --- a/.github/workflows/ios.yaml +++ b/.github/workflows/ios.yaml @@ -24,6 +24,8 @@ jobs: steps: - uses: actions/checkout@v3 name: Checkout + - name: Select Xcode 15.3 + run: sudo xcode-select -s /Applications/Xcode_15.3.app/Contents/Developer - name: Install rust version run: | rustup install ${{ matrix.rust }} --profile minimal From 1117385328164f91923c728d9fab4519c230fdb8 Mon Sep 17 00:00:00 2001 From: Amod Malviya Date: Sat, 4 May 2024 18:33:15 +0530 Subject: [PATCH 09/12] ios: apple sucks. check if ios 17.4 works --- .github/workflows/ios.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ios.yaml b/.github/workflows/ios.yaml index 09c6a75..adb68a5 100644 --- a/.github/workflows/ios.yaml +++ b/.github/workflows/ios.yaml @@ -37,7 +37,7 @@ jobs: set -e open -a Simulator sleep 5 - IOSRUNTIME=$(xcrun simctl list 2>&1 | egrep '^iOS' | head -n 1 | awk '{ print $NF }') + IOSRUNTIME=com.apple.CoreSimulator.SimRuntime.iOS-17-4 IOSDEV=$(xcrun simctl list 2>&1 | grep com.apple.CoreSimulator.SimDeviceType.iPhone | grep -v ' SE ' | tail -n 1 | tr -d '()' | awk '{ print $NF }') DEVID=$(xcrun simctl create iphone-latest $IOSDEV $IOSRUNTIME) echo "==== using device $IOSDEV, $IOSRUNTIME ====" From 45f73d3390000571f79615c46e72e49b5ee20fab Mon Sep 17 00:00:00 2001 From: Amod Malviya Date: Sat, 4 May 2024 18:47:19 +0530 Subject: [PATCH 10/12] ios: use a higher timeout in tests --- tests/common.rs | 2 +- tests/test_ios.rs | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/common.rs b/tests/common.rs index ea1998a..b88ab39 100644 --- a/tests/common.rs +++ b/tests/common.rs @@ -81,7 +81,7 @@ where op(&format!("http://{}:{}{}", host, port, &uri), port); // wait for the url to be hit - let timeout = 90; + let timeout = if cfg!(target_os = "ios") { 360 } else { 90 }; match rx.recv_timeout(std::time::Duration::from_secs(timeout)) { Ok(msg) => assert_eq!(decode(&msg).unwrap(), uri), Err(_) => panic!("failed to receive uri data"), diff --git a/tests/test_ios.rs b/tests/test_ios.rs index d1fb8e7..552e455 100644 --- a/tests/test_ios.rs +++ b/tests/test_ios.rs @@ -72,8 +72,9 @@ mod tests { }) .collect::>() .join("\n"); - fs::write(&swift_src, new_code).expect("failed to modify ContentView.swift"); + fs::write(&swift_src, &new_code).expect("failed to modify ContentView.swift"); let revert_code = || fs::write(&swift_src, &old_code).expect("failed to revert code"); + println!("Modifying ContentView.swift to:\n{}", &new_code); let handle_exec_result = |result: std::io::Result, err_msg: &str| { revert_code(); let success = match result { From e5d27c514ce9f5bfeb517573558ce04297de7b20 Mon Sep 17 00:00:00 2001 From: Amod Malviya Date: Sat, 4 May 2024 19:11:11 +0530 Subject: [PATCH 11/12] ios: fix test timeout --- .github/workflows/ios.yaml | 2 ++ tests/common.rs | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ios.yaml b/.github/workflows/ios.yaml index adb68a5..0636a20 100644 --- a/.github/workflows/ios.yaml +++ b/.github/workflows/ios.yaml @@ -48,6 +48,8 @@ jobs: # Run tests - name: Run tests run: cargo +${{ matrix.rust }} test --verbose --test test_ios -- --include-ignored --nocapture + env: + TEST_REQ_TIMEOUT: '300' # Code format, linting etc. - name: Check Code Formatting diff --git a/tests/common.rs b/tests/common.rs index b88ab39..aa2c464 100644 --- a/tests/common.rs +++ b/tests/common.rs @@ -81,7 +81,9 @@ where op(&format!("http://{}:{}{}", host, port, &uri), port); // wait for the url to be hit - let timeout = if cfg!(target_os = "ios") { 360 } else { 90 }; + let timeout = option_env!("TEST_REQ_TIMEOUT") + .map(|s| s.parse().expect("failed to parse TEST_REQ_TIMEOUT")) + .unwrap_or(90); match rx.recv_timeout(std::time::Duration::from_secs(timeout)) { Ok(msg) => assert_eq!(decode(&msg).unwrap(), uri), Err(_) => panic!("failed to receive uri data"), From 9184245a47c9c5abdce7e9de1bffd31657c09999 Mon Sep 17 00:00:00 2001 From: Amod Malviya Date: Sat, 4 May 2024 19:31:39 +0530 Subject: [PATCH 12/12] ios: try tests with xcode 15.4 and ios 17.5 --- .github/workflows/ios.yaml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ios.yaml b/.github/workflows/ios.yaml index 0636a20..b6b765e 100644 --- a/.github/workflows/ios.yaml +++ b/.github/workflows/ios.yaml @@ -24,8 +24,8 @@ jobs: steps: - uses: actions/checkout@v3 name: Checkout - - name: Select Xcode 15.3 - run: sudo xcode-select -s /Applications/Xcode_15.3.app/Contents/Developer + - name: Select Xcode 15.4 + run: sudo xcode-select -s /Applications/Xcode_15.4.app/Contents/Developer - name: Install rust version run: | rustup install ${{ matrix.rust }} --profile minimal @@ -35,9 +35,7 @@ jobs: - name: Configure and start iOS Simulator run: | set -e - open -a Simulator - sleep 5 - IOSRUNTIME=com.apple.CoreSimulator.SimRuntime.iOS-17-4 + IOSRUNTIME=com.apple.CoreSimulator.SimRuntime.iOS-17-5 IOSDEV=$(xcrun simctl list 2>&1 | grep com.apple.CoreSimulator.SimDeviceType.iPhone | grep -v ' SE ' | tail -n 1 | tr -d '()' | awk '{ print $NF }') DEVID=$(xcrun simctl create iphone-latest $IOSDEV $IOSRUNTIME) echo "==== using device $IOSDEV, $IOSRUNTIME ===="