use cargo_metadata::{camino::Utf8PathBuf, Message};
use cfg_if::cfg_if;
use fs::PathExt;
use fs_err as fs;
use std::{
io::{self, ErrorKind},
path::{Path, PathBuf},
process::{exit, Child, Command, Stdio},
};
fn cargo_bin() -> std::ffi::OsString {
std::env::var_os("CARGO").unwrap_or_else(|| "cargo".to_owned().into())
}
pub trait CommandExt {
fn spawn_handling_not_found(&mut self) -> io::Result<Child>;
}
impl CommandExt for Command {
fn spawn_handling_not_found(&mut self) -> io::Result<Child> {
let command_name = self.get_program().to_string_lossy().to_string();
self.spawn().map_err(|err| match err.kind() {
ErrorKind::NotFound => {
eprintln!("error: command `{}` not found", command_name);
#[cfg(feature = "legacy-pros-rs-support")]
{
eprintln!(
"Please refer to the documentation for installing pros-rs' dependencies on your platform."
);
eprintln!("> https://github.com/vexide/pros-rs#compiling");
}
#[cfg(not(feature = "legacy-pros-rs-support"))]
{
eprintln!("Please refer to the documentation for installing vexide's dependencies on your platform.");
eprintln!("> https://github.com/vexide/vexide#compiling");
}
exit(1);
}
_ => err,
})
}
}
const TARGET_PATH: &str = "armv7a-vexos-eabi.json";
pub fn build(
path: PathBuf,
args: Vec<String>,
for_simulator: bool,
mut handle_executable: impl FnMut(Utf8PathBuf),
) {
let target_path = path.join(TARGET_PATH);
let mut build_cmd = Command::new(cargo_bin());
build_cmd
.current_dir(&path)
.arg("build")
.arg("--message-format")
.arg("json-render-diagnostics")
.arg("--manifest-path")
.arg(format!("{}/Cargo.toml", path.display()));
if !is_nightly_toolchain() {
eprintln!("ERROR: pros-rs requires Nightly Rust features, but you're using stable.");
eprintln!(" hint: this can be fixed by running `rustup override set nightly`");
exit(1);
}
if for_simulator {
if !has_wasm_target() {
eprintln!(
"ERROR: simulation requires the wasm32-unknown-unknown target to be installed"
);
eprintln!(
" hint: this can be fixed by running `rustup target add wasm32-unknown-unknown`"
);
exit(1);
}
build_cmd
.arg("--target")
.arg("wasm32-unknown-unknown")
.arg("-Zbuild-std=std,panic_abort")
.arg("--config=build.rustflags=['-Ctarget-feature=+atomics,+bulk-memory,+mutable-globals','-Clink-arg=--shared-memory','-Clink-arg=--export-table']")
.stdout(Stdio::piped());
} else {
#[cfg(feature = "legacy-pros-rs-support")]
let target = include_str!("targets/pros-rs.json");
#[cfg(not(feature = "legacy-pros-rs-support"))]
let target = include_str!("targets/vexide.json");
if !target_path.exists() {
fs::create_dir_all(target_path.parent().unwrap()).unwrap();
fs::write(&target_path, target).unwrap();
}
build_cmd.arg("--target");
build_cmd.arg(&target_path);
build_cmd
.arg("-Zbuild-std=core,alloc,compiler_builtins")
.stdout(Stdio::piped());
}
build_cmd.args(args);
let mut out = build_cmd.spawn_handling_not_found().unwrap();
let reader = std::io::BufReader::new(out.stdout.take().unwrap());
for message in Message::parse_stream(reader) {
if let Message::CompilerArtifact(artifact) = message.unwrap() {
if let Some(binary_path) = artifact.executable {
handle_executable(binary_path);
}
}
}
}
#[cfg(target_os = "windows")]
fn find_objcopy_path_windows() -> Option<String> {
let arm_install_path =
PathBuf::from("C:\\Program Files (x86)\\Arm GNU Toolchain arm-none-eabi");
let mut versions = fs::read_dir(arm_install_path).ok()?;
let install = versions.next()?.ok()?.path();
let path = install.join("bin").join("arm-none-eabi-objcopy.exe");
Some(path.to_string_lossy().to_string())
}
#[cfg(feature = "legacy-pros-rs-support")]
fn objcopy_path() -> String {
#[cfg(target_os = "windows")]
let objcopy_path = find_objcopy_path_windows();
#[cfg(not(target_os = "windows"))]
let objcopy_path = None;
objcopy_path.unwrap_or_else(|| "arm-none-eabi-objcopy".to_owned())
}
#[cfg(feature = "legacy-pros-rs-support")]
pub fn finish_binary(bin: Utf8PathBuf) {
println!("Stripping Binary: {}", bin.clone());
let objcopy = objcopy_path();
let strip = std::process::Command::new(&objcopy)
.args([
"--strip-symbol=install_hot_table",
"--strip-symbol=__libc_init_array",
"--strip-symbol=_PROS_COMPILE_DIRECTORY",
"--strip-symbol=_PROS_COMPILE_TIMESTAMP",
"--strip-symbol=_PROS_COMPILE_TIMESTAMP_INT",
bin.as_str(),
&format!("{}.stripped", bin),
])
.spawn_handling_not_found()
.unwrap();
strip.wait_with_output().unwrap();
let elf_to_bin = std::process::Command::new(&objcopy)
.args([
"-O",
"binary",
"-R",
".hot_init",
&format!("{}.stripped", bin),
&format!("{}.bin", bin),
])
.spawn_handling_not_found()
.unwrap();
elf_to_bin.wait_with_output().unwrap();
}
#[cfg(not(feature = "legacy-pros-rs-support"))]
pub fn finish_binary(bin: Utf8PathBuf) {
println!("Stripping Binary: {}", bin.clone());
Command::new("rust-objcopy")
.args(["-O", "binary", bin.as_str(), &format!("{}.bin", bin)])
.spawn_handling_not_found()
.unwrap();
println!("Output binary: {}.bin", bin.clone());
}
fn is_nightly_toolchain() -> bool {
let rustc = std::process::Command::new("rustc")
.arg("--version")
.output()
.unwrap();
let rustc = String::from_utf8(rustc.stdout).unwrap();
rustc.contains("nightly")
}
fn has_wasm_target() -> bool {
let Ok(rustup) = std::process::Command::new("rustup")
.arg("target")
.arg("list")
.arg("--installed")
.output()
else {
return true;
};
let rustup = String::from_utf8(rustup.stdout).unwrap();
rustup.contains("wasm32-unknown-unknown")
}
#[cfg(target_os = "windows")]
fn find_simulator_path_windows() -> Option<String> {
let wix_path = PathBuf::from(r#"C:\Program Files\PROS Simulator\PROS Simulator.exe"#);
if wix_path.exists() {
return Some(wix_path.to_string_lossy().to_string());
}
let nsis_path = PathBuf::from(std::env::var("LOCALAPPDATA").unwrap())
.join("PROS Simulator")
.join("PROS Simulator.exe");
if nsis_path.exists() {
return Some(nsis_path.to_string_lossy().to_string());
}
None
}
fn find_simulator() -> Command {
cfg_if! {
if #[cfg(target_os = "macos")] {
let mut cmd = Command::new("open");
cmd.args(["-nWb", "rs.pros.simulator", "--args"]);
cmd
} else if #[cfg(target_os = "windows")] {
Command::new(find_simulator_path_windows().expect("Simulator install not found"))
} else {
Command::new("pros-simulator")
}
}
}
pub fn launch_simulator(ui: Option<String>, workspace_dir: &Path, binary_path: &Path) {
let mut command = if let Some(ui) = ui {
Command::new(ui)
} else {
find_simulator()
};
command
.arg("--code")
.arg(binary_path.fs_err_canonicalize().unwrap())
.arg(workspace_dir.fs_err_canonicalize().unwrap());
let command_name = command.get_program().to_string_lossy().to_string();
let args = command
.get_args()
.map(|arg| arg.to_string_lossy().to_string())
.collect::<Vec<_>>();
eprintln!("$ {} {}", command_name, args.join(" "));
let res = command
.spawn()
.map_err(|err| match err.kind() {
ErrorKind::NotFound => {
eprintln!("Failed to start simulator:");
eprintln!("error: command `{command_name}` not found");
eprintln!();
eprintln!("Please install PROS Simulator using the link below.");
eprintln!("> https://github.com/pros-rs/pros-simulator-gui/releases");
exit(1);
}
_ => err,
})
.unwrap()
.wait();
if let Err(err) = res {
eprintln!("Failed to launch simulator: {}", err);
}
}