use std::env;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
const PCDL_APP_VERSION: &str = "0.1.0";
const RUST_MIR2_WRAPPER_VERSION: &str = "0.1.2";
const RUST_MIR2_VERSION: &str = "0.1.2";
const MIR2PCDL_VERSION: &str = "0.1.2";
pub fn maybe_open_editor() {
println!("cargo:rerun-if-env-changed=PCDL_OPEN");
println!("cargo:rerun-if-env-changed=PCDL_APP_BIN");
println!("cargo:rerun-if-env-changed=PCDL_RUST_MIR2_BIN");
println!("cargo:rerun-if-env-changed=RUST_MIR2_WRAPPER_BIN");
println!("cargo:rerun-if-env-changed=PCDL_PACKAGE_NAME");
println!("cargo:rerun-if-env-changed=PCDL_2R_DIR");
println!("cargo:rerun-if-env-changed=PCDL_APP_PORT");
println!("cargo:rerun-if-env-changed=PCDL_FRONTEND_PORT");
if env::var("PCDL_OPEN").ok().as_deref() != Some("1") {
return;
}
if let Err(error) = install_published_tools() {
warn(&format!("failed to install published PCDL tools: {error}"));
return;
}
let cargo_bin_dir = cargo_bin_dir();
let rust_mir2_bin = env::var_os("PCDL_RUST_MIR2_BIN")
.map(PathBuf::from)
.unwrap_or_else(|| binary_path(&cargo_bin_dir, "rust_mir2"));
let wrapper_bin = env::var_os("RUST_MIR2_WRAPPER_BIN")
.map(PathBuf::from)
.unwrap_or_else(|| binary_path(&cargo_bin_dir, "rust_mir2-wrapper"));
let app_bin = env::var_os("PCDL_APP_BIN")
.map(PathBuf::from)
.unwrap_or_else(|| binary_path(&cargo_bin_dir, "pcdl-app"));
let Some(manifest_dir) = env::var_os("CARGO_MANIFEST_DIR").map(PathBuf::from) else {
warn("missing CARGO_MANIFEST_DIR");
return;
};
let project_root = workspace_or_manifest_root(&manifest_dir);
let file_config = PcdlBuildConfig::load(&project_root.join("pcdl.toml")).unwrap_or_default();
let source_project = env::var("PCDL_SOURCE_PROJECT")
.map(PathBuf::from)
.ok()
.or(file_config.source_project.map(PathBuf::from))
.map(|path| resolve_config_path(&project_root, path))
.unwrap_or_else(|| project_root.clone());
let package_name = env::var("PCDL_PACKAGE_NAME").unwrap_or_else(|_| {
file_config
.package_name
.unwrap_or_else(|| env::var("CARGO_PKG_NAME").unwrap_or_else(|_| "unknown".to_string()))
});
let two_r_dir = env::var("PCDL_2R_DIR")
.map(PathBuf::from)
.ok()
.or(file_config.two_r_dir.map(PathBuf::from))
.map(|path| resolve_config_path(&project_root, path))
.map(|path| path.display().to_string())
.unwrap_or_else(|| project_root.join(".pcdl/2r").display().to_string());
let backend_port = env::var("PCDL_APP_PORT")
.ok()
.or(file_config.backend_port)
.unwrap_or_else(|| "8787".to_string());
let frontend_port = env::var("PCDL_FRONTEND_PORT")
.ok()
.or(file_config.frontend_port)
.unwrap_or_else(|| "3000".to_string());
let log_dir = project_root.join(".pcdl");
let log_path = log_dir.join("pcdl_app.log");
if let Err(error) = std::fs::create_dir_all(&log_dir) {
warn(&format!("failed to create PCDL log dir: {error}"));
return;
}
if is_port_open(&backend_port) {
println!("cargo:warning=PCDL editor already appears to be running on port {backend_port}");
return;
}
let mut command = Command::new(app_bin);
command
.arg("open")
.arg("--project")
.arg(source_project)
.arg("--package")
.arg(package_name)
.arg("--2r-dir")
.arg(two_r_dir)
.arg("--backend-port")
.arg(&backend_port)
.arg("--frontend-port")
.arg(frontend_port)
.arg("--open-browser")
.env("PCDL_RUST_MIR2_BIN", &rust_mir2_bin)
.env("RUST_MIR2_WRAPPER_BIN", &wrapper_bin)
.current_dir(&project_root)
.stdin(Stdio::null())
.stdout(log_file(&log_path))
.stderr(log_file(&log_path));
match command.spawn() {
Ok(_) => println!(
"cargo:warning=PCDL editor launch requested; log: {}",
log_path.display()
),
Err(error) => warn(&format!(
"failed to launch pcdl-app; install it or set PCDL_APP_BIN: {error}"
)),
}
}
fn workspace_or_manifest_root(manifest_dir: &Path) -> PathBuf {
let mut current = Some(manifest_dir);
while let Some(dir) = current {
if dir.join("Cargo.toml").is_file() && has_workspace_manifest(&dir.join("Cargo.toml")) {
return dir.to_path_buf();
}
current = dir.parent();
}
manifest_dir.to_path_buf()
}
fn install_published_tools() -> Result<(), String> {
install_published_tool("pcdl-app", PCDL_APP_VERSION, None)?;
install_published_tool(
"rust_mir2_wrapper",
RUST_MIR2_WRAPPER_VERSION,
Some("nightly"),
)?;
install_published_tool("rust_mir2", RUST_MIR2_VERSION, None)?;
install_published_tool("mir2pcdl", MIR2PCDL_VERSION, None)?;
Ok(())
}
fn install_published_tool(
crate_name: &str,
version: &str,
toolchain: Option<&str>,
) -> Result<(), String> {
let cargo = env::var("CARGO").unwrap_or_else(|_| "cargo".to_string());
let mut command = Command::new(cargo);
if let Some(toolchain) = toolchain {
command.arg(format!("+{toolchain}"));
}
command
.arg("install")
.arg(crate_name)
.arg("--version")
.arg(version);
let status = command
.status()
.map_err(|error| format!("failed to run cargo install for {crate_name}: {error}"))?;
if !status.success() {
return Err(format!(
"cargo install {crate_name} --version {version} failed with status {status}"
));
}
Ok(())
}
fn cargo_bin_dir() -> PathBuf {
env::var_os("CARGO_HOME")
.map(PathBuf::from)
.or_else(|| env::var_os("HOME").map(|home| PathBuf::from(home).join(".cargo")))
.unwrap_or_else(|| PathBuf::from(".cargo"))
.join("bin")
}
fn binary_path(dir: &Path, name: &str) -> PathBuf {
if cfg!(target_os = "windows") {
dir.join(format!("{name}.exe"))
} else {
dir.join(name)
}
}
fn resolve_config_path(base: &Path, path: PathBuf) -> PathBuf {
if path.is_absolute() {
path
} else {
base.join(path)
}
}
fn has_workspace_manifest(path: &Path) -> bool {
std::fs::read_to_string(path)
.map(|text| text.contains("[workspace]"))
.unwrap_or(false)
}
fn is_port_open(port: &str) -> bool {
let Ok(port) = port.parse::<u16>() else {
return false;
};
std::net::TcpStream::connect_timeout(
&std::net::SocketAddr::from(([127, 0, 0, 1], port)),
std::time::Duration::from_millis(150),
)
.is_ok()
}
#[derive(Default)]
struct PcdlBuildConfig {
source_project: Option<String>,
package_name: Option<String>,
two_r_dir: Option<String>,
backend_port: Option<String>,
frontend_port: Option<String>,
}
impl PcdlBuildConfig {
fn load(path: &Path) -> Option<Self> {
let text = std::fs::read_to_string(path).ok()?;
let mut config = Self::default();
for line in text.lines() {
let line = line.split('#').next().unwrap_or("").trim();
if line.is_empty() {
continue;
}
let Some((key, value)) = line.split_once('=') else {
continue;
};
let key = key.trim();
let value = value
.trim()
.trim_matches('"')
.trim_matches('\'')
.to_string();
match key {
"source_project" => config.source_project = Some(value),
"package_name" => config.package_name = Some(value),
"two_r_dir" => config.two_r_dir = Some(value),
"backend_port" => config.backend_port = Some(value),
"frontend_port" => config.frontend_port = Some(value),
_ => {}
}
}
Some(config)
}
}
fn warn(message: &str) {
println!("cargo:warning={message}");
}
fn log_file(path: &Path) -> Stdio {
std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(path)
.map(Stdio::from)
.unwrap_or_else(|_| Stdio::null())
}