diff --git a/src/default.nix b/src/default.nix new file mode 100644 index 0000000..e69de29 diff --git a/src/main.rs b/src/main.rs index 0511fd7..f189714 100644 --- a/src/main.rs +++ b/src/main.rs @@ -72,6 +72,25 @@ struct Outputs { out: String, } +fn is_target_for_packaging(src_dir: &PathBuf) -> bool { + let has_cargo = src_dir.join("Cargo.toml").is_file(); + let cargo_lock = File::open(src_dir.join("Cargo.lock")); + let has_cargo_lock = cargo_lock.is_ok(); + let has_cmake = src_dir.join("CMakeLists.txt").is_file(); + let has_go = src_dir.join("go.mod").is_file(); + let has_meson = src_dir.join("meson.build").is_file(); + let pyproject_toml = src_dir.join("pyproject.toml"); + let has_pyproject = pyproject_toml.is_file(); + let has_setuptools = src_dir.join("setup.py").is_file(); + + (has_cargo && has_cargo_lock) + || has_cmake + || has_go + || has_meson + || has_pyproject + || has_setuptools +} + #[tokio::main] async fn main() -> Result<()> { run().await @@ -394,7 +413,74 @@ async fn run() -> Result<()> { PathBuf::from(&src) }; - let mut choices = Vec::new(); + // Let's first find all subdirectories that are compatible with our supported recipes. + let root_src_dir = src_dir.clone(); + let mut open_subdirs = vec![(0, src_dir)]; + let mut sourceroot_candidates = vec![]; + while !open_subdirs.is_empty() { + // If we call this, we have a non-empty vector. + let (depth, target_dir) = open_subdirs.pop().unwrap(); + let subdirs = std::fs::read_dir(&target_dir).with_context(|| { + format!("failed to list subdirectories of {}", target_dir.display()) + })?; + + for maybe_subdir in subdirs { + let subdir = maybe_subdir.context(format!( + "unexpected io error while reading a subdirectory of {}", + target_dir.to_string_lossy() + ))?; + + let subdir_path = &subdir.path(); + + // We want only subdirectories. + if !subdir_path.is_dir() { + continue; + } + + if is_target_for_packaging(subdir_path) { + sourceroot_candidates.push(subdir.file_name()); + } + + // Recurse into that subdir. + if depth < 3 { + open_subdirs.push((depth + 1, subdir_path.to_path_buf())); + } + } + } + + editor.set_helper(Some(Prompter::List( + sourceroot_candidates + .iter() + .map(|sr| { + PathBuf::from(sr).strip_prefix(&root_src_dir) + .expect("Failed to strip the root source directory prefix") + .to_string_lossy() + .to_string() + }) + .collect(), + ))); + let choice = editor.readline(&prompt("Which subdirectory should we use?"))?; + let src_dir = &PathBuf::from(choice + .parse() + .ok() + .and_then(|i: usize| sourceroot_candidates.get(i)) + .unwrap_or_else(|| &sourceroot_candidates[0])); + let source_root = if src_dir != &root_src_dir { + Some(src_dir) + } else { + None + }; + let source_root_expr = match source_root { + Some(subdir) => format!( + "\nsourceRoot = \"source/{}\";", + subdir + .strip_prefix(&root_src_dir) + .expect("Failed to strip the root source directory prefix") + .to_string_lossy() + ), + None => "".to_string(), + }; + let has_cargo = src_dir.join("Cargo.toml").is_file(); let cargo_lock = File::open(src_dir.join("Cargo.lock")); let has_cargo_lock = cargo_lock.is_ok(); @@ -405,6 +491,7 @@ async fn run() -> Result<()> { let has_pyproject = pyproject_toml.is_file(); let has_setuptools = src_dir.join("setup.py").is_file(); + let mut choices = Vec::new(); let rust_vendors = if has_cargo { if cargo_lock.map_or(true, |file| { BufReader::new(file) @@ -454,7 +541,6 @@ async fn run() -> Result<()> { } choices.push(BuildType::MkDerivation { rust: None }); - editor.set_helper(Some(Prompter::Build(choices))); let choice = editor.readline(&prompt("How should this package be built?"))?; let Some(Prompter::Build(choices)) = editor.helper_mut() else { @@ -564,7 +650,7 @@ async fn run() -> Result<()> { pname = {pname:?}; version = {version:?}; - src = {src_expr}; + src = {src_expr};{source_root_expr} vendorHash = {hash}; @@ -653,7 +739,7 @@ async fn run() -> Result<()> { version = {version:?}; format = "{format}"; - src = {src_expr}; + src = {src_expr};{source_root_expr} "#, if application { @@ -712,7 +798,7 @@ async fn run() -> Result<()> { pname = {pname:?}; version = {version:?}; - src = {src_expr}; + src = {src_expr};{source_root_expr} cargoHash = "{hash}"; @@ -742,7 +828,7 @@ async fn run() -> Result<()> { pname = "{pname}"; version = "{version}"; - src = {src_expr}; + src = {src_expr};{source_root_expr} cargoLock = "#, )?; @@ -762,6 +848,7 @@ async fn run() -> Result<()> { version = {version:?}; src = {src_expr}; + {source_root_expr} "#, )?; @@ -791,7 +878,7 @@ async fn run() -> Result<()> { pname = {pname:?}; version = {version:?}; - src = {src_expr}; + src = {src_expr};{source_root_expr} cargoDeps = rustPlatform.fetchCargoTarball {{ inherit src; @@ -824,7 +911,7 @@ async fn run() -> Result<()> { pname = "{pname}"; version = "{version}"; - src = {src_expr}; + src = {src_expr};{source_root_expr} cargoDeps = rustPlatform.importCargoLock "#, )?; @@ -908,7 +995,7 @@ async fn run() -> Result<()> { } let mut desc = desc.trim_matches(|c: char| !c.is_alphanumeric()).to_owned(); - desc.get_mut(0 .. 1).map(str::make_ascii_uppercase); + desc.get_mut(0..1).map(str::make_ascii_uppercase); write!(out, " ")?; writedoc!( out, @@ -1022,5 +1109,5 @@ fn get_version(rev: &str) -> &str { } fn get_version_number(rev: &str) -> &str { - &rev[rev.find(char::is_numeric).unwrap_or_default() ..] + &rev[rev.find(char::is_numeric).unwrap_or_default()..] } diff --git a/src/prompt.rs b/src/prompt.rs index bafb29e..7ea6c28 100644 --- a/src/prompt.rs +++ b/src/prompt.rs @@ -17,6 +17,7 @@ use crate::{ #[derive(Helper, Highlighter)] pub enum Prompter { Path(FilenameCompleter), + List(Vec), Revision(Revisions), NonEmpty, YesNo, @@ -49,6 +50,17 @@ impl Completer for Prompter { Prompter::Revision(revisions) => Ok((0, revisions.completions.clone())), Prompter::NonEmpty => Ok((0, Vec::new())), Prompter::YesNo => Ok((0, Vec::new())), + Prompter::List(choices) => Ok(( + 0, + choices + .iter() + .enumerate() + .map(|(i, choice)| Pair { + display: format!("{i} - {choice}"), + replacement: i.to_string(), + }) + .collect(), + )), Prompter::Build(choices) => Ok(( 0, choices @@ -114,6 +126,16 @@ impl Hinter for Prompter { Prompter::YesNo => None, + Prompter::List(choices) => Some(SimpleHint(if line.is_empty() { + format_args!(" ({})", choices[0]) + .blue() + .italic() + .to_string() + } else if let Some(choice) = line.parse().ok().and_then(|i: usize| choices.get(i)) { + format_args!(" ({choice})").blue().italic().to_string() + } else { + " press to see options".yellow().italic().to_string() + })), Prompter::Build(choices) => Some(SimpleHint(if line.is_empty() { format_args!(" ({})", choices[0]) .blue() @@ -157,6 +179,21 @@ impl Validator for Prompter { Prompter::YesNo => ValidationResult::Valid(None), + Prompter::List(choices) => { + let input = ctx.input(); + if input.is_empty() { + ValidationResult::Valid(Some(choices[0].to_string())) + } else if let Some(choice) = input + .parse::() + .ok() + .and_then(|choice| choices.get(choice)) + { + ValidationResult::Valid(Some(format!(" - {choice}"))) + } else { + ValidationResult::Invalid(None) + } + } + Prompter::Build(choices) => { let input = ctx.input(); if input.is_empty() {