diff --git a/Cargo.toml b/Cargo.toml index 83aa0142ce..cdb91bd7d9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -79,9 +79,9 @@ members = [ "crates/perry-stdlib-static", "docs/examples/_fixtures/native-libraries/my-bindings", ] -# Only build platform-independent crates by default. -# Platform-specific UI crates (perry-ui-macos, perry-ui-ios, etc.) must be built -# explicitly with -p on their target platform. +# Build only the host-independent product set by default. +# Platform UI manifests keep their source and dependencies behind target cfgs. +# Build each platform package explicitly for its applicable target. default-members = [ "crates/perry", "crates/perry-parser", diff --git a/changelog.d/6761-multiplatform-workspace.md b/changelog.d/6761-multiplatform-workspace.md new file mode 100644 index 0000000000..7f11b5fa27 --- /dev/null +++ b/changelog.d/6761-multiplatform-workspace.md @@ -0,0 +1 @@ +**Cross-platform workspace builds:** gate each platform UI crate and its native dependencies for the applicable Cargo target. Host workspace checks no longer require SDKs or system libraries for other platforms. Update the container Compose FFI for the current engine API. diff --git a/crates/perry-audio-miniaudio/Cargo.toml b/crates/perry-audio-miniaudio/Cargo.toml index 4afc5ef786..adf6750310 100644 --- a/crates/perry-audio-miniaudio/Cargo.toml +++ b/crates/perry-audio-miniaudio/Cargo.toml @@ -14,5 +14,5 @@ crate-type = ["rlib"] [build-dependencies] cc = "1" -[dependencies] +[target.'cfg(any(target_os = "linux", target_os = "windows", target_os = "android"))'.dependencies] libc.workspace = true diff --git a/crates/perry-audio-miniaudio/build.rs b/crates/perry-audio-miniaudio/build.rs index 6e9b05b56a..8eb58da374 100644 --- a/crates/perry-audio-miniaudio/build.rs +++ b/crates/perry-audio-miniaudio/build.rs @@ -7,10 +7,14 @@ fn main() { let target = env::var("TARGET").unwrap_or_default(); - // We compile miniaudio for every host, including Apple — Apple targets - // get the rlib but never link the symbols (perry-ui-macos owns the - // AVAudioEngine backend). Compiling here keeps `cargo check` / - // `cargo build -p perry-audio-miniaudio` clean on developer Macs. + let supported = + target.contains("linux") || target.contains("android") || target.contains("windows"); + if !supported { + return; + } + + // This backend supports Linux, Windows, and Android. Apple targets use + // the AVAudioEngine backend in their platform UI crates. let mut build = cc::Build::new(); build .file("vendor/miniaudio_impl.c") @@ -48,17 +52,6 @@ fn main() { // requirement explicit here so the rlib stands on its own. println!("cargo:rustc-link-lib=ole32"); println!("cargo:rustc-link-lib=winmm"); - } else if target.contains("apple") { - // Apple targets compile this crate (so the workspace builds - // cleanly on macOS developer machines) but should never link - // its symbols — perry-ui-macos provides the real backend - // backed by AVAudioEngine. Pull in the Core Audio frameworks - // anyway because miniaudio_impl.c references them in its - // Apple branch. - println!("cargo:rustc-link-lib=framework=CoreAudio"); - println!("cargo:rustc-link-lib=framework=AudioToolbox"); - println!("cargo:rustc-link-lib=framework=AudioUnit"); - println!("cargo:rustc-link-lib=framework=CoreFoundation"); } build.compile("miniaudio_impl"); diff --git a/crates/perry-audio-miniaudio/src/lib.rs b/crates/perry-audio-miniaudio/src/lib.rs index ab6b95fe07..56727dfcb2 100644 --- a/crates/perry-audio-miniaudio/src/lib.rs +++ b/crates/perry-audio-miniaudio/src/lib.rs @@ -15,6 +15,8 @@ //! PlaybackId 0x10000001..=0x1FFFFFFF — live ma_sound voice //! Bus 0x20000001..=0x2FFFFFFF — ma_sound_group (0 = master) +#![cfg(any(target_os = "linux", target_os = "windows", target_os = "android"))] + use libc::{c_char, c_float, c_int, c_uint, c_void}; use std::cell::RefCell; use std::ffi::CString; diff --git a/crates/perry-codegen/tests/i64_spec_ternary_recursion.rs b/crates/perry-codegen/tests/i64_spec_ternary_recursion.rs index 4626089591..68912e47a1 100644 --- a/crates/perry-codegen/tests/i64_spec_ternary_recursion.rs +++ b/crates/perry-codegen/tests/i64_spec_ternary_recursion.rs @@ -28,7 +28,6 @@ fn empty_opts() -> CompileOptions { verify_native_regions: false, disable_buffer_fast_path: false, namespace_imports: Vec::new(), - namespace_reexport_named_imports: std::collections::HashSet::new(), imported_classes: Vec::new(), imported_enums: Vec::new(), imported_async_funcs: std::collections::HashSet::new(), diff --git a/crates/perry-container-compose/src/ffi.rs b/crates/perry-container-compose/src/ffi.rs index f6e5995f61..2ce9d53e3e 100644 --- a/crates/perry-container-compose/src/ffi.rs +++ b/crates/perry-container-compose/src/ffi.rs @@ -68,11 +68,16 @@ fn parse_compose_file(file_ptr: *const StringHeader) -> Option { unsafe { string_from_header(file_ptr) }.map(PathBuf::from) } +fn parse_services_filter(raw: Option) -> Result, &'static str> { + match raw { + None => Ok(Vec::new()), + Some(raw) if raw.trim().is_empty() => Ok(Vec::new()), + Some(raw) => serde_json::from_str(&raw).map_err(|_| "services must be a JSON array"), + } +} + fn make_engine(files: Vec) -> Result, String> { - let config = crate::config::ProjectConfig { - files, - ..Default::default() - }; + let config = crate::config::ProjectConfig::new(files, None, Vec::new()); let proj = crate::project::ComposeProject::load(&config).map_err(|e| e.to_string())?; let backend: Arc = match block(crate::backend::detect_backend()) { @@ -145,20 +150,19 @@ pub unsafe extern "C" fn js_compose_logs( _follow: bool, ) -> *const StringHeader { let files: Vec = parse_compose_file(file_ptr).into_iter().collect(); - let service: Option = string_from_header(services_ptr) - .and_then(|s| serde_json::from_str::>(&s).ok()) - .and_then(|v| v.into_iter().next()); + let services = match parse_services_filter(string_from_header(services_ptr)) { + Ok(services) => services, + Err(message) => return json_err(message), + }; match make_engine(files) { Err(e) => json_err(&e), - Ok(engine) => match block(engine.logs(service.as_deref(), None)) { + Ok(engine) => match block(engine.logs(&services, None)) { Err(e) => json_err(&e.to_string()), - Ok(logs) => { - let stdout = logs.stdout.replace('"', "\\\"").replace('\n', "\\n"); - let stderr = logs.stderr.replace('"', "\\\"").replace('\n', "\\n"); - let payload = format!("{{\"stdout\":\"{}\",\"stderr\":\"{}\"}}", stdout, stderr); - json_ok(&payload) - } + Ok(logs) => match serde_json::to_string(&logs) { + Ok(payload) => json_ok(&payload), + Err(e) => json_err(&e.to_string()), + }, }, } } @@ -180,7 +184,7 @@ pub unsafe extern "C" fn js_compose_exec( match make_engine(files) { Err(e) => json_err(&e), - Ok(engine) => match block(engine.exec(&service, &cmd)) { + Ok(engine) => match block(engine.exec(&service, &cmd, None, None)) { Err(e) => json_err(&e.to_string()), Ok(result) => { let stdout = result.stdout.replace('"', "\\\"").replace('\n', "\\n"); @@ -195,10 +199,7 @@ pub unsafe extern "C" fn js_compose_exec( #[no_mangle] pub unsafe extern "C" fn js_compose_config(file_ptr: *const StringHeader) -> *const StringHeader { let files: Vec = parse_compose_file(file_ptr).into_iter().collect(); - let config = crate::config::ProjectConfig { - files, - ..Default::default() - }; + let config = crate::config::ProjectConfig::new(files, None, Vec::new()); match crate::project::ComposeProject::load(&config) { Err(e) => json_err(&e.to_string()), Ok(proj) => { @@ -208,3 +209,37 @@ pub unsafe extern "C" fn js_compose_config(file_ptr: *const StringHeader) -> *co } } } + +#[cfg(test)] +mod tests { + use super::parse_services_filter; + + #[test] + fn services_filter_accepts_absent_and_empty_values() { + assert_eq!(parse_services_filter(None), Ok(Vec::new())); + assert_eq!( + parse_services_filter(Some(" ".to_string())), + Ok(Vec::new()) + ); + } + + #[test] + fn services_filter_accepts_a_string_array() { + assert_eq!( + parse_services_filter(Some(r#"["api","db"]"#.to_string())), + Ok(vec!["api".to_string(), "db".to_string()]) + ); + } + + #[test] + fn services_filter_rejects_invalid_or_non_array_json() { + assert_eq!( + parse_services_filter(Some("not-json".to_string())), + Err("services must be a JSON array") + ); + assert_eq!( + parse_services_filter(Some(r#"{"service":"api"}"#.to_string())), + Err("services must be a JSON array") + ); + } +} diff --git a/crates/perry-ui-android/Cargo.toml b/crates/perry-ui-android/Cargo.toml index 0d1d0155f0..64925df494 100644 --- a/crates/perry-ui-android/Cargo.toml +++ b/crates/perry-ui-android/Cargo.toml @@ -14,7 +14,7 @@ crate-type = ["rlib", "staticlib"] [features] geisterhand = [] -[dependencies] +[target.'cfg(target_os = "android")'.dependencies] perry-ui = { path = "../perry-ui" } perry-audio-miniaudio = { workspace = true } perry-runtime = { path = "../perry-runtime", default-features = false } diff --git a/crates/perry-ui-android/src/lib.rs b/crates/perry-ui-android/src/lib.rs index a629c560eb..f3f0b140f5 100644 --- a/crates/perry-ui-android/src/lib.rs +++ b/crates/perry-ui-android/src/lib.rs @@ -1,3 +1,5 @@ +#![cfg(target_os = "android")] + // Issue #552: force libperry_ui_android.a to bundle perry-ext-sharp's // `js_sharp_*` symbols (resize / jpeg / toBuffer / etc). Without this `extern // crate` reference, the rlib dep would be optimized out and the stubs in diff --git a/crates/perry-ui-gtk4/Cargo.toml b/crates/perry-ui-gtk4/Cargo.toml index ac53473d39..53c2956a4f 100644 --- a/crates/perry-ui-gtk4/Cargo.toml +++ b/crates/perry-ui-gtk4/Cargo.toml @@ -10,7 +10,7 @@ workspace = true [lib] crate-type = ["rlib", "staticlib"] -[dependencies] +[target.'cfg(target_os = "linux")'.dependencies] perry-ui = { path = "../perry-ui" } perry-ui-testkit = { workspace = true } perry-audio-miniaudio = { workspace = true } @@ -44,7 +44,6 @@ gstreamer-app = "0.25" # Bluetooth headphones (#366). Linux-only; zbus has Linux-specific # system deps that don't build on macOS / Windows hosts, so the crate # stays gated behind cfg(target_os = "linux") in the source. -[target.'cfg(target_os = "linux")'.dependencies] mpris-server = { version = "0.10", features = ["tokio"] } tokio = { version = "1", features = ["rt", "sync", "time", "net", "io-util"] } # perry/ui — system tray icon support via the StatusNotifierItem diff --git a/crates/perry-ui-gtk4/src/lib.rs b/crates/perry-ui-gtk4/src/lib.rs index c74e519595..5b5fa8f9b0 100644 --- a/crates/perry-ui-gtk4/src/lib.rs +++ b/crates/perry-ui-gtk4/src/lib.rs @@ -1,3 +1,5 @@ +#![cfg(target_os = "linux")] + pub mod app; pub mod audio; pub mod audio_playback; diff --git a/crates/perry-ui-ios/Cargo.toml b/crates/perry-ui-ios/Cargo.toml index ca79d373b8..f1c4e7a27e 100644 --- a/crates/perry-ui-ios/Cargo.toml +++ b/crates/perry-ui-ios/Cargo.toml @@ -15,7 +15,7 @@ default = [] plugins = [] geisterhand = [] -[dependencies] +[target.'cfg(target_os = "ios")'.dependencies] perry-ui = { path = "../perry-ui" } perry-ui-testkit = { workspace = true } perry-runtime = { path = "../perry-runtime", default-features = false } diff --git a/crates/perry-ui-ios/src/lib.rs b/crates/perry-ui-ios/src/lib.rs index 806e701d37..b37b4d4c59 100644 --- a/crates/perry-ui-ios/src/lib.rs +++ b/crates/perry-ui-ios/src/lib.rs @@ -1,3 +1,5 @@ +#![cfg(target_os = "ios")] + pub mod app; pub mod audio; pub mod audio_playback; diff --git a/crates/perry-ui-macos/Cargo.toml b/crates/perry-ui-macos/Cargo.toml index 7b4e5e8156..064df363e5 100644 --- a/crates/perry-ui-macos/Cargo.toml +++ b/crates/perry-ui-macos/Cargo.toml @@ -15,7 +15,7 @@ default = [] plugins = [] geisterhand = [] -[dependencies] +[target.'cfg(target_os = "macos")'.dependencies] perry-ui = { path = "../perry-ui" } perry-ui-testkit = { workspace = true } base64.workspace = true diff --git a/crates/perry-ui-macos/src/lib.rs b/crates/perry-ui-macos/src/lib.rs index fb002e19f2..80c9d1dfca 100644 --- a/crates/perry-ui-macos/src/lib.rs +++ b/crates/perry-ui-macos/src/lib.rs @@ -1,3 +1,5 @@ +#![cfg(target_os = "macos")] + pub mod app; pub mod audio; pub mod audio_playback; diff --git a/crates/perry-ui-tvos/Cargo.toml b/crates/perry-ui-tvos/Cargo.toml index f5598a3795..38c9603a51 100644 --- a/crates/perry-ui-tvos/Cargo.toml +++ b/crates/perry-ui-tvos/Cargo.toml @@ -15,7 +15,7 @@ default = [] plugins = [] geisterhand = [] -[dependencies] +[target.'cfg(target_os = "tvos")'.dependencies] perry-ui = { path = "../perry-ui" } perry-ui-testkit = { workspace = true } perry-runtime = { path = "../perry-runtime", default-features = false } diff --git a/crates/perry-ui-tvos/src/lib.rs b/crates/perry-ui-tvos/src/lib.rs index 3c2e2d9128..af3129f881 100644 --- a/crates/perry-ui-tvos/src/lib.rs +++ b/crates/perry-ui-tvos/src/lib.rs @@ -1,3 +1,5 @@ +#![cfg(target_os = "tvos")] + pub mod app; pub mod audio; pub mod audio_playback; diff --git a/crates/perry-ui-visionos/Cargo.toml b/crates/perry-ui-visionos/Cargo.toml index 3ade880878..10eb3b81d2 100644 --- a/crates/perry-ui-visionos/Cargo.toml +++ b/crates/perry-ui-visionos/Cargo.toml @@ -15,7 +15,7 @@ default = [] plugins = [] geisterhand = [] -[dependencies] +[target.'cfg(target_os = "visionos")'.dependencies] perry-ui = { path = "../perry-ui" } perry-ui-testkit = { workspace = true } perry-runtime = { path = "../perry-runtime", default-features = false } diff --git a/crates/perry-ui-visionos/src/lib.rs b/crates/perry-ui-visionos/src/lib.rs index c123fb29e3..975ecb1c77 100644 --- a/crates/perry-ui-visionos/src/lib.rs +++ b/crates/perry-ui-visionos/src/lib.rs @@ -1,3 +1,5 @@ +#![cfg(target_os = "visionos")] + pub mod app; pub mod audio; pub mod audio_playback; diff --git a/crates/perry-ui-watchos/Cargo.toml b/crates/perry-ui-watchos/Cargo.toml index 3f39e30826..fc6162a208 100644 --- a/crates/perry-ui-watchos/Cargo.toml +++ b/crates/perry-ui-watchos/Cargo.toml @@ -15,7 +15,7 @@ default = [] plugins = [] geisterhand = [] -[dependencies] +[target.'cfg(target_os = "watchos")'.dependencies] perry-ui = { path = "../perry-ui" } perry-ui-testkit = { workspace = true } perry-runtime = { path = "../perry-runtime", default-features = false } diff --git a/crates/perry-ui-watchos/src/lib.rs b/crates/perry-ui-watchos/src/lib.rs index f62984941a..e244d43d6c 100644 --- a/crates/perry-ui-watchos/src/lib.rs +++ b/crates/perry-ui-watchos/src/lib.rs @@ -4,6 +4,8 @@ //! The fixed PerryWatchApp.swift queries this tree via FFI and renders //! it as SwiftUI views reactively. +#![cfg(target_os = "watchos")] + pub mod app; pub mod audio; pub mod audio_playback; diff --git a/crates/perry-ui-windows-winui/Cargo.toml b/crates/perry-ui-windows-winui/Cargo.toml index d905c7242b..33f01326cd 100644 --- a/crates/perry-ui-windows-winui/Cargo.toml +++ b/crates/perry-ui-windows-winui/Cargo.toml @@ -22,7 +22,7 @@ workspace = true # symbols), so no symbol is lost by going through this crate. crate-type = ["rlib", "staticlib"] -[dependencies] +[target.'cfg(target_os = "windows")'.dependencies] # The entire current Windows backend. Re-exported wholesale (see src/lib.rs). perry-ui-windows = { path = "../perry-ui-windows" } diff --git a/crates/perry-ui-windows-winui/src/lib.rs b/crates/perry-ui-windows-winui/src/lib.rs index 5723596792..b165c45a11 100644 --- a/crates/perry-ui-windows-winui/src/lib.rs +++ b/crates/perry-ui-windows-winui/src/lib.rs @@ -47,6 +47,8 @@ //! Tracks discussion #3486. Near-term Win32 polish on the default target lives //! in #4681. +#![cfg(target_os = "windows")] + // Re-export the entire Win32 backend. For Rust consumers this exposes the same // module API as `perry-ui-windows`; for the C ABI it is a no-op (the // `#[no_mangle]` entry points are emitted when `perry-ui-windows` is compiled diff --git a/crates/perry-ui-windows/Cargo.toml b/crates/perry-ui-windows/Cargo.toml index dbd9f75959..c449d5d97c 100644 --- a/crates/perry-ui-windows/Cargo.toml +++ b/crates/perry-ui-windows/Cargo.toml @@ -10,7 +10,7 @@ workspace = true [lib] crate-type = ["rlib", "staticlib"] -[dependencies] +[target.'cfg(target_os = "windows")'.dependencies] perry-ui = { path = "../perry-ui" } perry-ui-testkit = { workspace = true } perry-audio-miniaudio = { workspace = true } @@ -35,10 +35,6 @@ qrcodegen = "1.8" # (a version skew here is what broke the Windows build at v0.5.1180). webview2-com = "=0.39" -[features] -geisterhand = [] - -[target.'cfg(target_os = "windows")'.dependencies] # The `#[implement]` macro from `windows` expands to absolute # `::windows_core::` paths, so the consuming crate must depend on # `windows-core` directly or the COM authoring in drag_drop.rs fails to @@ -92,3 +88,6 @@ windows = { version = "0.62", features = [ # GdipLoadImageFromStream on the main thread during WM_PAINT. "Win32_Networking_WinHttp", ] } + +[features] +geisterhand = [] diff --git a/crates/perry-ui-windows/src/lib.rs b/crates/perry-ui-windows/src/lib.rs index c232572afe..d941acad13 100644 --- a/crates/perry-ui-windows/src/lib.rs +++ b/crates/perry-ui-windows/src/lib.rs @@ -1,3 +1,5 @@ +#![cfg(target_os = "windows")] + pub mod app; pub mod audio; pub mod audio_playback;