1#![feature(exitcode_exit_method)]
7
8use argp::{FromArgValue, FromArgs};
9use std::{
10 ffi::OsStr,
11 fmt::Display,
12 path::{Path, PathBuf},
13 process::{Command, ExitCode},
14};
15
16mod tools;
17
18fn graceful_error_exit(msg: impl Display) -> ! {
19 eprintln!("Exit failure.\n{msg}");
20 ExitCode::FAILURE.exit_process()
21}
22
23mod fields {
24 use super::*;
25
26 #[derive(Clone, Copy)]
27 pub enum Platform {
28 Gamecube,
29 }
30
31 impl Platform {
32 pub fn target_json_name(self) -> &'static str {
33 match self {
34 Platform::Gamecube => "gamecube.json",
35 }
36 }
37
38 pub fn config_toml_name(self) -> &'static str {
39 match self {
40 Platform::Gamecube => "gamecube.toml",
41 }
42 }
43 }
44
45 impl FromArgValue for Platform {
46 fn from_arg_value(value: &std::ffi::OsStr) -> Result<Self, String> {
47 let str = value.to_str().ok_or("invalid UTF-8 string".to_string())?;
48 Ok(match str {
49 "gamecube" | "gc" => Self::Gamecube,
50 _ => return Err("expected a valid platform.".to_string()),
51 })
52 }
53 }
54
55 #[derive(Default, Clone, Copy)]
56 pub enum OutputType {
57 #[default]
58 Elf,
59 Dol,
60 }
61
62 impl FromArgValue for OutputType {
63 fn from_arg_value(value: &std::ffi::OsStr) -> Result<Self, String> {
64 let str = value.to_str().ok_or("invalid UTF-8 string".to_string())?;
65 Ok(match str {
66 "elf" => Self::Elf,
67 "dol" => Self::Dol,
68 _ => return Err("expected a valid output type.".to_string()),
69 })
70 }
71 }
72
73 impl OutputType {
74 pub fn supports_platform(self, platform: Platform) -> bool {
75 #[allow(unreachable_patterns)]
76 #[allow(clippy::match_like_matches_macro)]
77 match (self, platform) {
78 (Self::Elf, _) | (Self::Dol, Platform::Gamecube) => true,
79 _ => false,
80 }
81 }
82
83 pub fn extension_name(self) -> &'static str {
84 match self {
85 OutputType::Elf => "",
86 OutputType::Dol => ".dol",
87 }
88 }
89 }
90}
91
92#[derive(FromArgs)]
94#[argp(subcommand, name = "build")]
95struct RbrewCliSubBuild {
96 #[argp(option)]
99 platform: fields::Platform,
100 #[argp(option, default = "Default::default()")]
102 output_type: fields::OutputType,
103 #[argp(switch)]
105 workspace: bool,
106 #[argp(option)]
108 package: Option<String>,
109 #[argp(option)]
111 output_directory: Option<PathBuf>,
112 #[argp(option)]
114 custom_options: Vec<String>,
115}
116
117#[derive(FromArgs)]
119#[argp(subcommand, name = "tools")]
120struct RbrewCliSubTools {}
121
122#[derive(FromArgs)]
123#[argp(subcommand)]
124enum RbrewCliSub {
125 Build(RbrewCliSubBuild),
126 Tools(RbrewCliSubTools),
127}
128
129#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
130enum Verbosity {
131 Quiet,
132 Normal,
133 Verbose,
134}
135
136impl Verbosity {
137 fn should_output(self, level: Self) -> bool {
138 self >= level
139 }
140}
141
142impl Default for Verbosity {
143 fn default() -> Self {
144 Self::Normal
145 }
146}
147
148impl FromArgValue for Verbosity {
149 fn from_arg_value(value: &std::ffi::OsStr) -> Result<Self, String> {
150 Ok(match value.to_str() {
151 Some("quiet") => Self::Quiet,
152 Some("normal") => Self::Normal,
153 Some("verbose") => Self::Verbose,
154 _ => return Err("expected 'quiet', 'normal' or 'verbose'.".to_string()),
155 })
156 }
157}
158
159#[derive(FromArgs)]
161pub struct RbrewCli {
162 #[argp(option, default = "Default::default()")]
164 verbosity: Verbosity,
165
166 #[argp(subcommand)]
167 subcommand: RbrewCliSub,
168}
169
170mod util {
171 use super::*;
172
173 pub fn cargo() -> Command {
174 Command::new("cargo")
175 }
176
177 fn try_path(path: PathBuf) -> Result<PathBuf, std::io::Error> {
178 if path.exists() {
179 Ok(path)
180 } else {
181 Err(std::io::Error::new(
182 std::io::ErrorKind::NotFound,
183 format!("unable to find path '{path:?}'"),
184 ))
185 }
186 }
187
188 pub fn rbrew_target_file(name: &str) -> Result<PathBuf, std::io::Error> {
189 try_path(PathBuf::from(format!(
190 "{}/{name}",
191 include_str!(concat!(env!("OUT_DIR"), "/target_path.inc"))
192 )))
193 }
194
195 pub fn rbrew_config_file(name: &str) -> Result<PathBuf, std::io::Error> {
196 try_path(PathBuf::from(format!(
197 "{}/{name}",
198 include_str!(concat!(env!("OUT_DIR"), "/config_path.inc"))
199 )))
200 }
201}
202
203pub fn run(cli: RbrewCli) {
204 match cli.subcommand {
205 RbrewCliSub::Build(args) => build(args, cli.verbosity),
206 RbrewCliSub::Tools(args) => tools(args, cli.verbosity),
207 }
208}
209
210fn build(args: RbrewCliSubBuild, verbosity: Verbosity) {
211 if !args.output_type.supports_platform(args.platform) {
212 graceful_error_exit("output type does not support platform. See `--help`.")
213 }
214
215 let mut cmd = util::cargo();
216 cmd.arg("build");
217 if let Some(package) = &args.package {
218 cmd.arg("--package").arg(package);
219 }
220 if args.workspace {
221 cmd.arg("--workspace");
222 }
223
224 let target_json_ident = args.platform.target_json_name();
225 let target_json = match util::rbrew_target_file(target_json_ident) {
226 Ok(ok) => ok,
227 Err(err) => graceful_error_exit(format!(
228 "failed to find the target json file for the platform: {err}"
229 )),
230 };
231
232 let target_config_ident = args.platform.config_toml_name();
233 let target_config = match util::rbrew_config_file(target_config_ident) {
234 Ok(ok) => ok,
235 Err(err) => graceful_error_exit(format!(
236 "failed to find the config toml file for the platform: {err}"
237 )),
238 };
239
240 cmd.arg(format!("--config={}", target_config.display()));
242
243 for option in &args.custom_options {
244 cmd.arg(option);
245 }
246
247 let mut status_cmd = Command::new(cmd.get_program());
248 status_cmd.args(cmd.get_args());
249 status_cmd.envs(cmd.get_envs().map(|env| (env.0, env.1.unwrap_or_default())));
250
251 let mut output_cmd = cmd;
252
253 match verbosity {
254 Verbosity::Quiet => {
255 status_cmd.arg("--quiet");
256 }
257 Verbosity::Normal => {}
258 Verbosity::Verbose => {
259 status_cmd.arg("--verbose");
260 }
261 };
262 let status = status_cmd
263 .status()
264 .expect("failed to execute cargo command");
265 if !status.success() {
266 graceful_error_exit("something went wrong when running cargo.")
267 }
268
269 let output = output_cmd
270 .arg("--message-format=json")
271 .arg("--quiet")
272 .output()
273 .unwrap();
274 if !output.status.success() {
275 panic!("should never be possible if we succeeded before");
276 }
277
278 let utf8 = String::from_utf8(output.stdout).expect("expected valid UTF-8");
279 let mut jsons = vec![];
280 for line in utf8.lines() {
281 jsons.push(json::parse(line).expect("expected valid json"))
282 }
283
284 let mut output_executable = vec![];
285 for json in jsons {
286 match json {
287 json::JsonValue::Object(object) => {
288 if let Some(executable) = object.get("executable") {
289 if let Some(str) = executable.as_str() {
290 output_executable.push(str.to_string())
291 }
292 }
293 }
294 _ => panic!("expected json object"),
295 }
296 }
297
298 for (gen, input) in output_executable.into_iter().enumerate() {
299 let input = Path::new(&input);
300 let output_dir = args
301 .output_directory
302 .clone()
303 .unwrap_or(input.parent().map(Path::to_path_buf).unwrap_or_default());
304 let output_gennerated_name = format!("output{gen}");
305 let output_name = input
306 .file_stem()
307 .unwrap_or(OsStr::new(&output_gennerated_name));
308
309 let mut output = output_dir.join(output_name);
310 output.set_extension(args.output_type.extension_name());
311
312 if verbosity.should_output(Verbosity::Normal) {
313 println!("output file: {}", output.display());
314 }
315
316 match args.output_type {
317 fields::OutputType::Elf => {
318 std::fs::copy(input, output).unwrap();
319 }
320 fields::OutputType::Dol => {
321 tools::elf2dol(input, output).unwrap();
322 }
323 }
324 }
325}
326
327fn tools(_args: RbrewCliSubTools, _verbosity: Verbosity) {}