tauri-apps / cargo-mobile2

Rust on mobile made easy!
Apache License 2.0
1.62k stars 78 forks source link

Equi template fails on macos and ios #165

Closed QuantumEntangledAndy closed 1 year ago

QuantumEntangledAndy commented 1 year ago

Describe the bug Equi example fails on all devices I have access to. Macos and ios.

Macos fails with

error[E0603]: function `main` is private
   --> gen/bin/desktop.rs:3:18
    |
3   |     mobile_equi::main();
    |                  ^^^^ private function
    |
note: the function `main` is defined here
   --> /Users/awk21/Projects/Coding/Rust/mobile_equi/src/lib.rs:160:1
    |
160 | fn main() {
    | ^^^^^^^^^

For more information about this error, try `rustc --explain E0603`.
error: could not compile `mobile-equi` due to previous error

and ios with

Undefined symbols for architecture arm64:
  "_start_app", referenced from:
      _main in main.o
ld: symbol(s) not found for architecture arm64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
note: Building targets in dependency order
note: Run script build phase 'Build Rust Code' will be run during every build because the option to run the script phase "Based on dependency analysis" is unchecked. (in target 'mobile-equi_iOS' from project 'mobile-equi')
** BUILD FAILED **

error: Failed to build via `xcodebuild`
    command ["xcodebuild"] exited with code 65

The first error is easy to fix with the addition of a pub but the latter needed more work

Steps To Reproduce Create a equi tempalte and try to run or archive it with apple

Expected behavior No errors

Platform and Versions (please complete the following information): Host OS: Macos Target OS: Macos and ios Rustc: rustc 1.69.0 (84c898d65 2023-04-16) cargo mobile doctor:

[✔] cargo-mobile v0.5.0 • Contains commits up to "Add egui example (#164)\n" • Installed at "~/.cargo/.tauri-mobile" • macOS v12.5 (21G72) • rustc v1.69.0 (84c898d65 2023-4-16)

[✔] Apple developer tools • Xcode v14.2 • Active developer dir: "/Applications/Xcode.app/Contents/Developer" • ios-deploy v1.12.2 • XcodeGen v2.35.0 ✗ xcode-rust-plugin plugin absent ✗ xcode-rust-plugin lang spec absent ✗ xcode-rust-plugin lang metadata absent • xcode-rust-plugin is up-to-date ✗ xcode-rust-plugin doesn't support Xcode UUID "C91F3560-00E7-4749-8E3F-4D83B1496051" • Development team: Andrew King (N7VJA8E4L8)

[!] Android developer tools ✗ Have you installed the Android SDK? The ANDROID_HOME environment variable isn't set, and is required: environment variable not found

[!] Connected devices ✗ Failed to get iOS device list: Failed to request device list from ios-deploy: command ["ios-deploy", "--detect", "--timeout", "1", "--json", "--no-wifi"] exited with code 253

Would you want to assign yourself to resolve this bug?

Additional context See comments on #164 too

QuantumEntangledAndy commented 1 year ago

The following diff should get it to work

--- ../mobile_cmp/src/lib.rs    2023-05-30 16:09:01.000000000 +0700
+++ src/lib.rs  2023-06-01 16:48:39.000000000 +0700
@@ -1,6 +1,7 @@
 #[cfg(target_os = "android")]
 use winit::platform::android::activity::AndroidApp;

+use anyhow::Result;
 use winit::event::Event::*;
 use winit::event_loop::{ControlFlow, EventLoop, EventLoopBuilder, EventLoopWindowTarget};

@@ -139,25 +140,34 @@
     });
 }

-#[allow(dead_code)]
-#[cfg(target_os = "android")]
-#[no_mangle]
-fn android_main(app: AndroidApp) {
-    use winit::platform::android::EventLoopBuilderExtAndroid;
+#[cfg(any(target_os = "android", target_os = "ios"))]
+fn stop_unwind<F: FnOnce() -> T, T>(f: F) -> T {
+    match std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)) {
+        Ok(t) => t,
+        Err(err) => {
+            eprintln!("attempt to unwind out of `rust` with err: {:?}", err);
+            std::process::abort()
+        }
+    }
+}

-    android_logger::init_once(
-        android_logger::Config::default().with_max_level(log::LevelFilter::Warn),
-    );
+#[cfg(any(target_os = "android", target_os = "ios"))]
+fn _start_app() {
+    stop_unwind(|| main().unwrap());
+}

