From dc0620ebd42a69c26e460c01ff314e2f25757bd3 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Mon, 12 Oct 2026 16:28:59 +0200 Subject: [PATCH] Build the winit example for the web and push to it in Chrome in CI --- .github/workflows/ci.yml | 32 +++++++++++++++ examples/winit/.gitignore | 2 + examples/winit/Cargo.lock | 3 ++ examples/winit/Cargo.toml | 9 +++++ examples/winit/Trunk.toml | 13 +++++++ examples/winit/build.rs | 11 ++++++ examples/winit/index.html | 20 ++++++++++ examples/winit/src/app.rs | 79 +++++++++++++++++++++++++++++++++++--- examples/winit/src/main.rs | 27 ++++++++++++- examples/winit/sw.js | 5 +++ 10 files changed, 194 insertions(+), 7 deletions(-) create mode 100644 examples/winit/Trunk.toml create mode 100644 examples/winit/build.rs create mode 100644 examples/winit/index.html create mode 100644 examples/winit/sw.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4191252..0db7898 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -552,6 +552,38 @@ jobs: - name: The static-worker build in Chrome, with real pushes run: node ci/web/example.mjs check examples/yew/dist-static "$RUNNER_TEMP/vapid.json" '/pushups-sw\.js\?pushups=static' + example-winit-web: + name: Example winit app on Trunk in Chrome + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v7 + - run: | + rustup toolchain install stable --profile minimal + rustup target add --toolchain stable wasm32-unknown-unknown + - uses: taiki-e/install-action@v2 + with: + tool: trunk@0.21.14 + - uses: actions/setup-node@v7 + with: + node-version: "24" + cache: npm + cache-dependency-path: ci/web/package-lock.json + - run: npm ci + working-directory: ci/web + - name: Build the winit app in both worker modes + working-directory: examples/winit + run: | + key=$(node ../../ci/web/example.mjs keys "$RUNNER_TEMP/vapid.json") + PUSHUPS_VAPID_PUBLIC_KEY="$key" trunk build --release --dist dist-rust + PUSHUPS_WEB_WORKER=static PUSHUPS_VAPID_PUBLIC_KEY="$key" trunk build --release --dist dist-static + # `build.rs` wrote `pushups::SERVICE_WORKER`, which the Trunk hook copied. + cmp dist-static/pushups-sw.js ../../js/pushups-sw.js + - name: The Rust-handler build in Chrome, with real pushes + run: node ci/web/example.mjs check examples/winit/dist-rust "$RUNNER_TEMP/vapid.json" '/sw\.js\?pushups=rust' --rust + - name: The static-worker build in Chrome, with real pushes + run: node ci/web/example.mjs check examples/winit/dist-static "$RUNNER_TEMP/vapid.json" '/pushups-sw\.js\?pushups=static' + example-leptos: name: Example Leptos SSR app in Chrome runs-on: ubuntu-latest diff --git a/examples/winit/.gitignore b/examples/winit/.gitignore index 949be0c..ffc490a 100644 --- a/examples/winit/.gitignore +++ b/examples/winit/.gitignore @@ -4,3 +4,5 @@ /android/app/build /android/app/src/main/jniLibs /android/local.properties +/dist* +/pushups-sw.js diff --git a/examples/winit/Cargo.lock b/examples/winit/Cargo.lock index 4684c69..662bbe5 100644 --- a/examples/winit/Cargo.lock +++ b/examples/winit/Cargo.lock @@ -1459,6 +1459,9 @@ dependencies = [ "futures-executor", "pushups", "serde_json", + "wasm-bindgen-futures", + "web-sys", + "web-time", "winit", ] diff --git a/examples/winit/Cargo.toml b/examples/winit/Cargo.toml index 6346ee3..bed315d 100644 --- a/examples/winit/Cargo.toml +++ b/examples/winit/Cargo.toml @@ -20,9 +20,18 @@ winit = "=0.31.0-beta.3" [target.'cfg(target_os = "android")'.dependencies] winit = { version = "=0.31.0-beta.3", features = ["android-native-activity"] } +# The web build, which `Trunk` starts from `index.html` and which `build.rs` writes its worker beside. +[target.'cfg(target_arch = "wasm32")'.dependencies] +wasm-bindgen-futures = "0.4" +web-sys = { version = "0.3", features = ["console"] } +web-time = "1" [workspace] +# The build script writes the static worker from `pushups::SERVICE_WORKER` for Trunk to copy. +[build-dependencies] +pushups = { path = "../.." } + # winit's Android loop ignores `Destroy` and allows one loop per process, so a relaunched Activity # freezes its main thread (winit#4303). The fix's PR head, until a winit release carries it. [patch.crates-io] diff --git a/examples/winit/Trunk.toml b/examples/winit/Trunk.toml new file mode 100644 index 0000000..7e6ff56 --- /dev/null +++ b/examples/winit/Trunk.toml @@ -0,0 +1,13 @@ +[build] +# `sw.js` imports the glue by name, so its file name carries no hash. +filehash = false + +[watch] +# The build script writes it, so watching it would rebuild in a loop. +ignore = ["pushups-sw.js"] + +# Runs after the build script wrote the worker, into the directory Trunk turns into `dist`. +[[hooks]] +stage = "post_build" +command = "sh" +command_arguments = ["-c", "cp pushups-sw.js \"$TRUNK_STAGING_DIR/pushups-sw.js\""] diff --git a/examples/winit/build.rs b/examples/winit/build.rs new file mode 100644 index 0000000..43dc8db --- /dev/null +++ b/examples/winit/build.rs @@ -0,0 +1,11 @@ +//! Writes the static service worker beside the manifest, where the Trunk hook in `Trunk.toml` +//! copies it into the site. + +fn main() { + let dir = std::env::var_os("CARGO_MANIFEST_DIR").expect("cargo sets CARGO_MANIFEST_DIR"); + let path = std::path::Path::new(&dir).join("pushups-sw.js"); + // Rewriting an unchanged file would wake `trunk serve`'s watcher for nothing. + if std::fs::read_to_string(&path).ok().as_deref() != Some(pushups::SERVICE_WORKER) { + std::fs::write(&path, pushups::SERVICE_WORKER).expect("the worker is written"); + } +} diff --git a/examples/winit/index.html b/examples/winit/index.html new file mode 100644 index 0000000..85a322c --- /dev/null +++ b/examples/winit/index.html @@ -0,0 +1,20 @@ + + + + pushups winit example + + + + + + + +

