Files
rojo/src/cli/build.rs
Astrid 8053909bd0 Add --git-since option to rojo serve
- Add new GitFilter struct for tracking files changed since a Git reference
- Only sync changed (added/deleted/modified) files to Roblox Studio
- Files remain acknowledged once synced, even if content is reverted
- Add enhanced logging for debugging sync issues
- Force acknowledge project structure to prevent 'Cannot sync a model as a place' errors
2026-01-19 22:02:59 +01:00

204 lines
6.1 KiB
Rust

use std::{
io::{BufWriter, Write},
mem::forget,
path::{Path, PathBuf},
};
use anyhow::{bail, Context};
use clap::{CommandFactory, Parser};
use fs_err::File;
use memofs::Vfs;
use roblox_install::RobloxStudio;
use tokio::runtime::Runtime;
use crate::serve_session::ServeSession;
use super::resolve_path;
const UNKNOWN_OUTPUT_KIND_ERR: &str = "Could not detect what kind of file to build. \
Expected output file to end in .rbxl, .rbxlx, .rbxm, or .rbxmx.";
const UNKNOWN_PLUGIN_KIND_ERR: &str = "Could not detect what kind of file to build. \
Expected plugin file to end in .rbxm or .rbxmx.";
/// Generates a model or place file from the Rojo project.
#[derive(Debug, Parser)]
pub struct BuildCommand {
/// Path to the project to build. Defaults to the current directory.
#[clap(default_value = "")]
pub project: PathBuf,
/// Where to output the result.
///
/// Should end in .rbxm, .rbxl, .rbxmx, or .rbxlx.
#[clap(long, short, conflicts_with = "plugin")]
pub output: Option<PathBuf>,
/// Alternative to the output flag that outputs the result in the local plugins folder.
///
/// Should end in .rbxm or .rbxl.
#[clap(long, short, conflicts_with = "output")]
pub plugin: Option<PathBuf>,
/// Whether to automatically rebuild when any input files change.
#[clap(long)]
pub watch: bool,
}
impl BuildCommand {
pub fn run(self) -> anyhow::Result<()> {
let (output_path, output_kind) = match (self.output, self.plugin) {
(None, None) => {
BuildCommand::command()
.error(
clap::ErrorKind::MissingRequiredArgument,
"one of the following arguments must be provided: \n --output <OUTPUT>\n --plugin <PLUGIN>",
)
.exit();
}
(Some(output), None) => {
let output_kind =
OutputKind::from_output_path(&output).context(UNKNOWN_OUTPUT_KIND_ERR)?;
(output, output_kind)
}
(None, Some(plugin)) => {
if plugin.is_absolute() {
bail!("plugin flag path cannot be absolute.")
}
let output_kind =
OutputKind::from_plugin_path(&plugin).context(UNKNOWN_PLUGIN_KIND_ERR)?;
let studio = RobloxStudio::locate()?;
(studio.plugins_path().join(&plugin), output_kind)
}
_ => unreachable!(),
};
let project_path = resolve_path(&self.project);
log::trace!("Constructing in-memory filesystem");
let vfs = Vfs::new_default();
vfs.set_watch_enabled(self.watch);
let session = ServeSession::new(vfs, project_path, None)?;
let mut cursor = session.message_queue().cursor();
write_model(&session, &output_path, output_kind)?;
if self.watch {
let rt = Runtime::new().unwrap();
loop {
let receiver = session.message_queue().subscribe(cursor);
let (new_cursor, _patch_set) = rt.block_on(receiver).unwrap();
cursor = new_cursor;
write_model(&session, &output_path, output_kind)?;
}
}
// Avoid dropping ServeSession: it's potentially VERY expensive to drop
// and we're about to exit anyways.
forget(session);
Ok(())
}
}
/// The different kinds of output that Rojo can build to.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum OutputKind {
/// An XML model file.
Rbxmx,
/// An XML place file.
Rbxlx,
/// A binary model file.
Rbxm,
/// A binary place file.
Rbxl,
}
impl OutputKind {
fn from_output_path(output: &Path) -> Option<OutputKind> {
let extension = output.extension()?.to_str()?;
match extension {
"rbxlx" => Some(OutputKind::Rbxlx),
"rbxmx" => Some(OutputKind::Rbxmx),
"rbxl" => Some(OutputKind::Rbxl),
"rbxm" => Some(OutputKind::Rbxm),
_ => None,
}
}
fn from_plugin_path(output: &Path) -> Option<OutputKind> {
let extension = output.extension()?.to_str()?;
match extension {
"rbxmx" => Some(OutputKind::Rbxmx),
"rbxm" => Some(OutputKind::Rbxm),
_ => None,
}
}
}
fn xml_encode_config() -> rbx_xml::EncodeOptions<'static> {
rbx_xml::EncodeOptions::new().property_behavior(rbx_xml::EncodePropertyBehavior::WriteUnknown)
}
#[profiling::function]
fn write_model(
session: &ServeSession,
output: &Path,
output_kind: OutputKind,
) -> anyhow::Result<()> {
println!("Building project '{}'", session.project_name());
let tree = session.tree();
let root_id = tree.get_root_id();
log::trace!("Opening output file for write");
let mut file = BufWriter::new(File::create(output)?);
match output_kind {
OutputKind::Rbxm => {
rbx_binary::to_writer(&mut file, tree.inner(), &[root_id])?;
}
OutputKind::Rbxl => {
let root_instance = tree.get_instance(root_id).unwrap();
let top_level_ids = root_instance.children();
rbx_binary::to_writer(&mut file, tree.inner(), top_level_ids)?;
}
OutputKind::Rbxmx => {
// Model files include the root instance of the tree and all its
// descendants.
rbx_xml::to_writer(&mut file, tree.inner(), &[root_id], xml_encode_config())?;
}
OutputKind::Rbxlx => {
// Place files don't contain an entry for the DataModel, but our
// WeakDom representation does.
let root_instance = tree.get_instance(root_id).unwrap();
let top_level_ids = root_instance.children();
rbx_xml::to_writer(&mut file, tree.inner(), top_level_ids, xml_encode_config())?;
}
}
file.flush()?;
let filename = output
.file_name()
.and_then(|name| name.to_str())
.unwrap_or("<invalid utf-8>");
println!("Built project to {}", filename);
Ok(())
}