-    let event_loop = EventLoopBuilder::with_user_event()
-        .with_android_app(app)
-        .build();
-    _main(event_loop);
+#[no_mangle]
+#[inline(never)]
+#[cfg(any(target_os = "android", target_os = "ios"))]
+pub extern "C" fn start_app() {
+    #[cfg(target_os = "android")]
+    android_binding!({{reverse-domain-snake-case app.domain}}, {{snake-case app.name}}, _start_app);
+    #[cfg(target_os = "ios")]
+    _start_app()
 }

-#[allow(dead_code)]
 #[cfg(not(target_os = "android"))]
-fn main() {
+pub fn main() -> Result<()> {
     env_logger::builder()
         .filter_level(log::LevelFilter::Warn)
         .parse_default_env()
@@ -165,4 +175,22 @@

     let event_loop = EventLoopBuilder::with_user_event().build();
     _main(event_loop);
+
+    Ok(())
+}
+
+#[cfg(target_os = "android")]
+pub fn main() -> Result<()> {
+    use winit::platform::android::EventLoopBuilderExtAndroid;
+
+    android_logger::init_once(
+        android_logger::Config::default().with_max_level(log::LevelFilter::Warn),
+    );
+
+    let event_loop = EventLoopBuilder::with_user_event()
+        .with_android_app(app)
+        .build();
+    _main(event_loop);
+
+    Ok(())
 }

Changes were adding the pub on main and also adding start_app and safe_unwind parts. I also did the android_main as a main cfg varient but can't test that.

zeerooth commented 1 year ago

Not adding pub to the desktop main function is a stupid mistake on my part lol. It should definitely be there.

But your diff doesn't work on android because app is not found in the scope. We need to have a special android_main for android otherwise winit's android-activity won't bind to it correctly.

May I suggest this for lib.rs?

#[cfg(target_os = "android")]
use winit::platform::android::activity::AndroidApp;

use winit::event::Event::*;
use winit::event_loop::{ControlFlow, EventLoop, EventLoopBuilder, EventLoopWindowTarget};

use egui_wgpu::winit::Painter;
use egui_winit::State;

const INITIAL_WIDTH: u32 = 1920;
const INITIAL_HEIGHT: u32 = 1080;

/// A custom event type for the winit app.
enum Event {
    RequestRedraw,
}

/// Enable egui to request redraws via a custom Winit event...
#[derive(Clone)]
struct RepaintSignal(std::sync::Arc<std::sync::Mutex<winit::event_loop::EventLoopProxy<Event>>>);

fn create_window<T>(
    event_loop: &EventLoopWindowTarget<T>,
    state: &mut State,
    painter: &mut Painter,
) -> winit::window::Window {
    let window = winit::window::WindowBuilder::new()
        .with_decorations(true)
        .with_resizable(true)
        .with_transparent(false)
        .with_title("egui winit + wgpu example")
        .with_inner_size(winit::dpi::PhysicalSize {
            width: INITIAL_WIDTH,
            height: INITIAL_HEIGHT,
        })
        .build(event_loop)
        .unwrap();

    pollster::block_on(painter.set_window(Some(&window))).unwrap();

    // NB: calling set_window will lazily initialize render state which
    // means we will be able to query the maximum supported texture
    // dimensions
    if let Some(max_size) = painter.max_texture_side() {
        state.set_max_texture_side(max_size);
    }

    let pixels_per_point = window.scale_factor() as f32;
    state.set_pixels_per_point(pixels_per_point);

    window.request_redraw();

    window
}

fn _main(event_loop: EventLoop<Event>) {
    let ctx = egui::Context::default();
    let repaint_signal = RepaintSignal(std::sync::Arc::new(std::sync::Mutex::new(
        event_loop.create_proxy(),
    )));
    ctx.set_request_repaint_callback(move |_| {
        repaint_signal
            .0
            .lock()
            .unwrap()
            .send_event(Event::RequestRedraw)
            .ok();
    });

    let mut state = State::new(&event_loop);
    let mut painter = Painter::new(
        egui_wgpu::WgpuConfiguration::default(),
        1, // msaa samples
        None,
        false,
    );
    let mut window: Option<winit::window::Window> = None;
    let mut egui_demo_windows = egui_demo_lib::DemoWindows::default();

    event_loop.run(move |event, event_loop, control_flow| match event {
        Resumed => match window {
            None => {
                window = Some(create_window(event_loop, &mut state, &mut painter));
            }
            Some(ref window) => {
                pollster::block_on(painter.set_window(Some(window))).unwrap();
                window.request_redraw();
            }
        },
        Suspended => {
            window = None;
        }
        RedrawRequested(..) => {
            if let Some(window) = window.as_ref() {
                let raw_input = state.take_egui_input(window);

                let full_output = ctx.run(raw_input, |ctx| {
                    egui_demo_windows.ui(ctx);
                });
                state.handle_platform_output(window, &ctx, full_output.platform_output);

                painter.paint_and_update_textures(
                    state.pixels_per_point(),
                    egui::Rgba::default().to_array(),
                    &ctx.tessellate(full_output.shapes),
                    &full_output.textures_delta,
                    false,
                );

                if full_output.repaint_after.is_zero() {
                    window.request_redraw();
                }
            }
        }
        MainEventsCleared | UserEvent(Event::RequestRedraw) => {
            if let Some(window) = window.as_ref() {
                window.request_redraw();
            }
        }
        WindowEvent { event, .. } => {
            match event {
                winit::event::WindowEvent::Resized(size) => {
                    painter.on_window_resized(size.width, size.height);
                }
                winit::event::WindowEvent::CloseRequested => {
                    *control_flow = ControlFlow::Exit;
                }
                _ => {}
            }

            let response = state.on_event(&ctx, &event);
            if response.repaint {
                if let Some(window) = window.as_ref() {
                    window.request_redraw();
                }
            }
        }
        _ => (),
    });
}

#[cfg(any(target_os = "ios", target_os = "android"))]
fn stop_unwind<F: FnOnce() -> T, T>(f: F) -> T {
    match std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)) {
        Ok(t) => t,
        Err(err) => {
            eprintln!("attempt to unwind out of `rust` with err: {:?}", err);
            std::process::abort()
        }
    }
}

