From 4ae20bc1db36aae10427efde470a039a8d1ab8ba Mon Sep 17 00:00:00 2001 From: Anthony Lannutti Date: Thu, 4 Jun 2026 13:34:57 -0500 Subject: [PATCH] feat: add openrc cargo feature for OpenRC bluetooth service management Coding-Agent: Claude Code Model: claude-sonnet-4-5 --- cosmic-settings/Cargo.toml | 4 +- cosmic-settings/src/main.rs | 7 + cosmic-settings/src/pages/bluetooth/mod.rs | 126 ++-- cosmic-settings/src/service_manager.rs | 631 +++++++++++++++++++++ debian/rules | 2 +- justfile | 41 +- 6 files changed, 752 insertions(+), 59 deletions(-) create mode 100644 cosmic-settings/src/service_manager.rs diff --git a/cosmic-settings/Cargo.toml b/cosmic-settings/Cargo.toml index 763b781..7a50cf7 100644 --- a/cosmic-settings/Cargo.toml +++ b/cosmic-settings/Cargo.toml @@ -113,8 +113,10 @@ git = "https://github.com/AerynOS/locales-rs" optional = true [features] -default = ["a11y", "linux", "single-instance", "wgpu"] +default = ["a11y", "linux", "single-instance", "wgpu", "systemd"] gettext = ["dep:gettext-rs"] +systemd = [] +openrc = [] # Default features for Linux linux = [ diff --git a/cosmic-settings/src/main.rs b/cosmic-settings/src/main.rs index 3c18d77..7a39baf 100644 --- a/cosmic-settings/src/main.rs +++ b/cosmic-settings/src/main.rs @@ -7,6 +7,12 @@ #![allow(clippy::cast_lossless)] #![allow(clippy::too_many_lines)] +#[cfg(not(any(feature = "systemd", feature = "openrc")))] +compile_error!( + "At least one service manager feature must be enabled. \ + Enable 'systemd' or 'openrc' for bluetooth service management." +); + pub mod app; use std::str::FromStr; @@ -16,6 +22,7 @@ pub mod config; #[macro_use] pub mod localize; pub mod pages; +pub mod service_manager; pub mod subscription; pub mod theme; pub mod utils; diff --git a/cosmic-settings/src/pages/bluetooth/mod.rs b/cosmic-settings/src/pages/bluetooth/mod.rs index 11a1712..8676665 100644 --- a/cosmic-settings/src/pages/bluetooth/mod.rs +++ b/cosmic-settings/src/pages/bluetooth/mod.rs @@ -16,6 +16,10 @@ use std::sync::Arc; use std::time::Duration; use zbus::zvariant::OwnedObjectPath; +#[cfg(test)] +use crate::service_manager::MockServiceManager; +use crate::service_manager::ServiceManagerHandle; + enum Dialog { RequestConfirmation { device: String, @@ -158,6 +162,8 @@ pub struct Page { service_is_active: bool, subscription: Option>, + + service_manager: ServiceManagerHandle, } impl page::Page for Page { @@ -524,7 +530,18 @@ impl Page { } Event::DBusServiceUnknown => { - self.bluez_service_unknown = true; + // D-Bus activation for org.bluez is absent. On systemd this usually + // means bluez isn't installed; on OpenRC it may be installed but lack + // the D-Bus service file. Check the service manager to disambiguate. + if self.service_manager.is_installed() { + // Service manager confirms the service exists — query its real state. + self.bluez_service_unknown = false; + self.service_is_active = self.service_manager.is_active(); + self.service_is_enabled = self.service_manager.is_enabled(); + } else { + // Genuinely not installed — let status() show the unknown message. + self.bluez_service_unknown = true; + } } Event::Agent(message) => { @@ -668,8 +685,8 @@ impl Page { } Message::DBusConnect(connection) => { - self.service_is_active = systemd::is_bluetooth_active(); - self.service_is_enabled = systemd::is_bluetooth_enabled(); + self.service_is_active = self.service_manager.is_active(); + self.service_is_enabled = self.service_manager.is_enabled(); self.connection = Some(connection.clone()); let get_adapters_fut = get_adapters(connection.clone()); @@ -801,8 +818,9 @@ impl Page { } Message::ServiceActivate => { - return cosmic::task::future(async { - systemd::activate_bluetooth().await; + let activate_future = self.service_manager.activate(); + return cosmic::task::future(async move { + activate_future.await; tokio::time::sleep(Duration::from_secs(3)).await; match zbus::Connection::system().await { @@ -813,8 +831,9 @@ impl Page { } Message::ServiceEnable => { - return cosmic::task::future(async { - systemd::enable_bluetooth().await; + let enable_future = self.service_manager.enable(); + return cosmic::task::future(async move { + enable_future.await; tokio::time::sleep(Duration::from_secs(3)).await; match zbus::Connection::system().await { @@ -873,7 +892,7 @@ fn status() -> Section { return bluetooth_service_issue( fl!("bluetooth", "inactive"), fl!("activate"), - Message::ServiceEnable, + Message::ServiceActivate, ); } @@ -1126,36 +1145,65 @@ fn multiple_adapter() -> Section { impl page::AutoBind for Page {} -mod systemd { - use futures::FutureExt; - - pub fn activate_bluetooth() -> impl Future + Send { - tokio::process::Command::new("pkexec") - .args(["systemctl", "start", "bluetooth"]) - .status() - .map(|_| ()) - } - - pub fn enable_bluetooth() -> impl Future + Send { - tokio::process::Command::new("pkexec") - .args(["systemctl", "enable", "--now", "bluetooth"]) - .status() - .map(|_| ()) - } - - pub fn is_bluetooth_enabled() -> bool { - std::process::Command::new("systemctl") - .args(["is-enabled", "bluetooth"]) - .status() - .map(|status| status.success()) - .unwrap_or(true) - } - - pub fn is_bluetooth_active() -> bool { - std::process::Command::new("systemctl") - .args(["is-active", "bluetooth"]) - .status() - .map(|status| status.success()) - .unwrap_or(true) +impl Page { + #[cfg(test)] + fn with_service_manager(service_manager: ServiceManagerHandle) -> Self { + Self { + service_manager, + ..Page::default() + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_dbus_service_unknown_with_installed_service_queries_manager() { + let bluetooth = MockServiceManager::new(true, false); + let mut page = Page::with_service_manager(ServiceManagerHandle::new(Box::new(bluetooth))); + let _task = page.update(Message::BluetoothEvent(Event::DBusServiceUnknown)); + + assert!(page.service_is_enabled); + assert!(!page.service_is_active); + assert!(!page.bluez_service_unknown); + } + + #[test] + fn test_dbus_service_unknown_with_uninstalled_service_sets_bluez_unknown() { + let bluetooth = MockServiceManager::new(true, true).with_installed(false); + let mut page = Page::with_service_manager(ServiceManagerHandle::new(Box::new(bluetooth))); + let _task = page.update(Message::BluetoothEvent(Event::DBusServiceUnknown)); + + assert!(page.bluez_service_unknown); + assert!(!page.service_is_enabled); + assert!(!page.service_is_active); + } + + #[test] + fn test_dbus_service_unknown_clears_previously_set_bluez_service_unknown() { + // Simulate a stale state and verify the handler re-checks via is_installed(). + let bluetooth = MockServiceManager::new(true, true); + let mut page = Page::with_service_manager(ServiceManagerHandle::new(Box::new(bluetooth))); + page.bluez_service_unknown = true; + + let _task = page.update(Message::BluetoothEvent(Event::DBusServiceUnknown)); + + assert!(!page.bluez_service_unknown); + } + + #[tokio::test] + async fn test_service_activate_calls_through_to_service_manager() { + let bluetooth = MockServiceManager::new(false, false); + let mut page = Page::with_service_manager(ServiceManagerHandle::new(Box::new(bluetooth))); + let _task = page.update(Message::ServiceActivate); + } + + #[tokio::test] + async fn test_service_enable_calls_through_to_service_manager() { + let bluetooth = MockServiceManager::new(false, false); + let mut page = Page::with_service_manager(ServiceManagerHandle::new(Box::new(bluetooth))); + let _task = page.update(Message::ServiceEnable); } } diff --git a/cosmic-settings/src/service_manager.rs b/cosmic-settings/src/service_manager.rs new file mode 100644 index 0000000..2090fe8 --- /dev/null +++ b/cosmic-settings/src/service_manager.rs @@ -0,0 +1,631 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: GPL-3.0-only + +//! Service manager abstraction for managing system services. +//! +//! This module provides a trait-based interface for interacting with system +//! service managers (like systemd, OpenRC, etc.), allowing for testability +//! and flexibility across different init systems. + +use std::future::Future; +use std::pin::Pin; + +/// Trait for managing system services. +/// +/// Implementations are bound to a specific service name and provide +/// methods to query service state and perform lifecycle operations. +pub trait ServiceManager { + /// Whether the service is configured to start on boot. + fn is_enabled(&self) -> bool; + + /// Whether the service is currently running. + fn is_active(&self) -> bool; + + /// Start the service. + fn activate(&self) -> Pin + Send>>; + + /// Enable the service to start on boot and start it now. + fn enable(&self) -> Pin + Send>>; + + /// Whether the service is installed on this system. + fn is_installed(&self) -> bool; +} + +/// A newtype around `Box` that enables `#[derive(Default)]` +/// on containing structs. Methods on `ServiceManager` are accessible via `Deref`. +/// +/// The `Default` impl creates a manager for the `"bluetooth"` service, which is +/// currently the only consumer. If other services need this in the future the +/// default can be made configurable. +pub struct ServiceManagerHandle(Box); + +impl ServiceManagerHandle { + pub fn new(manager: Box) -> Self { + Self(manager) + } +} + +impl std::ops::Deref for ServiceManagerHandle { + type Target = dyn ServiceManager; + fn deref(&self) -> &Self::Target { + &*self.0 + } +} + +impl Default for ServiceManagerHandle { + fn default() -> Self { + Self(create_default_service_manager("bluetooth")) + } +} + +/// Log a warning or error after a privileged command completes. +fn log_command_result( + result: &Result, + description: &str, + service: &str, +) { + match result { + Ok(status) if status.success() => {} + Ok(status) => tracing::warn!( + "{} for '{}' failed with exit code: {:?}", + description, + service, + status.code(), + ), + Err(e) => tracing::error!("Failed to execute {} for '{}': {}", description, service, e,), + } +} + +/// Run a privileged command via `pkexec`. +/// +/// This is the common async entry point for service management actions +/// that require elevated privileges. +async fn run_pkexec_command(args: &[&str], description: &str, service: &str) { + let result = tokio::process::Command::new("pkexec") + .args(args) + .status() + .await; + log_command_result(&result, description, service); +} + +/// Mock ServiceManager that returns configurable boolean values. +#[cfg(test)] +pub struct MockServiceManager { + enabled: bool, + active: bool, + installed: bool, +} + +#[cfg(test)] +impl MockServiceManager { + pub fn new(enabled: bool, active: bool) -> Self { + Self { + enabled, + active, + installed: true, + } + } + + pub fn with_installed(mut self, installed: bool) -> Self { + self.installed = installed; + self + } +} + +#[cfg(test)] +impl ServiceManager for MockServiceManager { + fn is_enabled(&self) -> bool { + self.enabled + } + + fn is_active(&self) -> bool { + self.active + } + + fn activate(&self) -> Pin + Send>> { + Box::pin(async {}) + } + + fn enable(&self) -> Pin + Send>> { + Box::pin(async {}) + } + + fn is_installed(&self) -> bool { + self.installed + } +} + +/// SystemD implementation of ServiceManager. +#[cfg(feature = "systemd")] +pub struct SystemDServiceManager { + service_name: String, +} + +#[cfg(feature = "systemd")] +impl SystemDServiceManager { + pub fn new(service_name: impl Into) -> Self { + Self { + service_name: service_name.into(), + } + } +} + +#[cfg(feature = "systemd")] +impl ServiceManager for SystemDServiceManager { + fn is_enabled(&self) -> bool { + std::process::Command::new("systemctl") + .args(["is-enabled", &self.service_name]) + .status() + .map(|status| status.success()) + .unwrap_or(true) + } + + fn is_active(&self) -> bool { + std::process::Command::new("systemctl") + .args(["is-active", &self.service_name]) + .status() + .map(|status| status.success()) + .unwrap_or(true) + } + + fn activate(&self) -> Pin + Send>> { + let service = self.service_name.clone(); + Box::pin(async move { + run_pkexec_command( + &["systemctl", "start", &service], + "systemctl start", + &service, + ) + .await; + }) + } + + fn enable(&self) -> Pin + Send>> { + let service = self.service_name.clone(); + Box::pin(async move { + run_pkexec_command( + &["systemctl", "enable", "--now", &service], + "systemctl enable --now", + &service, + ) + .await; + }) + } + + fn is_installed(&self) -> bool { + std::process::Command::new("systemctl") + .args(["cat", &self.service_name]) + .status() + .map(|status| status.success()) + .unwrap_or(false) + } +} + +/// OpenRC implementation of ServiceManager. +#[cfg(feature = "openrc")] +pub struct OpenRcServiceManager { + service_name: String, +} + +#[cfg(feature = "openrc")] +impl OpenRcServiceManager { + pub fn new(service_name: impl Into) -> Self { + Self { + service_name: service_name.into(), + } + } +} + +/// Parses `rc-update show` output to check if a service is in any runlevel. +/// +/// Uses exact service name matching to avoid false positives from prefix matches +/// (e.g., "bluetoothd" should not match a lookup for "bluetooth"). +#[cfg(any(feature = "openrc", test))] +fn is_service_in_runlevel_output(output: &str, service_name: &str) -> bool { + output.lines().any(|line| { + line.split('|') + .next() + .map(|name| name.trim() == service_name) + .unwrap_or(false) + }) +} + +#[cfg(feature = "openrc")] +impl ServiceManager for OpenRcServiceManager { + fn is_enabled(&self) -> bool { + std::process::Command::new("rc-update") + .args(["show"]) + .output() + .map(|output| { + is_service_in_runlevel_output( + &String::from_utf8_lossy(&output.stdout), + &self.service_name, + ) + }) + .unwrap_or(true) + } + + fn is_active(&self) -> bool { + std::process::Command::new("rc-service") + .args([&self.service_name, "status"]) + .status() + .map(|status| status.success()) + .unwrap_or(true) + } + + fn activate(&self) -> Pin + Send>> { + let service = self.service_name.clone(); + Box::pin(async move { + run_pkexec_command( + &["rc-service", &service, "start"], + "rc-service start", + &service, + ) + .await; + }) + } + + fn enable(&self) -> Pin + Send>> { + let service = self.service_name.clone(); + Box::pin(async move { + run_pkexec_command( + &["rc-update", "add", &service, "default"], + "rc-update add", + &service, + ) + .await; + run_pkexec_command( + &["rc-service", &service, "start"], + "rc-service start", + &service, + ) + .await; + }) + } + + fn is_installed(&self) -> bool { + std::path::Path::new("/etc/init.d") + .join(&self.service_name) + .exists() + } +} + +/// Detect the running service manager and create a manager for the named service. +/// +/// Checks runtime directories (`/run/systemd/system`, `/run/openrc`) to determine +/// which service manager is actively running, rather than relying on which binaries +/// are installed. This avoids false detections when multiple service managers are +/// installed but only one is actively managing services. +pub fn detect_service_manager( + service_name: impl Into, +) -> Result, String> { + let service_name = service_name.into(); + + #[cfg(feature = "systemd")] + { + if std::path::Path::new("/run/systemd/system").exists() { + tracing::debug!("Detected systemd service manager via /run/systemd/system"); + return Ok(Box::new(SystemDServiceManager::new(service_name))); + } + } + + #[cfg(feature = "openrc")] + { + if std::path::Path::new("/run/openrc").exists() { + tracing::debug!("Detected OpenRC service manager via /run/openrc"); + return Ok(Box::new(OpenRcServiceManager::new(service_name))); + } + } + + { + let mut checked = Vec::new(); + #[cfg(feature = "systemd")] + checked.push("systemd (/run/systemd/system)"); + #[cfg(feature = "openrc")] + checked.push("openrc (/run/openrc)"); + + Err(format!( + "Could not detect a running service manager. Checked for: {}. \ + None of these service managers appear to be running on this system.", + checked.join(", ") + )) + } +} + +/// Factory: returns `MockServiceManager` in tests, detects the running service manager in production. +#[cfg(test)] +pub fn create_default_service_manager(_service_name: impl Into) -> Box { + Box::new(MockServiceManager::new(false, false)) +} + +#[cfg(not(test))] +pub fn create_default_service_manager(service_name: impl Into) -> Box { + let service_name = service_name.into(); + detect_service_manager(&service_name).unwrap_or_else(|e| { + tracing::warn!( + "Failed to detect service manager: {}. Service management features will not be available.", + e + ); + tracing::warn!( + "Using no-op service manager for '{}'. Service status will always report as enabled/active, but operations will not actually execute.", + service_name + ); + // Graceful degradation — app continues but service management won't work + Box::new(NoOpServiceManager) + }) +} + +/// Fallback when no service manager is detected — reports everything as active but does nothing. +struct NoOpServiceManager; + +impl ServiceManager for NoOpServiceManager { + fn is_enabled(&self) -> bool { + true + } + + fn is_active(&self) -> bool { + true + } + + fn activate(&self) -> Pin + Send>> { + Box::pin(async {}) + } + + fn enable(&self) -> Pin + Send>> { + Box::pin(async {}) + } + + fn is_installed(&self) -> bool { + false + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tracing_subscriber::util::SubscriberInitExt; + + #[test] + fn test_service_manager_trait_is_enabled() { + struct TestServiceManager { + enabled: bool, + installed: bool, + } + + impl ServiceManager for TestServiceManager { + fn is_enabled(&self) -> bool { + self.enabled + } + + fn is_active(&self) -> bool { + true + } + + fn activate(&self) -> Pin + Send>> { + Box::pin(async {}) + } + + fn enable(&self) -> Pin + Send>> { + Box::pin(async {}) + } + + fn is_installed(&self) -> bool { + self.installed + } + } + + let test_service = TestServiceManager { + enabled: true, + installed: true, + }; + + assert!(test_service.is_enabled()); + + let disabled_service = TestServiceManager { + enabled: false, + installed: true, + }; + assert!(!disabled_service.is_enabled()); + } + + #[test] + fn test_service_manager_trait_is_active() { + struct TestServiceManager { + active: bool, + installed: bool, + } + + impl ServiceManager for TestServiceManager { + fn is_enabled(&self) -> bool { + true + } + + fn is_active(&self) -> bool { + self.active + } + + fn activate(&self) -> Pin + Send>> { + Box::pin(async {}) + } + + fn enable(&self) -> Pin + Send>> { + Box::pin(async {}) + } + + fn is_installed(&self) -> bool { + self.installed + } + } + + let test_service = TestServiceManager { + active: true, + installed: true, + }; + + assert!(test_service.is_active()); + + let inactive_service = TestServiceManager { + active: false, + installed: true, + }; + assert!(!inactive_service.is_active()); + } + + #[test] + fn test_mock_service_manager_returns_configured_values() { + let bluetooth = MockServiceManager::new(true, true); + assert!(bluetooth.is_enabled()); + assert!(bluetooth.is_active()); + + let disabled_bluetooth = MockServiceManager::new(false, false); + assert!(!disabled_bluetooth.is_enabled()); + assert!(!disabled_bluetooth.is_active()); + } + + #[test] + #[cfg(feature = "systemd")] + fn test_systemd_service_manager_implements_trait() { + let bluetooth = SystemDServiceManager::new("bluetooth"); + + // Can't test actual systemd calls in unit tests, but verify the struct exists + let _enabled: bool = bluetooth.is_enabled(); + let _active: bool = bluetooth.is_active(); + } + + #[tokio::test] + async fn test_service_manager_trait_has_activate_method() { + let bluetooth = MockServiceManager::new(false, false); + bluetooth.activate().await; + } + + #[tokio::test] + async fn test_service_manager_trait_has_enable_method() { + let bluetooth = MockServiceManager::new(false, false); + bluetooth.enable().await; + } + + #[tokio::test] + async fn test_service_manager_activate_returns_future() { + let bluetooth = MockServiceManager::new(false, false); + let future = bluetooth.activate(); + future.await; + } + + #[tokio::test] + async fn test_service_manager_enable_returns_future() { + let bluetooth = MockServiceManager::new(false, false); + let future = bluetooth.enable(); + future.await; + } + + #[test] + fn test_default_service_manager_uses_mock_in_tests() { + let bluetooth = create_default_service_manager("bluetooth"); + + assert!(!bluetooth.is_enabled()); + assert!(!bluetooth.is_active()); + } + + #[test] + #[cfg(feature = "openrc")] + fn test_openrc_service_manager_implements_trait() { + let bluetooth = OpenRcServiceManager::new("bluetooth"); + + // Can't test actual OpenRC calls in unit tests, but verify the struct exists + let _enabled: bool = bluetooth.is_enabled(); + let _active: bool = bluetooth.is_active(); + } + + #[test] + fn test_detect_service_manager_returns_result() { + let result = detect_service_manager("test-service"); + + match result { + Ok(_) => {} + Err(e) => { + assert!(!e.is_empty()); + assert!( + e.contains("service manager"), + "Error message should mention service manager: {}", + e + ); + } + } + } + + #[test] + fn test_mock_service_manager_accepts_service_name() { + let bluetooth = MockServiceManager::new(true, true); + assert!(bluetooth.is_enabled()); + } + + #[test] + fn test_mock_service_manager_is_installed_defaults_to_true() { + let bluetooth = MockServiceManager::new(true, true); + assert!(bluetooth.is_installed()); + } + + #[test] + fn test_mock_service_manager_with_installed_configures_state() { + let not_installed = MockServiceManager::new(true, true).with_installed(false); + assert!(!not_installed.is_installed()); + + let installed = MockServiceManager::new(true, true).with_installed(true); + assert!(installed.is_installed()); + } + + #[test] + fn test_no_op_service_manager_reports_not_installed() { + let no_op = NoOpServiceManager; + + // When no service manager is detected at runtime, the fallback must report + // not-installed so DBusServiceUnknown falls through to the unknown-service path + // instead of showing a non-functional toggle. + assert!(!no_op.is_installed()); + } + + #[test] + fn test_parse_rc_update_output_exact_service_name() { + // Must use exact matching, not prefix matching — "bluetoothd" must not match "bluetooth". + let output = " bluetoothd | default\n"; + let found = is_service_in_runlevel_output(output, "bluetooth"); + assert!(!found); + } + + #[test] + fn test_parse_rc_update_output_matches_exact_name() { + let output = " bluetooth | default\n"; + let found = is_service_in_runlevel_output(output, "bluetooth"); + assert!(found); + } + + #[test] + fn test_parse_rc_update_output_not_found() { + let output = " dbus | default\n network | sysinit\n"; + let found = is_service_in_runlevel_output(output, "bluetooth"); + assert!(!found); + } + + #[test] + fn test_log_command_result_logs_all_branches() { + let _guard = tracing_subscriber::fmt().with_test_writer().set_default(); + + let err_result: Result = Err( + std::io::Error::new(std::io::ErrorKind::NotFound, "command not found"), + ); + log_command_result(&err_result, "test command", "test-service"); + + let fail_result = std::process::Command::new("false").status(); + log_command_result(&fail_result, "test false", "test-service"); + + let ok_result = std::process::Command::new("true").status(); + log_command_result(&ok_result, "test true", "test-service"); + } + + #[test] + fn test_parse_rc_update_output_multi_line() { + let output = " dbus | default\n bluetooth | default\n network | sysinit\n"; + let found = is_service_in_runlevel_output(output, "bluetooth"); + assert!(found); + } +} diff --git a/debian/rules b/debian/rules index 6f3ef8c..2027bd7 100755 --- a/debian/rules +++ b/debian/rules @@ -9,7 +9,7 @@ override_dh_auto_clean: ischroot || just vendor override_dh_auto_build: - test -e vendor.tar && just build-vendored || just + test -e vendor.tar && just build-vendored --features systemd || just build-release --features systemd override_dh_auto_install: just rootdir=$(DESTDIR) install diff --git a/justfile b/justfile index deed88c..c25c959 100644 --- a/justfile +++ b/justfile @@ -53,26 +53,31 @@ heaptrack *args: check-features: #!/usr/bin/env bash set -ex - cargo check --no-default-features cargo check - for feature in \ - "page-accessibility" \ - "page-about" \ - "page-bluetooth" \ - "page-date" \ - "page-default-apps" \ - "page-display" \ - "page-input" \ - "page-legacy-applications" \ - "page-networking" \ - "page-power" \ - "page-region" \ - "page-sound" \ - "page-users" \ - "page-window-management" \ - "page-workspaces" + for service_manager in \ + "systemd" \ + "openrc" do - cargo check --no-default-features --features "${feature}" + cargo check --no-default-features --features "${service_manager}" + for feature in \ + "page-accessibility" \ + "page-about" \ + "page-bluetooth" \ + "page-date" \ + "page-default-apps" \ + "page-display" \ + "page-input" \ + "page-legacy-applications" \ + "page-networking" \ + "page-power" \ + "page-region" \ + "page-sound" \ + "page-users" \ + "page-window-management" \ + "page-workspaces" + do + cargo check --no-default-features --features "${feature},${service_manager}" + done done # Bump cargo version, create git commit, and create tag