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: diff --git a/.github/workflows/ios.yaml b/.github/workflows/ios.yaml index 8973c67..b6b765e 100644 --- a/.github/workflows/ios.yaml +++ b/.github/workflows/ios.yaml @@ -8,9 +8,9 @@ on: env: CARGO_TERM_COLOR: always - RUST_BACKTRACE: 1 + RUST_BACKTRACE: full RUST_LOG: webbrowser=TRACE - IOS_TARGET: x86_64-apple-ios + IOS_TARGET: aarch64-apple-ios-sim jobs: build: @@ -24,6 +24,8 @@ jobs: steps: - uses: actions/checkout@v3 name: Checkout + - 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 @@ -33,19 +35,19 @@ jobs: - name: Configure and start iOS Simulator run: | 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-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 ====" 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 + env: + TEST_REQ_TIMEOUT: '300' # Code format, linting etc. - name: Check Code Formatting diff --git a/Cargo.toml b/Cargo.toml index 472148b..bcdfb64 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,9 +35,14 @@ core-foundation = "0.9" jni = "0.21" ndk-context = "0.1" -[target.'cfg(target_os = "ios")'.dependencies] -raw-window-handle = "0.5.0" -objc = "0.2.7" +[target.'cfg(any(target_os = "ios", target_os = "tvos", target_os = "visionos"))'.dependencies] +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 1767b14..ea926d4 100644 --- a/src/ios.rs +++ b/src/ios.rs @@ -1,7 +1,26 @@ 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}; -/// Deal with opening of browsers on iOS +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. +/// +/// watchOS doesn't have a browser, so this won't work there. pub(super) fn open_browser_internal( _browser: Browser, target: &TargetType, @@ -15,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(()) } 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()) diff --git a/tests/common.rs b/tests/common.rs index ea1998a..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 = 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"), diff --git a/tests/test_ios.rs b/tests/test_ios.rs index 62c16cd..552e455 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 @@ -47,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 { @@ -62,27 +88,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 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]