pushups winit example

+ + + + diff --git a/examples/winit/src/app.rs b/examples/winit/src/app.rs index fa11208..464c4c3 100644 --- a/examples/winit/src/app.rs +++ b/examples/winit/src/app.rs @@ -1,10 +1,19 @@ -//! The app both entry points run, `main` on desktop and iOS and `android_main` on Android. +//! The app both entry points run, `main` on desktop, iOS and the web and `android_main` on Android. use std::sync::mpsc::{Receiver, channel}; +#[cfg(not(target_arch = "wasm32"))] +use std::time::{SystemTime, UNIX_EPOCH}; use base64::Engine as _; use base64::engine::general_purpose::URL_SAFE_NO_PAD; -use pushups::{Config, Context, Event, LinuxConfig, Message, UnifiedPushConfig, WnsConfig}; +use pushups::{Config, Context, Event, Message}; + +// The Linux and Windows sub-configs, unused on the web, which takes its key for `WebPushConfig` instead. +#[cfg(not(target_arch = "wasm32"))] +use pushups::{LinuxConfig, UnifiedPushConfig, WnsConfig}; +// `std`'s clock panics on `wasm32-unknown-unknown`, where `web-time` reads `Date.now()` instead. +#[cfg(target_arch = "wasm32")] +use web_time::{SystemTime, UNIX_EPOCH}; use winit::application::ApplicationHandler; use winit::event::WindowEvent; use winit::event_loop::{ActiveEventLoop, EventLoop}; @@ -18,6 +27,7 @@ pub(crate) fn install() { } } +#[cfg(not(target_arch = "wasm32"))] /// The app's configuration from build-time keys, `PUSHUPS_VAPID_PUBLIC_KEY` (base64url) for UnifiedPush on Linux and Android and `PUSHUPS_WNS_REMOTE_ID` for Windows. fn config() -> Config { // The bundle identifier, which also names the app on the Linux session bus. @@ -34,6 +44,23 @@ fn config() -> Config { config } +/// The web configuration from `PUSHUPS_VAPID_PUBLIC_KEY` (base64url). `PUSHUPS_WEB_WORKER=static` registers the static worker, which `build.rs` writes from `pushups::SERVICE_WORKER`, in place of the Rust handler, which runs from the `sw.js` entry. +#[cfg(target_arch = "wasm32")] +fn config() -> Config { + let key = option_env!("PUSHUPS_VAPID_PUBLIC_KEY") + .and_then(|key| URL_SAFE_NO_PAD.decode(key).ok()) + .and_then(|key| <[u8; 65]>::try_from(key).ok()); + let Some(key) = key else { + return Config::new(); + }; + let web = pushups::WebPushConfig::new(key); + Config::new().web(match option_env!("PUSHUPS_WEB_WORKER") { + Some("static") => web, + _ => web.rust_handler().service_worker_path("/sw.js"), + }) +} + +#[cfg(not(target_arch = "wasm32"))] /// Registers, asks for permission, and runs the event loop until the app exits. pub(crate) fn run(event_loop: EventLoop) -> Result<(), Box> { let proxy = event_loop.create_proxy(); @@ -56,6 +83,29 @@ pub(crate) fn run(event_loop: EventLoop) -> Result<(), Box Result<(), Box> { + let proxy = event_loop.create_proxy(); + let (sender, events) = channel(); + pushups::set_handler(move |event| { + // A closed channel means the app is exiting. + let _ = sender.send(event); + proxy.wake_up(); + }); + if let Err(error) = pushups::register() { + log(&format!("register failed: {error}")); + } + wasm_bindgen_futures::spawn_local(async move { + let answer = pushups::request_permission().await; + log(&format!("permission {answer:?}")); + }); + Ok(event_loop.run_app(App { + window: None, + events, + })?) +} + struct App { window: Option>, events: Receiver, @@ -128,6 +178,17 @@ fn on_push(_context: Context, message: Message) { )); } +/// Runs in the service worker for every push, and decides what the user sees. +#[cfg(target_arch = "wasm32")] +pub(crate) async fn notification_for(message: Message) -> pushups::Notification { + let at = now_ms(); + let line = timing(&message, at); + log(&format!("worker push at_ms={at} {line}")); + pushups::Notification::new("pushups from Rust") + .body(format!("pushups winit example, {line}")) + .navigate("/") +} + /// The push's `seq` and its latency from the sender's `sent_at_ms` to `at`, for a JSON payload /// such as an FCM data map. fn timing(message: &Message, at: u128) -> String { @@ -146,14 +207,14 @@ fn timing(message: &Message, at: u128) -> String { } fn now_ms() -> u128 { - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) + SystemTime::now() + .duration_since(UNIX_EPOCH) .map_or(0, |since| since.as_millis()) } /// Prints the line, and appends it to `pushups-example.log` in the temporary directory, which outlives a process the system launched with nothing reading its output. -#[cfg(not(target_os = "android"))] -fn log(line: &str) { +#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))] +pub(crate) fn log(line: &str) { use std::io::Write; let line = format!("pushups-winit-example {line}"); @@ -169,6 +230,12 @@ fn log(line: &str) { } } +/// Logs the line to the console, which `ci/web/example.mjs` reads with the page's lines. +#[cfg(target_arch = "wasm32")] +pub(crate) fn log(line: &str) { + web_sys::console::log_1(&format!("pushups-winit-example {line}").into()); +} + /// Writes the line to logcat under the tag `pushups-example`, which `ci/android/example.sh` reads. #[cfg(target_os = "android")] fn log(line: &str) { diff --git a/examples/winit/src/main.rs b/examples/winit/src/main.rs index 7ee3159..4175dfd 100644 --- a/examples/winit/src/main.rs +++ b/examples/winit/src/main.rs @@ -1,8 +1,33 @@ -//! Receives pushes through `pushups` under winit 0.31, which installs no app delegate on Apple, and logs every step with its wall-clock time to standard output and to `pushups-example.log` in the temporary directory. +//! Receives pushes through `pushups` under winit 0.31, which installs no app delegate on Apple, +//! and logs every step with its wall-clock time to standard output and to `pushups-example.log` +//! in the temporary directory, or to the console on the web. mod app; +#[cfg(not(target_arch = "wasm32"))] fn main() -> Result<(), Box> { app::install(); app::run(winit::event_loop::EventLoop::new()?) } + +/// The web entry, which Trunk builds from this target. In the service worker it serves the Rust +/// handler, and in the page it installs pushups and runs the winit event loop. +#[cfg(target_arch = "wasm32")] +fn main() { + if pushups::in_service_worker() { + if let Err(error) = pushups::serve_service_worker(app::notification_for) { + app::log(&format!("serving the service worker failed: {error}")); + } + return; + } + app::install(); + match winit::event_loop::EventLoop::new() { + // The event loop does not block on the web, and keeps running after `main` returns. + Ok(event_loop) => { + if let Err(error) = app::run(event_loop) { + app::log(&format!("event loop failed: {error}")); + } + } + Err(error) => app::log(&format!("event loop failed: {error}")), + } +} diff --git a/examples/winit/sw.js b/examples/winit/sw.js new file mode 100644 index 0000000..84a4cd2 --- /dev/null +++ b/examples/winit/sw.js @@ -0,0 +1,5 @@ +// The service worker under `rust_handler`. Trunk starts the wasm only in the page, so this module +// starts it in the worker, where the app's `main` serves its Rust handler. +import init from "./pushups-winit-example.js"; + +init();