1use std::{
2 io::{BufWriter, Write},
3 mem::forget,
4 path::{Path, PathBuf},
5};
6
7use anyhow::{bail, Context};
8use clap::{CommandFactory, Parser};
9use fs_err::File;
10use memofs::Vfs;
11use roblox_install::RobloxStudio;
12use tokio::runtime::Runtime;
13
14use crate::serve_session::ServeSession;
15
16use super::resolve_path;
17
18const UNKNOWN_OUTPUT_KIND_ERR: &str = "Could not detect what kind of file to build. \
19 Expected output file to end in .rbxl, .rbxlx, .rbxm, or .rbxmx.";
20const UNKNOWN_PLUGIN_KIND_ERR: &str = "Could not detect what kind of file to build. \
21 Expected plugin file to end in .rbxm or .rbxmx.";
22
23#[derive(Debug, Parser)]
25pub struct BuildCommand {
26 #[clap(default_value = "")]
28 pub project: PathBuf,
29
30 #[clap(long, short, conflicts_with = "plugin")]
34 pub output: Option<PathBuf>,
35
36 #[clap(long, short, conflicts_with = "output")]
40 pub plugin: Option<PathBuf>,
41
42 #[clap(long)]
44 pub watch: bool,
45}
46
47impl BuildCommand {
48 pub fn run(self) -> anyhow::Result<()> {
49 let (output_path, output_kind) = match (self.output, self.plugin) {
50 (None, None) => {
51 BuildCommand::command()
52 .error(
53 clap::ErrorKind::MissingRequiredArgument,
54 "one of the following arguments must be provided: \n --output <OUTPUT>\n --plugin <PLUGIN>",
55 )
56 .exit();
57 }
58 (Some(output), None) => {
59 let output_kind =
60 OutputKind::from_output_path(&output).context(UNKNOWN_OUTPUT_KIND_ERR)?;
61
62 (output, output_kind)
63 }
64 (None, Some(plugin)) => {
65 if plugin.is_absolute() {
66 bail!("plugin flag path cannot be absolute.")
67 }
68
69 let output_kind =
70 OutputKind::from_plugin_path(&plugin).context(UNKNOWN_PLUGIN_KIND_ERR)?;
71 let studio = RobloxStudio::locate()?;
72
73 (studio.plugins_path().join(&plugin), output_kind)
74 }
75 _ => unreachable!(),
76 };
77
78 let project_path = resolve_path(&self.project);
79
80 log::trace!("Constructing in-memory filesystem");
81 let vfs = Vfs::new_default();
82 vfs.set_watch_enabled(self.watch);
83
84 let session = ServeSession::new(vfs, project_path)?;
85 let mut cursor = session.message_queue().cursor();
86
87 write_model(&session, &output_path, output_kind)?;
88
89 if self.watch {
90 let rt = Runtime::new().unwrap();
91
92 loop {
93 let receiver = session.message_queue().subscribe(cursor);
94 let (new_cursor, _patch_set) = rt.block_on(receiver).unwrap();
95 cursor = new_cursor;
96
97 write_model(&session, &output_path, output_kind)?;
98 }
99 }
100
101 forget(session);
104
105 Ok(())
106 }
107}
108
109#[derive(Debug, Clone, Copy, PartialEq, Eq)]
111enum OutputKind {
112 Rbxmx,
114
115 Rbxlx,
117
118 Rbxm,
120
121 Rbxl,
123}
124
125impl OutputKind {
126 fn from_output_path(output: &Path) -> Option<OutputKind> {
127 let extension = output.extension()?.to_str()?;
128
129 match extension {
130 "rbxlx" => Some(OutputKind::Rbxlx),
131 "rbxmx" => Some(OutputKind::Rbxmx),
132 "rbxl" => Some(OutputKind::Rbxl),
133 "rbxm" => Some(OutputKind::Rbxm),
134 _ => None,
135 }
136 }
137
138 fn from_plugin_path(output: &Path) -> Option<OutputKind> {
139 let extension = output.extension()?.to_str()?;
140
141 match extension {
142 "rbxmx" => Some(OutputKind::Rbxmx),
143 "rbxm" => Some(OutputKind::Rbxm),
144 _ => None,
145 }
146 }
147}
148
149fn xml_encode_config() -> rbx_xml::EncodeOptions<'static> {
150 rbx_xml::EncodeOptions::new().property_behavior(rbx_xml::EncodePropertyBehavior::WriteUnknown)
151}
152
153#[profiling::function]
154fn write_model(
155 session: &ServeSession,
156 output: &Path,
157 output_kind: OutputKind,
158) -> anyhow::Result<()> {
159 println!("Building project '{}'", session.project_name());
160
161 let tree = session.tree();
162 let root_id = tree.get_root_id();
163
164 log::trace!("Opening output file for write");
165 let mut file = BufWriter::new(File::create(output)?);
166
167 match output_kind {
168 OutputKind::Rbxm => {
169 rbx_binary::to_writer(&mut file, tree.inner(), &[root_id])?;
170 }
171 OutputKind::Rbxl => {
172 let root_instance = tree.get_instance(root_id).unwrap();
173 let top_level_ids = root_instance.children();
174
175 rbx_binary::to_writer(&mut file, tree.inner(), top_level_ids)?;
176 }
177 OutputKind::Rbxmx => {
178 rbx_xml::to_writer(&mut file, tree.inner(), &[root_id], xml_encode_config())?;
182 }
183 OutputKind::Rbxlx => {
184 let root_instance = tree.get_instance(root_id).unwrap();
188 let top_level_ids = root_instance.children();
189
190 rbx_xml::to_writer(&mut file, tree.inner(), top_level_ids, xml_encode_config())?;
191 }
192 }
193
194 file.flush()?;
195
196 let filename = output
197 .file_name()
198 .and_then(|name| name.to_str())
199 .unwrap_or("<invalid utf-8>");
200 println!("Built project to {}", filename);
201
202 Ok(())
203}