cargo_pros/
lib.rs

1use cargo_metadata::{camino::Utf8PathBuf, Message};
2use cfg_if::cfg_if;
3use fs::PathExt;
4use fs_err as fs;
5use std::{
6    io::{self, ErrorKind},
7    path::{Path, PathBuf},
8    process::{exit, Child, Command, Stdio},
9};
10
11fn cargo_bin() -> std::ffi::OsString {
12    std::env::var_os("CARGO").unwrap_or_else(|| "cargo".to_owned().into())
13}
14
15pub trait CommandExt {
16    fn spawn_handling_not_found(&mut self) -> io::Result<Child>;
17}
18
19impl CommandExt for Command {
20    fn spawn_handling_not_found(&mut self) -> io::Result<Child> {
21        let command_name = self.get_program().to_string_lossy().to_string();
22        self.spawn().map_err(|err| match err.kind() {
23            ErrorKind::NotFound => {
24                eprintln!("error: command `{}` not found", command_name);
25                #[cfg(feature = "legacy-pros-rs-support")]
26                {
27                    eprintln!(
28                        "Please refer to the documentation for installing pros-rs' dependencies on your platform."
29                    );
30                    eprintln!("> https://github.com/vexide/pros-rs#compiling");
31                }
32                #[cfg(not(feature = "legacy-pros-rs-support"))]
33                {
34                    eprintln!("Please refer to the documentation for installing vexide's dependencies on your platform.");
35                    eprintln!("> https://github.com/vexide/vexide#compiling");
36                }
37                exit(1);
38            }
39            _ => err,
40        })
41    }
42}
43
44const TARGET_PATH: &str = "armv7a-vexos-eabi.json";
45
46pub fn build(
47    path: PathBuf,
48    args: Vec<String>,
49    for_simulator: bool,
50    mut handle_executable: impl FnMut(Utf8PathBuf),
51) {
52    let target_path = path.join(TARGET_PATH);
53    let mut build_cmd = Command::new(cargo_bin());
54    build_cmd
55        .current_dir(&path)
56        .arg("build")
57        .arg("--message-format")
58        .arg("json-render-diagnostics")
59        .arg("--manifest-path")
60        .arg(format!("{}/Cargo.toml", path.display()));
61
62    if !is_nightly_toolchain() {
63        eprintln!("ERROR: pros-rs requires Nightly Rust features, but you're using stable.");
64        eprintln!(" hint: this can be fixed by running `rustup override set nightly`");
65        exit(1);
66    }
67
68    if for_simulator {
69        if !has_wasm_target() {
70            eprintln!(
71                "ERROR: simulation requires the wasm32-unknown-unknown target to be installed"
72            );
73            eprintln!(
74                " hint: this can be fixed by running `rustup target add wasm32-unknown-unknown`"
75            );
76            exit(1);
77        }
78
79        build_cmd
80            .arg("--target")
81            .arg("wasm32-unknown-unknown")
82            .arg("-Zbuild-std=std,panic_abort")
83            .arg("--config=build.rustflags=['-Ctarget-feature=+atomics,+bulk-memory,+mutable-globals','-Clink-arg=--shared-memory','-Clink-arg=--export-table']")
84            .stdout(Stdio::piped());
85    } else {
86        #[cfg(feature = "legacy-pros-rs-support")]
87        let target = include_str!("targets/pros-rs.json");
88        #[cfg(not(feature = "legacy-pros-rs-support"))]
89        let target = include_str!("targets/vexide.json");
90        if !target_path.exists() {
91            fs::create_dir_all(target_path.parent().unwrap()).unwrap();
92            fs::write(&target_path, target).unwrap();
93        }
94        build_cmd.arg("--target");
95        build_cmd.arg(&target_path);
96
97        build_cmd
98            .arg("-Zbuild-std=core,alloc,compiler_builtins")
99            .stdout(Stdio::piped());
100    }
101
102    build_cmd.args(args);
103
104    let mut out = build_cmd.spawn_handling_not_found().unwrap();
105    let reader = std::io::BufReader::new(out.stdout.take().unwrap());
106    for message in Message::parse_stream(reader) {
107        if let Message::CompilerArtifact(artifact) = message.unwrap() {
108            if let Some(binary_path) = artifact.executable {
109                handle_executable(binary_path);
110            }
111        }
112    }
113}
114
115#[cfg(target_os = "windows")]
116fn find_objcopy_path_windows() -> Option<String> {
117    let arm_install_path =
118        PathBuf::from("C:\\Program Files (x86)\\Arm GNU Toolchain arm-none-eabi");
119    let mut versions = fs::read_dir(arm_install_path).ok()?;
120    let install = versions.next()?.ok()?.path();
121    let path = install.join("bin").join("arm-none-eabi-objcopy.exe");
122    Some(path.to_string_lossy().to_string())
123}
124
125#[cfg(feature = "legacy-pros-rs-support")]
126fn objcopy_path() -> String {
127    #[cfg(target_os = "windows")]
128    let objcopy_path = find_objcopy_path_windows();
129
130    #[cfg(not(target_os = "windows"))]
131    let objcopy_path = None;
132
133    objcopy_path.unwrap_or_else(|| "arm-none-eabi-objcopy".to_owned())
134}
135
136#[cfg(feature = "legacy-pros-rs-support")]
137pub fn finish_binary(bin: Utf8PathBuf) {
138    println!("Stripping Binary: {}", bin.clone());
139    let objcopy = objcopy_path();
140    let strip = std::process::Command::new(&objcopy)
141        .args([
142            "--strip-symbol=install_hot_table",
143            "--strip-symbol=__libc_init_array",
144            "--strip-symbol=_PROS_COMPILE_DIRECTORY",
145            "--strip-symbol=_PROS_COMPILE_TIMESTAMP",
146            "--strip-symbol=_PROS_COMPILE_TIMESTAMP_INT",
147            bin.as_str(),
148            &format!("{}.stripped", bin),
149        ])
150        .spawn_handling_not_found()
151        .unwrap();
152    strip.wait_with_output().unwrap();
153    let elf_to_bin = std::process::Command::new(&objcopy)
154        .args([
155            "-O",
156            "binary",
157            "-R",
158            ".hot_init",
159            &format!("{}.stripped", bin),
160            &format!("{}.bin", bin),
161        ])
162        .spawn_handling_not_found()
163        .unwrap();
164    elf_to_bin.wait_with_output().unwrap();
165}
166
167#[cfg(not(feature = "legacy-pros-rs-support"))]
168pub fn finish_binary(bin: Utf8PathBuf) {
169    println!("Stripping Binary: {}", bin.clone());
170    Command::new("rust-objcopy")
171        .args(["-O", "binary", bin.as_str(), &format!("{}.bin", bin)])
172        .spawn_handling_not_found()
173        .unwrap();
174    println!("Output binary: {}.bin", bin.clone());
175}
176
177fn is_nightly_toolchain() -> bool {
178    let rustc = std::process::Command::new("rustc")
179        .arg("--version")
180        .output()
181        .unwrap();
182    let rustc = String::from_utf8(rustc.stdout).unwrap();
183    rustc.contains("nightly")
184}
185
186fn has_wasm_target() -> bool {
187    let Ok(rustup) = std::process::Command::new("rustup")
188        .arg("target")
189        .arg("list")
190        .arg("--installed")
191        .output()
192    else {
193        return true;
194    };
195    let rustup = String::from_utf8(rustup.stdout).unwrap();
196    rustup.contains("wasm32-unknown-unknown")
197}
198
199#[cfg(target_os = "windows")]
200fn find_simulator_path_windows() -> Option<String> {
201    let wix_path = PathBuf::from(r#"C:\Program Files\PROS Simulator\PROS Simulator.exe"#);
202    if wix_path.exists() {
203        return Some(wix_path.to_string_lossy().to_string());
204    }
205    // C:\Users\USER\AppData\Local\PROS Simulator
206    let nsis_path = PathBuf::from(std::env::var("LOCALAPPDATA").unwrap())
207        .join("PROS Simulator")
208        .join("PROS Simulator.exe");
209    if nsis_path.exists() {
210        return Some(nsis_path.to_string_lossy().to_string());
211    }
212    None
213}
214
215fn find_simulator() -> Command {
216    cfg_if! {
217        if #[cfg(target_os = "macos")] {
218            let mut cmd = Command::new("open");
219            cmd.args(["-nWb", "rs.pros.simulator", "--args"]);
220            cmd
221        } else if #[cfg(target_os = "windows")] {
222            Command::new(find_simulator_path_windows().expect("Simulator install not found"))
223        } else {
224            Command::new("pros-simulator")
225        }
226    }
227}
228
229pub fn launch_simulator(ui: Option<String>, workspace_dir: &Path, binary_path: &Path) {
230    let mut command = if let Some(ui) = ui {
231        Command::new(ui)
232    } else {
233        find_simulator()
234    };
235    command
236        .arg("--code")
237        .arg(binary_path.fs_err_canonicalize().unwrap())
238        .arg(workspace_dir.fs_err_canonicalize().unwrap());
239
240    let command_name = command.get_program().to_string_lossy().to_string();
241    let args = command
242        .get_args()
243        .map(|arg| arg.to_string_lossy().to_string())
244        .collect::<Vec<_>>();
245
246    eprintln!("$ {} {}", command_name, args.join(" "));
247
248    let res = command
249        .spawn()
250        .map_err(|err| match err.kind() {
251            ErrorKind::NotFound => {
252                eprintln!("Failed to start simulator:");
253                eprintln!("error: command `{command_name}` not found");
254                eprintln!();
255                eprintln!("Please install PROS Simulator using the link below.");
256                eprintln!("> https://github.com/pros-rs/pros-simulator-gui/releases");
257                exit(1);
258            }
259            _ => err,
260        })
261        .unwrap()
262        .wait();
263    if let Err(err) = res {
264        eprintln!("Failed to launch simulator: {}", err);
265    }
266}