#[cfg(target_os = "ios")]
fn _start_app() {
    stop_unwind(|| main().unwrap());
}

#[no_mangle]
#[inline(never)]
#[cfg(target_os = "ios")]
pub extern "C" fn start_app() {
    _start_app();
}

#[cfg(not(target_os = "android"))]
pub fn main() -> Result<()> {
    env_logger::builder()
        .filter_level(log::LevelFilter::Warn)
        .parse_default_env()
        .init();

    let event_loop = EventLoopBuilder::with_user_event().build();
    _main(event_loop);

    Ok(())
}

#[allow(dead_code)]
#[cfg(target_os = "android")]
#[no_mangle]
fn android_main(app: AndroidApp) {
    use winit::platform::android::EventLoopBuilderExtAndroid;

    android_logger::init_once(
        android_logger::Config::default().with_max_level(log::LevelFilter::Warn),
    );

    let event_loop = EventLoopBuilder::with_user_event()
        .with_android_app(app)
        .build();
    stop_unwind(|| _main(event_loop));
}

Works on linux and android, but I can't test it on iOS or mac

QuantumEntangledAndy commented 1 year ago

Works on mac, just having trouble connecting ios at the moment. I think perhaps the 1s timeout is too short on ios-deploy, is there anyway to configure that?

Also you will need to remove -> Result<()> from main the Ok(()) too aswell as the .unwrap in the stop_unwind call

QuantumEntangledAndy commented 1 year ago

Also have you considered adding some checks on the github runners for macos at least? Could probably do ios too with some setup

QuantumEntangledAndy commented 1 year ago

Here's the final one that works with me. (macos and ios)

#[cfg(target_os = "android")]
use winit::platform::android::activity::AndroidApp;

use winit::event::Event::*;
use winit::event_loop::{ControlFlow, EventLoop, EventLoopBuilder, EventLoopWindowTarget};

use egui_wgpu::winit::Painter;
use egui_winit::State;

const INITIAL_WIDTH: u32 = 1920;
const INITIAL_HEIGHT: u32 = 1080;

/// A custom event type for the winit app.
enum Event {
    RequestRedraw,
}

/// Enable egui to request redraws via a custom Winit event...
#[derive(Clone)]
struct RepaintSignal(std::sync::Arc<std::sync::Mutex<winit::event_loop::EventLoopProxy<Event>>>);

fn create_window<T>(
    event_loop: &EventLoopWindowTarget<T>,
    state: &mut State,
    painter: &mut Painter,
) -> winit::window::Window {
    let window = winit::window::WindowBuilder::new()
        .with_decorations(true)
        .with_resizable(true)
        .with_transparent(false)
        .with_title("egui winit + wgpu example")
        .with_inner_size(winit::dpi::PhysicalSize {
            width: INITIAL_WIDTH,
            height: INITIAL_HEIGHT,
        })
        .build(event_loop)
        .unwrap();

    pollster::block_on(painter.set_window(Some(&window))).unwrap();

    // NB: calling set_window will lazily initialize render state which
    // means we will be able to query the maximum supported texture
    // dimensions
    if let Some(max_size) = painter.max_texture_side() {
        state.set_max_texture_side(max_size);
    }

    let pixels_per_point = window.scale_factor() as f32;
    state.set_pixels_per_point(pixels_per_point);

    window.request_redraw();

    window
}

