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 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}