Skip to content

Phase 1: Platform Portability

Parent plan: PLAN-packaging.md

Goal

Make the ryll codebase compile on Linux, macOS, and Windows without changing runtime behaviour on Linux (the primary platform). This phase produces no packages — it lays the groundwork for the CI and packaging phases that follow.

Current state

  • The code compiles and runs only on Linux x86_64.
  • libc::signal(SIGINT, ...) in src/main.rs is Unix-only and will fail to compile on Windows.
  • Cargo.lock is in .gitignore — not committed.
  • The --capture feature uses openh264, mp4, pcap-file, and etherparse crates. Per the master plan, capture should be disabled on Windows via a Cargo feature.
  • No #[cfg] guards exist anywhere in the codebase.

Changes

Step 1: Commit Cargo.lock

Generate Cargo.lock and remove it from .gitignore. This ensures reproducible builds across CI runners and developer machines.

Files changed: - .gitignore — remove the Cargo.lock line - Cargo.lock — generated by cargo generate-lockfile (run inside the devcontainer to match the existing build environment)

Commit: standalone, before any code changes.

Step 2: Replace libc signal handler with ctrlc crate

Replace the raw libc::signal(SIGINT, ...) call with the ctrlc crate, which provides portable Ctrl+C handling on both Unix and Windows.

Current code (src/main.rs:24-39):

pub static SHUTDOWN_REQUESTED: AtomicBool = ...;

extern "C" fn handle_sigint(_: libc::c_int) {
    SHUTDOWN_REQUESTED.store(true, Ordering::SeqCst);
}

fn main() -> Result<()> {
    unsafe {
        libc::signal(
            libc::SIGINT,
            handle_sigint as *const () as libc::sighandler_t,
        );
    }
    ...
}

New code:

pub static SHUTDOWN_REQUESTED: AtomicBool = ...;

fn main() -> Result<()> {
    ctrlc::set_handler(|| {
        SHUTDOWN_REQUESTED.store(true, Ordering::SeqCst);
    })
    .expect("failed to set Ctrl+C handler");
    ...
}

Cargo.toml changes: - Add: ctrlc = "3" - Remove: libc = "0.2" (nothing else uses it)

Files changed: - Cargo.toml — swap libc for ctrlc - src/main.rs — replace signal handler, remove handle_sigint function, remove unsafe block

Commit: standalone.

Step 3: Feature-gate capture for Windows

Add a Cargo feature capture that is enabled by default. The --capture CLI flag and all capture code are compiled only when this feature is active. On Windows, CI will build with --no-default-features to exclude capture.

Cargo.toml changes:

[features]
default = ["capture"]
capture = [
    "dep:pcap-file",
    "dep:etherparse",
    "dep:openh264",
    "dep:mp4",
]

[dependencies]
# Capture mode (--capture) — optional, not available on
# Windows
pcap-file = { version = "2", optional = true }
etherparse = { version = "0.16", optional = true }
openh264 = { version = "0.6", optional = true }
mp4 = { version = "0.14", optional = true }

Source code changes:

All references to the capture module and CaptureSession need #[cfg(feature = "capture")] guards. The files that need changes are:

  • src/main.rs — gate mod capture, the use crate::capture::CaptureSession import, the --capture argument handling, and the capture parameter on run_headless/run_gui. When the feature is off, capture is always None.

  • src/capture.rs — gate the entire file with #![cfg(feature = "capture")] (module-level).

  • src/config.rs — gate the capture field on Args with #[cfg(feature = "capture")].

  • src/app.rs — gate all CaptureSession imports and uses. The capture parameter on RyllApp::new, run_headless, and run_connection becomes Option<Arc<CaptureSession>> when the feature is on, and is elided (always None) when off. The cleanest approach is to always accept Option<Arc<CaptureSession>> but use a type alias:

#[cfg(feature = "capture")]
use crate::capture::CaptureSession;
#[cfg(feature = "capture")]
type CaptureRef = Option<Arc<CaptureSession>>;
#[cfg(not(feature = "capture"))]
type CaptureRef = ();

However, this adds complexity to every call site. A simpler approach: keep the Option<Arc<...>> parameter everywhere, but when the feature is off, define a minimal stub:

#[cfg(not(feature = "capture"))]
mod capture {
    pub struct CaptureSession;
}

This lets all function signatures remain unchanged — they just always receive None. The only gated code is the mod capture import and the --capture CLI flag.

Recommended: use the stub approach for minimal diff and no signature changes across the codebase.

  • src/channels/display.rs, cursor.rs, main_channel.rs, inputs.rs — these all accept Option<Arc<CaptureSession>> and call methods on it conditionally. With the stub approach, these files need no changes because they always check if let Some(ref c) = self.capture and the value will simply always be None on Windows.

Commit: standalone.

Step 4: Verify cross-compilation

Attempt to compile for all three target triples. This can be done locally with rustup target add or we can defer full verification to Phase 2 (CI). At minimum, verify:

cargo check --target aarch64-apple-darwin
cargo check --target x86_64-pc-windows-msvc

Note: cargo check for foreign targets may fail on linking but will catch Rust compilation errors. Full build verification will happen in CI (Phase 2) on native runners.

If cross-checks surface additional issues (unlikely given the dependency set, but possible), fix them in this step.

Commit: only if fixes are needed.

Step summary

Step Description Files Commit
1 Commit Cargo.lock .gitignore, Cargo.lock Yes
2 Replace libc with ctrlc Cargo.toml, src/main.rs Yes
3 Feature-gate capture Cargo.toml, src/main.rs, src/config.rs, src/capture.rs Yes
4 Verify cross-compilation Fix any issues found Only if needed

Risks and mitigations

  • ctrlc crate behaviour difference: On Unix, ctrlc uses signal(SIGINT) internally, so behaviour is identical to today. On Windows it uses SetConsoleCtrlHandler. The AtomicBool approach remains the same on both platforms.

  • Feature-gated capture breaks something: The stub approach means all function signatures stay the same and None flows through everywhere. Risk is low. We verify with cargo build --no-default-features on Linux before pushing.

  • openh264 C compilation on macOS: The openh264 crate bundles C source and builds via cc. On macOS with Xcode CLT, this should work. We'll confirm in Phase 2 CI. No mitigation needed in Phase 1.

Documentation updates

After completing all steps: - Update docs/portability.md to reflect cross-platform support and the capture feature gate. - Update AGENTS.md to mention the capture Cargo feature and the ctrlc dependency replacement. - Update README.md build instructions if needed.

📝 Report an issue with this page