fn _main(event_loop: EventLoop<Event>) {
    let ctx = egui::Context::default();
    let repaint_signal = RepaintSignal(std::sync::Arc::new(std::sync::Mutex::new(
        event_loop.create_proxy(),
    )));
    ctx.set_request_repaint_callback(move |_| {
        repaint_signal
            .0
            .lock()
            .unwrap()
            .send_event(Event::RequestRedraw)
            .ok();
    });

    let mut state = State::new(&event_loop);
    let mut painter = Painter::new(
        egui_wgpu::WgpuConfiguration::default(),
        1, // msaa samples
        None,
        false,
    );
    let mut window: Option<winit::window::Window> = None;
    let mut egui_demo_windows = egui_demo_lib::DemoWindows::default();

    event_loop.run(move |event, event_loop, control_flow| match event {
        Resumed => match window {
            None => {
                window = Some(create_window(event_loop, &mut state, &mut painter));
            }
            Some(ref window) => {
                pollster::block_on(painter.set_window(Some(window))).unwrap();
                window.request_redraw();
            }
        },
        Suspended => {
            window = None;
        }
        RedrawRequested(..) => {
            if let Some(window) = window.as_ref() {
                let raw_input = state.take_egui_input(window);

                let full_output = ctx.run(raw_input, |ctx| {
                    egui_demo_windows.ui(ctx);
                });
                state.handle_platform_output(window, &ctx, full_output.platform_output);

                painter.paint_and_update_textures(
                    state.pixels_per_point(),
                    egui::Rgba::default().to_array(),
                    &ctx.tessellate(full_output.shapes),
                    &full_output.textures_delta,
                    false,
                );

                if full_output.repaint_after.is_zero() {
                    window.request_redraw();
                }
            }
        }
        MainEventsCleared | UserEvent(Event::RequestRedraw) => {
            if let Some(window) = window.as_ref() {
                window.request_redraw();
            }
        }
        WindowEvent { event, .. } => {
            match event {
                winit::event::WindowEvent::Resized(size) => {
                    painter.on_window_resized(size.width, size.height);
                }
                winit::event::WindowEvent::CloseRequested => {
                    *control_flow = ControlFlow::Exit;
                }
                _ => {}
            }

            let response = state.on_event(&ctx, &event);
            if response.repaint {
                if let Some(window) = window.as_ref() {
                    window.request_redraw();
                }
            }
        }
        _ => (),
    });
}

#[cfg(any(target_os = "ios", target_os = "android"))]
fn stop_unwind<F: FnOnce() -> T, T>(f: F) -> T {
    match std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)) {
        Ok(t) => t,
        Err(err) => {
            eprintln!("attempt to unwind out of `rust` with err: {:?}", err);
            std::process::abort()
        }
    }
}

#[cfg(target_os = "ios")]
fn _start_app() {
    stop_unwind(|| main());
}

#[no_mangle]
#[inline(never)]
#[cfg(target_os = "ios")]
pub extern "C" fn start_app() {
    _start_app();
}

#[cfg(not(target_os = "android"))]
pub fn main() {
    env_logger::builder()
        .filter_level(log::LevelFilter::Warn)
        .parse_default_env()
        .init();

    let event_loop = EventLoopBuilder::with_user_event().build();
    _main(event_loop);
}

#[allow(dead_code)]
#[cfg(target_os = "android")]
#[no_mangle]
fn android_main(app: AndroidApp) {
    use winit::platform::android::EventLoopBuilderExtAndroid;

    android_logger::init_once(
        android_logger::Config::default().with_max_level(log::LevelFilter::Warn),
    );

    let event_loop = EventLoopBuilder::with_user_event()
        .with_android_app(app)
        .build();
    stop_unwind(|| _main(event_loop));
}
zeerooth commented 1 year ago

I can confirm that your version works great on Linux and Android! Nice :)

If you'd like you can can set up the PR to merge there changes into the dev branch, but I can also have it set up later.

Also have you considered adding some checks on the github runners for macos at least? Could probably do ios too with some setup

There actually is a runner for macOS, but it's only for unit tests and right now these tests don't check the templates at all afaik.

QuantumEntangledAndy commented 1 year ago

Might be easier if you just add it in. It's a small change for a PR

QuantumEntangledAndy commented 1 year ago

If you need any other ios/mac tests drop me a message and I will see if I can accomadate