diff --git a/Cargo.lock b/Cargo.lock index c0e8c30..aee8a9f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -145,6 +145,7 @@ dependencies = [ "serde_derive", "structopt", "toml", + "walkdir", "zip", "zip-extensions", ] @@ -389,6 +390,15 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "semver" version = "0.9.0" @@ -562,6 +572,17 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "walkdir" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +dependencies = [ + "same-file", + "winapi", + "winapi-util", +] + [[package]] name = "wasi" version = "0.10.0+wasi-snapshot-preview1" diff --git a/Cargo.toml b/Cargo.toml index 1c80f60..1788b17 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,4 +25,7 @@ serde_derive = "1.0.110" structopt = "0.3.14" toml = "0.5.6" zip = "0.5" -zip-extensions = "0.6" \ No newline at end of file +zip-extensions = "0.6" + +[target.'cfg(target_os = "linux")'.dependencies] +walkdir = "2.3.2" diff --git a/src/main.rs b/src/main.rs index 87d3cc3..aac57ae 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,6 +17,9 @@ use zip_extensions::zip_create_from_directory_with_options; #[cfg(unix)] use anyhow::Context; +#[cfg(target_os = "linux")] +use walkdir::WalkDir; + mod config; #[cfg(target_os = "macos")] @@ -438,7 +441,13 @@ impl Build { env::var("PLAYDATE_SERIAL_DEVICE") .unwrap_or(String::from("/dev/cu.usbmodemPDU1_Y0005491")), ); - #[cfg(not(target_os = "macos"))] + #[cfg(target_os = "linux")] + let modem_path = PathBuf::from( + env::var("PLAYDATE_SERIAL_DEVICE") + // On Linux, we can use named symlinks to find the device in most cases + .unwrap_or(find_serial_device().unwrap_or(String::from("/dev/ttyACM0"))), + ); + #[cfg(all(not(target_os = "linux"), not(target_os = "macos")))] let modem_path = PathBuf::from( env::var("PLAYDATE_SERIAL_DEVICE").unwrap_or(String::from("/dev/ttyACM0")), ); @@ -686,6 +695,75 @@ impl Build { } } +#[cfg(target_os = "linux")] +/// Finds the canonical (resolved) path for the Playdate serial device. If multiple Playdate devices are +/// found, warns and returns the first. If none is found, returns None. If any error occurs, +/// returns None. +fn find_serial_device() -> Option { + // Walk through this directory to find Playdate device filenames + let directory = "/dev/serial/by-id"; + let filename_prefix = "usb-Panic_Inc_Playdate_PDU1-"; + + let walker = WalkDir::new(directory) + .min_depth(1) + .max_depth(1) + // Don't follow links (yet) because we want file_name to give us the name in this directory + .follow_links(false) + // If there are multiple, we let the user know and take the first; sort so it's consistent. + // If the user wants a different one, they can set PLAYDATE_SERIAL_DEVICE. + .sort_by_file_name() + .into_iter() + .filter_entry(|e| { + e.file_name() + .to_str() + .map(|s| s.starts_with(filename_prefix)) + .unwrap_or(false) + }) + .filter_map(|e| e.ok()); + + // See what we found + let mut result: Option = None; + for entry in walker { + match result { + // If there are multiple matches, let the user know, and return the first + Some(ref existing) => { + println!( + "Found multiple Playdate devices in {}, using first: {}", + directory, + existing.display() + ); + break; + } + None => { + result = Some(entry.into_path()); + } + } + } + + if let Some(path) = result { + // Fully resolve the link, which should result in something like "/dev/ttyACM0" + let resolved = fs::canonicalize(path).ok()?; + // Quick check that it did what we expected + if resolved + .to_str() + .map(|s| s.contains("tty")) + .unwrap_or(false) + { + println!("Resolved Playdate serial device to: {}", resolved.display()); + // Other code expects String paths + return Some(resolved.to_string_lossy().into_owned()); + } else { + eprintln!( + "Warning: found a device at '{}' but it's not named like we expect. Using the default.", + resolved.display() + ); + return None; + } + } + + None +} + #[derive(Debug, StructOpt)] struct Package { /// Build a specific example from the examples/ dir.