use structopt::StructOpt;
use error::{Error, Result};
use std::process::Command;
use owo_colors::OwoColorize;
mod installer;
mod error;
mod cargo_info;
mod package;
mod build;
mod ftp;
mod tcp_listen;
mod ip_addr;
mod git_clone_wrappers;
mod game_paths;
mod update_std;
#[derive(StructOpt)]
enum SubCommands {
#[structopt(about = "Create a new plugin from a template")]
New {
name: String,
#[structopt(
short, long,
default_value = "https://github.com/ultimate-research/skyline-rs-template.git"
)]
template_git: String,
#[structopt(
short, long,
default_value = "master"
)]
template_git_branch: String,
},
#[structopt(about = "Check if the current plugin builds and emit any errors found")]
Check,
#[structopt(about = "Emit beginner-helpful lints and warnings")]
Clippy,
#[structopt(about = "Build the current plugin as an NRO")]
Build {
#[structopt(long)]
release: bool,
#[structopt(long)]
nso: bool,
#[structopt(long)]
no_default_features: bool,
#[structopt(long)]
features: Vec<String>,
args: Vec<String>
},
#[structopt(about = "Build the current plugin and install to a switch over FTP")]
Install {
#[structopt(short, long)]
debug: bool,
#[structopt(short, long)]
ip: Option<String>,
#[structopt(
short, long,
about = "Title ID of the game to install the plugin for, can be overriden in Cargo.toml",
)]
title_id: Option<String>,
#[structopt(
short, long,
about = "Install a project from a git url to the switch"
)]
git: Option<String>,
#[structopt(long)]
no_default_features: bool,
#[structopt(long)]
features: Vec<String>,
#[structopt(long)]
install_path: Option<String>
},
#[structopt(about = "Set the IP address of the switch to install to")]
SetIp {
ip: String
},
#[structopt(about = "Show the currently configured IP address")]
ShowIp,
#[structopt(about = "Install the current plugin and listen for skyline logging")]
Run {
#[structopt(short, long)]
debug: bool,
#[structopt(short, long)]
restart: bool,
#[structopt(short, long)]
ip: Option<String>,
#[structopt(
short, long,
about = "Title ID of the game to install the plugin for, can be overriden in Cargo.toml",
)]
title_id: Option<String>,
#[structopt(long)]
no_default_features: bool,
#[structopt(long)]
features: Vec<String>,
#[structopt(long)]
install_path: Option<String>
},
#[structopt(about = "Install the current plugin and listen for skyline logging")]
Restart {
#[structopt(short, long)]
ip: Option<String>,
#[structopt(
short, long,
about = "Title ID of the game to install the plugin for, can be overriden in Cargo.toml",
)]
title_id: Option<String>
},
#[structopt(about = "Download the latest stdlib for aarch64-skyline-switch")]
UpdateStd {
#[structopt(short, long, default_value = "skyline-rs/rust")]
repo: String,
},
#[structopt(about = "Listen for logs being output from a switch running skyline at the given ip")]
Listen {
#[structopt(short, long)]
ip: Option<String>,
},
#[structopt(about = "List the files in the plugin directory for the given game")]
List {
#[structopt(short, long)]
ip: Option<String>,
#[structopt(
short, long,
about = "Title ID of the game to list the installed plugins for, can be overriden in Cargo.toml",
)]
title_id: Option<String>,
path: Option<String>
},
#[structopt(about = "Delete a file in the plugin directory for the given game")]
Rm {
#[structopt(short, long)]
ip: Option<String>,
#[structopt(
short, long,
about = "Title ID of the game to list the installed plugins for, can be overriden in Cargo.toml",
)]
title_id: Option<String>,
filename: Option<String>
},
#[structopt(about = "Copy a file over FTP")]
Cp {
#[structopt(short, long)]
ip: Option<String>,
#[structopt(
short, long,
about = "Title ID of the game to list the installed plugins for, can be overriden in Cargo.toml",
)]
title_id: Option<String>,
src: String,
dest: String
},
#[structopt(about = "Update cargo-skyline command")]
SelfUpdate {
#[structopt(short, long, default_value = "https://github.com/jam1garner/cargo-skyline")]
git: String,
#[structopt(short, long)]
from_master: bool,
},
#[structopt(about = "Package plugin and latest Skyline into a zip file to prepare it for release")]
Package {
#[structopt(
short, long,
default_value = "https://github.com/skyline-dev/skyline/releases/download/beta/skyline.zip"
)]
skyline_release: String,
#[structopt(
short, long,
about = "Disable the inclusion of skyline into the package"
)]
no_skyline: bool,
#[structopt(
short, long,
about = "Title ID of the game to package the plugin for",
)]
title_id: Option<String>,
#[structopt(
short, long,
about = "Path to output zip to",
default_value = "target/release.zip"
)]
out_path: String,
},
#[structopt(about = "Update libraries for current plugin folder")]
Update,
#[structopt(about = "Document the current plugin and its dependencies")]
Doc {
#[structopt(
short, long,
about = "Whether or not to open the docs in the default browser afterwards",
)]
open: bool,
},
#[structopt(setting(structopt::clap::AppSettings::Hidden))]
CleanProject,
}
#[derive(StructOpt)]
#[structopt(bin_name = "cargo")]
enum Args {
Skyline(SubCommands)
}
fn main() {
let Args::Skyline(subcommand) = Args::from_args();
use SubCommands::*;
let result = match subcommand {
Install { ip, title_id, debug, git, features, no_default_features, install_path } => if let Some(git) = git {
installer::from_git(&git, ip, title_id, !debug, features, install_path, no_default_features)
} else {
installer::install(ip, title_id, !debug, features, install_path, no_default_features)
},
SetIp { ip } => ip_addr::set_ip(ip),
ShowIp => ip_addr::show_ip(),
Build { args, release, nso, features, no_default_features }
=> build::build(args, release, nso, features, no_default_features),
Check => build::check(),
Clippy => build::clippy(),
Run { ip, title_id, debug, restart , features, install_path, no_default_features }
=> installer::install_and_run(ip, title_id, !debug, restart, features, install_path, no_default_features),
Restart { ip, title_id } => installer::restart_game(ip, title_id),
New { name, template_git, template_git_branch } => git_clone_wrappers::new_plugin(name, template_git, template_git_branch),
UpdateStd { repo } => update_std::update_std(&repo),
Listen { ip } => tcp_listen::listen(ip),
List { ip, title_id, path } => installer::list(ip, title_id, path),
Rm { ip, title_id, filename } => installer::rm(ip, title_id, filename),
Cp { ip, title_id, src, dest } => installer::cp(ip, title_id, src, dest),
SelfUpdate { from_master, git } => self_update(from_master, git),
Package { skyline_release, title_id, out_path, no_skyline }
=> package::package(&skyline_release, title_id.as_deref(), &out_path, !no_skyline),
Update => update(),
Doc { open } => build::doc(if open { vec!["--open".into()] } else { vec![] }),
CleanProject => clean_project(),
};
if let Err(err) = result {
match err {
Error::NoIpFound => eprintln!("{}: {}", "ERROR".red(), error::NO_IP),
Error::BadIpAddr => eprintln!("{}: {}", "ERROR".red(), error::BAD_IP_ADDR),
Error::FtpError(ftp_err) => {
eprintln!("{}{}","An FTP Error Occurred: ".red(), ftp_err)
}
Error::NoHomeDir => eprintln!("{}: No home directory could be found", "ERROR".red()),
Error::NoPathFound => eprintln!("{}: No environment variable PATH could be found.", "ERROR".red()),
Error::CreateSwitchDirDenied
=> eprintln!("{}: Could not create $HOME/.switch", "ERROR".red()),
Error::WriteIpDenied => eprintln!("{}: Could not write IP to file", "ERROR".red()),
Error::NoTitleId => error::no_title_id(),
Error::FailParseCargoStream => eprintln!("{}", "Unable to parse cargo output stream"),
Error::CargoError(err) => eprintln!("{}{}", "CargoError: ".red(), err),
Error::ExitStatus(code) => std::process::exit(code),
Error::FailWriteNro => eprintln!("{}: Unable to convert file from ELF to NRO", "ERROR".red()),
Error::IoError(err) => eprintln!("{}{}", "IoError: ".red(), err),
Error::DownloadError => eprintln!("{}: Failed to download latest release of Skyline. An internet connection is required.", "ERROR".red()),
Error::ZipError => eprintln!("{}: Failed to read Skyline release zip. Either corrupted or missing files.", "ERROR".red()),
Error::NoNpdmFileFound => eprintln!("{}: Custom NPDM file specified in Cargo.toml not found at the specified path.", "ERROR".red()),
Error::AbsSwitchPath => eprintln!("{}: Absolute Switch paths must be prepended with \"sd:/\"", "ERROR".red()),
Error::BadSdPath => eprintln!("{}: Install paths must either start with \"rom:/\" or \"sd:/\"", "ERROR".red()),
Error::GithubError => eprintln!("{}: failed to get the latest release from github", "ERROR".red()),
Error::InvalidRepo => eprintln!("{}: repos must be in the form of `{{user}}/{{repo}}`", "ERROR".red()),
Error::HostNotSupported => eprintln!("{}: your host platform is not supported.", "ERROR".red()),
Error::DownloadFailed => eprintln!("{}: the update failed to download.", "ERROR".red()),
Error::RustupNotFound => eprintln!("{}: rustup could not be executed, make sure it is installed.", "ERROR".red()),
Error::RustupLinkFailed => eprintln!("{}: rustup could not link the skyline toolchain.", "ERROR".red()),
}
std::process::exit(1);
}
}
fn self_update(from_master: bool, git: String) -> Result<()> {
let mut args = vec!["install", "--force"];
if from_master {
args.push("--git");
args.push(&git);
} else {
args.push("cargo-skyline");
}
Command::new("cargo")
.args(&args)
.status()
.unwrap();
Ok(())
}
fn update() -> Result<()> {
Command::new("cargo")
.arg("update")
.status()?;
Ok(())
}
use std::path::Path;
use std::fs;
const DEFAULT_CONFIG: &str = "[build]\ntarget = \"aarch64-skyline-switch\"";
fn clean_project() -> Result<()> {
if Path::new("rust-toolchain").exists() {
fs::write("rust-toolchain", "stable").unwrap();
}
let delete_config = fs::read_to_string(".cargo/config")
.ok()
.map(|config| config.trim() == DEFAULT_CONFIG)
.unwrap_or(false);
if delete_config {
fs::remove_file(".cargo/config").unwrap();
fs::remove_dir(".cargo").unwrap();
}
let _ = fs::remove_file("Xargo.toml");
let _ = fs::remove_file("Cargo.lock");
let _ = fs::remove_file("aarch64-skyline-switch.json");
Command::new("cargo").arg("clean").status()?;
Ok(())
}