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().context("Failed to start the async runtime for watch mode")?;
91
92 loop {
93 let receiver = session.message_queue().subscribe(cursor);
94 let (new_cursor, _patch_set) = match rt.block_on(receiver) {
95 Ok(message) => message,
96 Err(_) => break,
99 };
100 cursor = new_cursor;
101
102 write_model(&session, &output_path, output_kind)?;
103 }
104 }
105
106 forget(session);
109
110 Ok(())
111 }
112}
113
114#[derive(Debug, Clone, Copy, PartialEq, Eq)]
116enum OutputKind {
117 Rbxmx,
119
120 Rbxlx,
122
123 Rbxm,
125
126 Rbxl,
128}
129
130impl OutputKind {
131 fn from_output_path(output: &Path) -> Option<OutputKind> {
132 let extension = output.extension()?.to_str()?;
133
134 match extension {
135 "rbxlx" => Some(OutputKind::Rbxlx),
136 "rbxmx" => Some(OutputKind::Rbxmx),
137 "rbxl" => Some(OutputKind::Rbxl),
138 "rbxm" => Some(OutputKind::Rbxm),
139 _ => None,
140 }
141 }
142
143 fn from_plugin_path(output: &Path) -> Option<OutputKind> {
144 let extension = output.extension()?.to_str()?;
145
146 match extension {
147 "rbxmx" => Some(OutputKind::Rbxmx),
148 "rbxm" => Some(OutputKind::Rbxm),
149 _ => None,
150 }
151 }
152}
153
154fn xml_encode_config() -> rbx_xml::EncodeOptions<'static> {
155 rbx_xml::EncodeOptions::new().property_behavior(rbx_xml::EncodePropertyBehavior::WriteUnknown)
156}
157
158#[profiling::function]
159fn write_model(
160 session: &ServeSession,
161 output: &Path,
162 output_kind: OutputKind,
163) -> anyhow::Result<()> {
164 println!("Building project '{}'", session.project_name());
165
166 let tree = session.tree();
167 let root_id = tree.get_root_id();
168
169 log::trace!("Opening output file for write");
170 let mut file = BufWriter::new(File::create(output)?);
171
172 match output_kind {
173 OutputKind::Rbxm => {
174 rbx_binary::to_writer(&mut file, tree.inner(), &[root_id])?;
175 }
176 OutputKind::Rbxl => {
177 let root_instance = tree.get_instance(root_id).unwrap();
178 let top_level_ids = root_instance.children();
179
180 rbx_binary::to_writer(&mut file, tree.inner(), top_level_ids)?;
181 }
182 OutputKind::Rbxmx => {
183 rbx_xml::to_writer(&mut file, tree.inner(), &[root_id], xml_encode_config())?;
187 }
188 OutputKind::Rbxlx => {
189 let root_instance = tree.get_instance(root_id).unwrap();
193 let top_level_ids = root_instance.children();
194
195 rbx_xml::to_writer(&mut file, tree.inner(), top_level_ids, xml_encode_config())?;
196 }
197 }
198
199 file.flush()?;
200
201 let filename = output
202 .file_name()
203 .and_then(|name| name.to_str())
204 .unwrap_or("<invalid utf-8>");
205 println!("Built project to {}", filename);
206
207 Ok(())
208}