Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 20 additions & 2 deletions cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,33 @@ use openscq30_lib::OpenSCQ30Session;
use tracing::level_filters::LevelFilter;
use tracing_subscriber::EnvFilter;

#[tokio::main]
async fn main() -> ExitCode {
fn main() -> ExitCode {
let matches = cli::build().get_matches();

if let Err(err) = initialize_logging(&matches) {
eprintln!("Logging error: {err:?}");
return ExitCode::FAILURE;
}

let run = move || {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("failed to build tokio runtime")
.block_on(run(matches))
};
// IOBluetooth needs the main thread to be running a run loop, so we have to work in another thread
#[cfg(target_os = "macos")]
{
openscq30_lib::run_with_main_run_loop(run)
}
#[cfg(not(target_os = "macos"))]
{
run()
}
}

async fn run(matches: ArgMatches) -> ExitCode {
if let Err(err) = cli::handle(&matches).await {
if matches.get_count("verbose") > 0 || matches.get_flag("debug-errors") {
eprintln!("Error: {err:?}");
Expand Down
29 changes: 29 additions & 0 deletions docs/build-macos.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Building on macOS

## Requirements

- Xcode Command Line Tools (`xcode-select --install`) for clang and the IOBluetooth framework
- Rust (see `.build-tool-versions` for the version used in CI)

## Building

```sh
cargo build --release -p openscq30-cli
cargo build --release -p openscq30-gui
```

The binaries will be at `target/release/openscq30` and `target/release/openscq30-gui`.

## How the macOS backend works

Bluetooth connections go through Apple's IOBluetooth framework, using a small Objective-C shim in
`lib/src/connection_backend/macos/rfcomm.m` that is compiled by `lib/build.rs`.

On current macOS versions, IOBluetooth delivers RFCOMM events (open complete, incoming data,
disconnect) on the main thread's run loop, no matter which thread opened the channel. The GUI
already has a main run loop, but the CLI does not, so on macOS the CLI runs its logic on a
secondary thread while the main thread spins a run loop
(`openscq30_lib::run_with_main_run_loop`). Anything else embedding `openscq30-lib` on macOS
needs to do the same.

The device must already be paired and connected in System Settings > Bluetooth.
8 changes: 8 additions & 0 deletions lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,14 @@ windows = { workspace = true, features = [
"Networking_Sockets",
] }

[target.'cfg(target_os = "macos")'.dependencies]
# macOS ships an old sqlite that lacks features used by the migrations
rusqlite = { workspace = true, features = ["bundled"] }
tokio = { workspace = true, features = ["rt-multi-thread"] }

[target.'cfg(target_os = "macos")'.build-dependencies]
cc = "1"

[target.'cfg(target_os = "android")'.dependencies]
rusqlite = { workspace = true, features = ["bundled"] }
rust-embed = { workspace = true, features = ["debug-embed"] }
Expand Down
12 changes: 12 additions & 0 deletions lib/build.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
fn main() {
println!("cargo:rerun-if-changed=i18n");

#[cfg(target_os = "macos")]
{
println!("cargo:rerun-if-changed=src/connection_backend/macos/rfcomm.m");
cc::Build::new()
.file("src/connection_backend/macos/rfcomm.m")
.flag("-fobjc-arc")
.flag("-fmodules")
.compile("openscq30_iobluetooth");
println!("cargo:rustc-link-lib=framework=Foundation");
println!("cargo:rustc-link-lib=framework=IOBluetooth");
}
}
7 changes: 7 additions & 0 deletions lib/src/connection_backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ cfg_select! {
target_os = "windows" => {
mod windows;
}
target_os = "macos" => {
mod macos;
pub use macos::run_with_main_run_loop;
}
_ => {
mod none;
}
Expand All @@ -30,6 +34,9 @@ pub fn default_backends() -> Option<impl ConnectionBackends> {
target_os = "windows" => {
Some(windows::PlatformConnectionBackends)
}
target_os = "macos" => {
Some(macos::PlatformConnectionBackends)
}
_ => {
None::<none::NoneConnectionBackends>
}
Expand Down
43 changes: 43 additions & 0 deletions lib/src/connection_backend/macos.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
mod rfcomm;

use std::thread;

use crate::{api::connection, connection_backend::ConnectionBackends};

#[derive(Default)]
pub struct PlatformConnectionBackends;

impl ConnectionBackends for PlatformConnectionBackends {
type Rfcomm = rfcomm::IOBluetoothRfcommBackend;

async fn rfcomm(&self) -> connection::Result<Self::Rfcomm> {
Ok(rfcomm::IOBluetoothRfcommBackend)
}
}

/// IOBluetooth delivers RFCOMM events through the main thread's run loop, so applications without
/// one of their own (such as the CLI) must run their logic on another thread while the main thread
/// spins the run loop. This runs `f` on a new thread, keeps the main run loop going until `f`
/// returns, then returns `f`'s result. Must be called from the main thread.
pub fn run_with_main_run_loop<T: Send + 'static>(f: impl FnOnce() -> T + Send + 'static) -> T {
/// Stops the main run loop when dropped, so that a panic in `f` doesn't leave the main thread
/// spinning forever.
struct StopMainRunLoopOnDrop;
impl Drop for StopMainRunLoopOnDrop {
fn drop(&mut self) {
unsafe { rfcomm::ffi::scq_main_run_loop_stop() };
}
}

let handle = thread::spawn(move || {
let _guard = StopMainRunLoopOnDrop;
// The run loop must keep going until everything inside f has been dropped (closing
// connections), which is why the guard is dropped after f returns.
f()
});
unsafe { rfcomm::ffi::scq_main_run_loop_run() };
match handle.join() {
Ok(result) => result,
Err(panic) => std::panic::resume_unwind(panic),
}
}
Loading