cargo-xcode 1.11.0

Make Xcode project files from Cargo projects
Documentation
use cargo_metadata::{Package, Target};
use getopts::Options;
use std::env;
use std::path::{PathBuf, Path};
use std::process::ExitCode;

fn main() -> ExitCode {
    let mut opts = Options::new();
    opts.optopt("", "manifest-path", "Location of the Rust/Cargo project to convert", "Cargo.toml");
    opts.optopt("o", "output-dir", "Where to write xcodeproj to (default: same directory as the crate)", "");
    opts.optopt("", "features", "Default Cargo features to always enable", "");
    opts.optflag("", "no-default-features", "Don't enable the `default` Cargo feature");
    opts.optflag("", "skip-install", "Set SKIP_INSTALL=YES for embedding of bins and dylibs");
    opts.optopt("", "platforms", "Space-separated list of supported Apple SDKs", "\"macosx iphoneos iphonesimulator watchos watchsimulator xros\"");
    opts.optmulti("", "xcconfig", "Base configuration file. Specify twice for separate debug and release configs", "base.xcconfig");
    opts.optopt("", "project-name", "Override crate name to use a differnet name in Xcode", "");
    opts.optflag("", "nightly", "Use nightly rustup toolchain to build");
    opts.optflag("h", "help", "This help.");
    let matches = match opts.parse(env::args().skip(1)) {
        Ok(m) => m,
        Err(f) => {
            eprintln!("error: {f}");
            return ExitCode::FAILURE;
        },
    };

    if matches.opt_present("help") {
        println!("{}", opts.usage("cargo-xcode generates Xcode project files for Cargo crates"));
        return ExitCode::SUCCESS;
    }

    let mut path = matches.opt_str("manifest-path").map(PathBuf::from);
    let output_dir = matches.opt_str("output-dir");
    let custom_project_name = matches.opt_str("project-name");
    let skip_install = matches.opt_present("skip-install");
    let features = matches.opt_str("features");
    let default_features = !matches.opt_present("no-default-features");
    let nightly = matches.opt_present("nightly");
    let xcconfigs = matches.opt_strs("xcconfig");
    let platforms = matches.opt_str("platforms").map(|p| {
        p.split(|c: char| c.is_ascii_whitespace() || c == ',').filter(|s| !s.is_empty()).map(Into::into).collect()
    });

    let skip_cargo_arg = if matches.free.first().map(|s| s.as_str()) == Some("xcode") { 1 } else { 0 };
    for arg in matches.free.into_iter().skip(skip_cargo_arg) {
        if path.is_some() {
            eprintln!("{}", opts.usage(&format!("error: unexpected argument '{arg}'")));
            return ExitCode::FAILURE;
        }

        let arg_path = PathBuf::from(arg);
        path = Some(if arg_path.is_file() { arg_path } else { arg_path.join("Cargo.toml") });
    }

    let mut cmd = cargo_metadata::MetadataCommand::new();
    cmd.no_deps();
    if let Some(ref path) = path {
        cmd.manifest_path(path);
    }
    let meta = match cmd.exec() {
        Ok(m) => m,
        Err(cargo_metadata::Error::Io(e)) => {
            eprintln!("error: Can't run `cargo metadata` for '{}'\n{e}", path.unwrap_or_else(|| env::current_dir().unwrap()).display());
            if e.kind() == std::io::ErrorKind::NotFound {
                eprintln!("  make sure `cargo` is in your PATH ({})", std::env::var("PATH").unwrap_or_default());
            }
            return ExitCode::FAILURE;
        },
        Err(e) => {
            eprintln!("error: Can't parse cargo metadata in '{}'\n{e}", path.unwrap_or_else(|| env::current_dir().unwrap()).display());
            return ExitCode::FAILURE;
        },
    };

    let ok = meta.packages
        .into_iter()
        .filter_map(filter_package)
        .map(move |p| {
            let mut g = cargo_xcode::Generator::new(p, output_dir.as_ref().map(Path::new), custom_project_name.clone()).expect("Manifest path");
            g.skip_install(skip_install);
            g.nightly(nightly);
            g.xcconfigs(xcconfigs.clone());
            g.platforms(platforms.clone());
            if let Some(f) = &features { g.features(f, default_features); }
            let p = g.write_pbxproj().expect("Project writing");
            println!("OK:\n{}", p.display());
        })
        .count();

    if ok == 0 {
        eprintln!(r#"error: No Rust crates found with crate-type "staticlib" or "cdylib"\nAdd `[lib] crate-type = ["lib", "cdylib"]` or add binaries"#);
        return ExitCode::FAILURE;
    }

    ExitCode::SUCCESS
}

fn filter_package(mut package: Package) -> Option<Package> {
    package.targets.retain(is_relevant_target);
    if package.targets.is_empty() {
        None
    } else {
        Some(package)
    }
}

fn is_relevant_target(target: &Target) -> bool {
    target.kind.iter().any(|k| k == "bin" || k == "staticlib" || k == "cdylib")
}