Port Tauri Android build to Bazel, cross-compile macOS x64, etc. #54
Port Tauri Android build to Bazel, cross-compile macOS x64, etc. #54MulverineX wants to merge 40 commits intoMoosync:bazelfrom
Conversation
…ed on accident before the correct gitignore was added
- Use anonymous object implementing Action<ExecSpec> instead of lambda - KLS can now properly infer types from the execute() method signature - Full completion support for spec.workingDir, spec.executable, etc. - Also adds kotlin-gradle-plugin to buildSrc dependencies for completeness
… the linux x64 artifact handling above the Android CI
Extracts macOS SDK from Xcode on a macOS runner and uploads it to a GitHub pre-release for use by Linux cross-compilation. Developed with assistance from Claude Opus 4.5
The Android build introduces non-deterministic outputs (likely from tauri-codegen HashMap iteration order), causing unnecessary cache re-uploads. Disable re-uploads for this job while still allowing initial cache population. Developed with assistance from Claude Opus 4.5
Users typically only need one format, not all three. Developed with assistance from Claude Opus 4.5
…orms other than Linux x86_64
Reviewer's GuidePorts Android builds to Bazel-backed native library + Gradle APK pipeline, adds Linux-based macOS x64 cross-compilation with apple-cross toolchain and foreign_cc patches, introduces platform/target-OS aware build settings and toolchains, and improves rodio audio handling to use device-native sample rates (with PulseAudio integration) while adding mobile-specific stubs and Tauri Android integration for plugins and mpris. Sequence diagram for Bazel-backed Android APK build pipelinesequenceDiagram
actor Dev
participant GH as GitHubActions
participant Bazel as Bazel//tauri:moosync_android
participant Gradle as GradleAndroidProject
participant BuildTask as BuildTask(Gradle)
participant RustPlugin as RustPlugin(Gradle)
Dev->>GH: Trigger build.yaml (ubuntu-22.04)
GH->>Bazel: bazel build //tauri:moosync_android --platforms=//toolchains/android:aarch64,x86_64 --//tools:target_os=android
Bazel-->>GH: Produce libapp_lib.so per ABI and UI dist assets
GH->>Gradle: Setup Java 17
GH->>Gradle: ./gradlew assembleArm64Debug assembleX86_64Debug
Gradle->>RustPlugin: apply plugin RustPlugin
RustPlugin->>RustPlugin: Determine abiList, archList, targetList
Gradle->>BuildTask: assemble for each target (aarch64,x86_64)
BuildTask->>BuildTask: Read rootDirRel, target, release
BuildTask->>BuildTask: Map target to platform, abi
BuildTask->>BuildTask: Locate bazel-bin/tauri/libapp_lib.so
BuildTask->>BuildTask: Copy to src/main/jniLibs/<abi>/libapp_lib.so
BuildTask->>BuildTask: Copy UI assets from bazel-bin/ui/gen_dist/moosync_dist to src/main/assets (once)
BuildTask->>BuildTask: Generate Kotlin files from tauri/wry crates into src/main/java/app/moosync/moosync (once)
BuildTask->>BuildTask: Copy proguard-tauri.pro from tauri crate (once)
BuildTask-->>Gradle: Native libs and assets ready
Gradle->>Gradle: Package APKs (arm64,x86_64)
Gradle-->>GH: APK files under app/build/outputs/apk/*/debug
GH->>GH: Upload artifacts moosync-android-arm64, moosync-android-x86_64
Class diagram for Rodio audio pipeline with PulseAudio integrationclassDiagram
class RodioPlayer {
-Sender~RodioCommand~ tx
-Arc~Mutex~Receiver~PlayerEvent~~ events_rx
+new() RodioPlayer
-initialize(events_tx Sender~PlayerEvent~) Sender~RodioCommand~
-set_src(src String, sink Arc~rodio.Player~, output_sample_rate u32) Result~()~
}
class RodioCommand {
<<enum>>
Load(String)
Play
Pause
Stop
Seek(f64)
SetVolume(f32)
}
class PlayerEvent {
<<enum>>
Loading(bool)
TimeUpdate(f64)
Error(String)
}
class FFMPEGDecoder {
-format_ctx AVFormatContextInput
-stream_idx usize
-codec_ctx AVCodecContext
-swr_ctx SwrContext
-current_frame Vec~u8~
-requested_seek_timestamp i64
-output_sample_rate u32
+open(path &str, output_sample_rate u32) Result~FFMPEGDecoder~
-initialize_swr_context(codec_ctx &AVCodecContext, output_sample_rate i32) Result~SwrContext~
-decode_frame(frame &AVFrame) Result~()~
}
class Source {
<<trait>>
+channels() ChannelCount
+sample_rate() SampleRate
}
class PulseEvent {
<<enum>>
SampleRateDetected(u32)
SampleRateChanged(old u32, new u32)
DefaultSinkChanged(String)
Error(String)
}
class pulse_monitor {
+start_pulse_monitor() Receiver~PulseEvent~
+get_default_sample_rate() Option~u32~
}
class rodio_cpal_helpers {
+get_system_sample_rate() u32
+get_cpal_default_sample_rate() u32
}
RodioPlayer o-- RodioCommand
RodioPlayer o-- PlayerEvent
RodioPlayer --> FFMPEGDecoder : uses
FFMPEGDecoder ..|> Source
rodio_cpal_helpers --> pulse_monitor : calls
RodioPlayer --> rodio_cpal_helpers : uses for sample rate
PulseEvent <.. pulse_monitor
Class diagram for Tauri rodio desktop vs mobile audio commandsclassDiagram
class RodioModuleDesktop {
<<tauri module>>
+get_rodio_state(app AppHandle) RodioPlayer
+rodio_load(app AppHandle, src String) Result~()~
+rodio_play(app AppHandle) Result~()~
+rodio_pause(app AppHandle) Result~()~
+rodio_stop(app AppHandle) Result~()~
+rodio_seek(app AppHandle, pos f64) Result~()~
+rodio_set_volume(app AppHandle, volume f32) Result~()~
+rodio_get_volume(app AppHandle) Result~f32~
}
class RodioModuleMobile {
<<tauri module>>
+RodioPlayerStub
+get_rodio_state(app AppHandle) RodioPlayerStub
+rodio_load(app AppHandle, src String) Result~()~
+rodio_play(app AppHandle) Result~()~
+rodio_pause(app AppHandle) Result~()~
+rodio_stop(app AppHandle) Result~()~
+rodio_seek(app AppHandle, pos f64) Result~()~
+rodio_set_volume(app AppHandle, volume f32) Result~()~
+rodio_get_volume(app AppHandle) Result~f32~
}
class RodioPlayerStub {
<<struct>>
}
class RodioPlayer {
<<struct>>
+new() RodioPlayer
}
class tauri_plugin_audioplayer {
<<plugin>>
+AudioplayerExt
+MprisPlayerDetails
}
RodioModuleDesktop --> RodioPlayer : desktop
RodioModuleMobile --> RodioPlayerStub : mobile stub
RodioModuleMobile --> tauri_plugin_audioplayer : audio handled by plugin
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've found 3 issues, and left some high level feedback:
- There are multiple
eprintln!debug prints added in the audio path (FFMPEGDecoder and RodioPlayer, e.g.>>> RODIO PLAYER BUILD TEST 1 <<<and channel/sample_rate logs); consider gating these behind a feature flag, log level, ortracingso they don’t spam output in normal runs. - The macOS cross-compile setup hardcodes specific Xcode/SDK paths and versions (e.g.
MacOSX15.5.sdk,Xcode.app/Contents/Developerin several BUILD files and shell wrappers); it would be more robust to centralize this in one place or derive it from a small set of configurable variables to ease upgrades and local deviations. - The Gradle settings for locating
bazelOutputBaseandtauri-androidare duplicated across multiplesettings.gradlefiles (app and several plugins); consider extracting this discovery logic into a shared Gradle script or function to avoid divergence and simplify future changes.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- There are multiple `eprintln!` debug prints added in the audio path (FFMPEGDecoder and RodioPlayer, e.g. `>>> RODIO PLAYER BUILD TEST 1 <<<` and channel/sample_rate logs); consider gating these behind a feature flag, log level, or `tracing` so they don’t spam output in normal runs.
- The macOS cross-compile setup hardcodes specific Xcode/SDK paths and versions (e.g. `MacOSX15.5.sdk`, `Xcode.app/Contents/Developer` in several BUILD files and shell wrappers); it would be more robust to centralize this in one place or derive it from a small set of configurable variables to ease upgrades and local deviations.
- The Gradle settings for locating `bazelOutputBase` and `tauri-android` are duplicated across multiple `settings.gradle` files (app and several plugins); consider extracting this discovery logic into a shared Gradle script or function to avoid divergence and simplify future changes.
## Individual Comments
### Comment 1
<location path="core/rodio_player/src/decoder.rs" line_range="281-286" />
<code_context>
#[inline]
fn channels(&self) -> ChannelCount {
- NonZero::new(self.codec_ctx.ch_layout.nb_channels as u16).unwrap()
+ static CH_LOGGED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
+ let ch = self.codec_ctx.ch_layout.nb_channels as u16;
+ if !CH_LOGGED.swap(true, std::sync::atomic::Ordering::Relaxed) {
+ eprintln!(">>> SOURCE: channels={}, sample_rate={}", ch, self.output_sample_rate);
+ }
+ NonZero::new(ch).unwrap()
}
</code_context>
<issue_to_address>
**issue (bug_risk):** Move the `CH_LOGGED` static out of the `channels` method into module scope
This won’t compile because Rust doesn’t allow `static` items inside functions. Instead, declare `static CH_LOGGED: AtomicBool = AtomicBool::new(false);` at module scope (e.g., near the top of `decoder.rs`) and use it from `channels()` to preserve the one-time logging behavior.
</issue_to_address>
### Comment 2
<location path="core/rodio_player/src/lib.rs" line_range="90-94" />
<code_context>
impl RodioPlayer {
#[tracing::instrument(level = "debug", skip())]
pub fn new() -> Self {
+ eprintln!(">>> RODIO PLAYER BUILD TEST 1 <<<");
let (events_tx, events_rx) = channel::<PlayerEvent>();
let tx = Self::initialize(events_tx);
</code_context>
<issue_to_address>
**suggestion:** Drop or gate the hard‑coded debug `eprintln!` in `RodioPlayer::new`
This unconditional `eprintln!` will fire on every player construction and clutter user output/logs. If you still need it, please gate it with a feature flag or `cfg!(debug_assertions)`, or remove it before merging.
```suggestion
#[tracing::instrument(level = "debug", skip())]
pub fn new() -> Self {
if cfg!(debug_assertions) {
eprintln!(">>> RODIO PLAYER BUILD TEST 1 <<<");
}
let (events_tx, events_rx) = channel::<PlayerEvent>();
let tx = Self::initialize(events_tx);
```
</issue_to_address>
### Comment 3
<location path="tools/tauri_crate.bzl" line_range="11-13" />
<code_context>
+ idx = line.find(marker)
+ if idx == -1:
+ continue
+ remainder = line[idx + len(marker):]
+ version_chars = []
+ for c in remainder.elems():
+ if c.isdigit() or c == ".":
+ version_chars.append(c)
</code_context>
<issue_to_address>
**issue (bug_risk):** Replace the Python‑style `.elems()` usage with valid Starlark string iteration
Starlark strings are directly iterable and don’t support `.elems()`, so this will fail at analysis time. Iterate with `for c in remainder:` instead; the rest of the logic can stay the same.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| common --enable_platform_specific_config | ||
|
|
||
| # Android NDK path (inherit from environment, or set in .bazelrc.user) | ||
| common --repo_env=ANDROID_NDK_HOME |
| "@rules_foreign_cc//toolchains:preinstalled_autoconf_toolchain", | ||
| "@rules_foreign_cc//toolchains:preinstalled_automake_toolchain", | ||
| "@rules_foreign_cc//toolchains:preinstalled_m4_toolchain", | ||
| "@rules_foreign_cc//toolchains:preinstalled_pkgconfig_toolchain", |
There was a problem hiding this comment.
Can we keep these as hermetic? Sort of defeats the purpose of using bazel
| }, | ||
| crate = "openssl-sys", | ||
| ) | ||
| # NOTE: openssl/openssl-sys removed - using rustls instead |
There was a problem hiding this comment.
Openssl is pulled regardless for ffmpeg. Is it actually better to use rustls?
| "@crates//:webkit2gtk", | ||
| ], | ||
| "//conditions:default": [], | ||
| "@platforms//os:android": [], |
There was a problem hiding this comment.
?? Why no rodio on android?
|
|
||
| // Build with Bazel (includes UI as a dependency) | ||
| val bazelArgs = mutableListOf( | ||
| "build", |
There was a problem hiding this comment.
Gradle should not call bazel. It should be the other way around.
The point of bazel was to get rid of all other build systems calling each other
| #[cfg(desktop)] | ||
| generate_command_async!(rodio_get_volume, RodioPlayer, f32,); | ||
|
|
||
| // Mobile stubs - rodio is not used on mobile, audio is handled by tauri-plugin-audioplayer |
There was a problem hiding this comment.
The plugin audio player cannot handle his streams and weird audio types. I planned on eventually getting rid of it as it was a makeshift solution.
Doesn't make sense replacing rodio completely with it
There was a problem hiding this comment.
Not sure I am informed enough to get Radio working on Android
There was a problem hiding this comment.
Does it compile for android and iOS? Or straight up breaks?
There was a problem hiding this comment.
IIRC it just breaks for the Android build, and this PR doesn't do anything with iOS
There was a problem hiding this comment.
I'll dig into this some more tomorrow
Summary by Sourcery
Introduce Bazel-based Android and cross-platform build support, including Linux, macOS, Windows, and cross-compiled macOS x64 from Linux, while improving audio handling and platform-specific integrations.
New Features:
Bug Fixes:
Enhancements:
Build:
CI:
Documentation:
Tests:
Chores: