1use std::env;
2use std::path::{Path, PathBuf};
3use std::process::{Command, Stdio};
4
5const PCDL_APP_VERSION: &str = "0.1.0";
6const RUST_MIR2_WRAPPER_VERSION: &str = "0.1.2";
7const RUST_MIR2_VERSION: &str = "0.1.2";
8const MIR2PCDL_VERSION: &str = "0.1.2";
9
10pub fn maybe_open_editor() {
11 println!("cargo:rerun-if-env-changed=PCDL_OPEN");
12 println!("cargo:rerun-if-env-changed=PCDL_APP_BIN");
13 println!("cargo:rerun-if-env-changed=PCDL_RUST_MIR2_BIN");
14 println!("cargo:rerun-if-env-changed=RUST_MIR2_WRAPPER_BIN");
15 println!("cargo:rerun-if-env-changed=PCDL_PACKAGE_NAME");
16 println!("cargo:rerun-if-env-changed=PCDL_2R_DIR");
17 println!("cargo:rerun-if-env-changed=PCDL_APP_PORT");
18 println!("cargo:rerun-if-env-changed=PCDL_FRONTEND_PORT");
19
20 if env::var("PCDL_OPEN").ok().as_deref() != Some("1") {
21 return;
22 }
23
24 if let Err(error) = install_published_tools() {
25 warn(&format!("failed to install published PCDL tools: {error}"));
26 return;
27 }
28
29 let cargo_bin_dir = cargo_bin_dir();
30 let rust_mir2_bin = env::var_os("PCDL_RUST_MIR2_BIN")
31 .map(PathBuf::from)
32 .unwrap_or_else(|| binary_path(&cargo_bin_dir, "rust_mir2"));
33 let wrapper_bin = env::var_os("RUST_MIR2_WRAPPER_BIN")
34 .map(PathBuf::from)
35 .unwrap_or_else(|| binary_path(&cargo_bin_dir, "rust_mir2-wrapper"));
36 let app_bin = env::var_os("PCDL_APP_BIN")
37 .map(PathBuf::from)
38 .unwrap_or_else(|| binary_path(&cargo_bin_dir, "pcdl-app"));
39
40 let Some(manifest_dir) = env::var_os("CARGO_MANIFEST_DIR").map(PathBuf::from) else {
41 warn("missing CARGO_MANIFEST_DIR");
42 return;
43 };
44 let project_root = workspace_or_manifest_root(&manifest_dir);
45 let file_config = PcdlBuildConfig::load(&project_root.join("pcdl.toml")).unwrap_or_default();
46 let source_project = env::var("PCDL_SOURCE_PROJECT")
47 .map(PathBuf::from)
48 .ok()
49 .or(file_config.source_project.map(PathBuf::from))
50 .map(|path| resolve_config_path(&project_root, path))
51 .unwrap_or_else(|| project_root.clone());
52 let package_name = env::var("PCDL_PACKAGE_NAME").unwrap_or_else(|_| {
53 file_config
54 .package_name
55 .unwrap_or_else(|| env::var("CARGO_PKG_NAME").unwrap_or_else(|_| "unknown".to_string()))
56 });
57 let two_r_dir = env::var("PCDL_2R_DIR")
58 .map(PathBuf::from)
59 .ok()
60 .or(file_config.two_r_dir.map(PathBuf::from))
61 .map(|path| resolve_config_path(&project_root, path))
62 .map(|path| path.display().to_string())
63 .unwrap_or_else(|| project_root.join(".pcdl/2r").display().to_string());
64 let backend_port = env::var("PCDL_APP_PORT")
65 .ok()
66 .or(file_config.backend_port)
67 .unwrap_or_else(|| "8787".to_string());
68 let frontend_port = env::var("PCDL_FRONTEND_PORT")
69 .ok()
70 .or(file_config.frontend_port)
71 .unwrap_or_else(|| "3000".to_string());
72 let log_dir = project_root.join(".pcdl");
73 let log_path = log_dir.join("pcdl_app.log");
74
75 if let Err(error) = std::fs::create_dir_all(&log_dir) {
76 warn(&format!("failed to create PCDL log dir: {error}"));
77 return;
78 }
79
80 if is_port_open(&backend_port) {
81 println!("cargo:warning=PCDL editor already appears to be running on port {backend_port}");
82 return;
83 }
84
85 let mut command = Command::new(app_bin);
86 command
87 .arg("open")
88 .arg("--project")
89 .arg(source_project)
90 .arg("--package")
91 .arg(package_name)
92 .arg("--2r-dir")
93 .arg(two_r_dir)
94 .arg("--backend-port")
95 .arg(&backend_port)
96 .arg("--frontend-port")
97 .arg(frontend_port)
98 .arg("--open-browser")
99 .env("PCDL_RUST_MIR2_BIN", &rust_mir2_bin)
100 .env("RUST_MIR2_WRAPPER_BIN", &wrapper_bin)
101 .current_dir(&project_root)
102 .stdin(Stdio::null())
103 .stdout(log_file(&log_path))
104 .stderr(log_file(&log_path));
105
106 match command.spawn() {
107 Ok(_) => println!(
108 "cargo:warning=PCDL editor launch requested; log: {}",
109 log_path.display()
110 ),
111 Err(error) => warn(&format!(
112 "failed to launch pcdl-app; install it or set PCDL_APP_BIN: {error}"
113 )),
114 }
115}
116
117fn workspace_or_manifest_root(manifest_dir: &Path) -> PathBuf {
118 let mut current = Some(manifest_dir);
119
120 while let Some(dir) = current {
121 if dir.join("Cargo.toml").is_file() && has_workspace_manifest(&dir.join("Cargo.toml")) {
122 return dir.to_path_buf();
123 }
124
125 current = dir.parent();
126 }
127
128 manifest_dir.to_path_buf()
129}
130
131fn install_published_tools() -> Result<(), String> {
132 install_published_tool("pcdl-app", PCDL_APP_VERSION, None)?;
133 install_published_tool(
134 "rust_mir2_wrapper",
135 RUST_MIR2_WRAPPER_VERSION,
136 Some("nightly"),
137 )?;
138 install_published_tool("rust_mir2", RUST_MIR2_VERSION, None)?;
139 install_published_tool("mir2pcdl", MIR2PCDL_VERSION, None)?;
140 Ok(())
141}
142
143fn install_published_tool(
144 crate_name: &str,
145 version: &str,
146 toolchain: Option<&str>,
147) -> Result<(), String> {
148 let cargo = env::var("CARGO").unwrap_or_else(|_| "cargo".to_string());
149 let mut command = Command::new(cargo);
150 if let Some(toolchain) = toolchain {
151 command.arg(format!("+{toolchain}"));
152 }
153 command
154 .arg("install")
155 .arg(crate_name)
156 .arg("--version")
157 .arg(version);
158
159 let status = command
160 .status()
161 .map_err(|error| format!("failed to run cargo install for {crate_name}: {error}"))?;
162
163 if !status.success() {
164 return Err(format!(
165 "cargo install {crate_name} --version {version} failed with status {status}"
166 ));
167 }
168
169 Ok(())
170}
171
172fn cargo_bin_dir() -> PathBuf {
173 env::var_os("CARGO_HOME")
174 .map(PathBuf::from)
175 .or_else(|| env::var_os("HOME").map(|home| PathBuf::from(home).join(".cargo")))
176 .unwrap_or_else(|| PathBuf::from(".cargo"))
177 .join("bin")
178}
179
180fn binary_path(dir: &Path, name: &str) -> PathBuf {
181 if cfg!(target_os = "windows") {
182 dir.join(format!("{name}.exe"))
183 } else {
184 dir.join(name)
185 }
186}
187
188fn resolve_config_path(base: &Path, path: PathBuf) -> PathBuf {
189 if path.is_absolute() {
190 path
191 } else {
192 base.join(path)
193 }
194}
195
196fn has_workspace_manifest(path: &Path) -> bool {
197 std::fs::read_to_string(path)
198 .map(|text| text.contains("[workspace]"))
199 .unwrap_or(false)
200}
201
202fn is_port_open(port: &str) -> bool {
203 let Ok(port) = port.parse::<u16>() else {
204 return false;
205 };
206
207 std::net::TcpStream::connect_timeout(
208 &std::net::SocketAddr::from(([127, 0, 0, 1], port)),
209 std::time::Duration::from_millis(150),
210 )
211 .is_ok()
212}
213
214#[derive(Default)]
215struct PcdlBuildConfig {
216 source_project: Option<String>,
217 package_name: Option<String>,
218 two_r_dir: Option<String>,
219 backend_port: Option<String>,
220 frontend_port: Option<String>,
221}
222
223impl PcdlBuildConfig {
224 fn load(path: &Path) -> Option<Self> {
225 let text = std::fs::read_to_string(path).ok()?;
226 let mut config = Self::default();
227
228 for line in text.lines() {
229 let line = line.split('#').next().unwrap_or("").trim();
230 if line.is_empty() {
231 continue;
232 }
233
234 let Some((key, value)) = line.split_once('=') else {
235 continue;
236 };
237 let key = key.trim();
238 let value = value
239 .trim()
240 .trim_matches('"')
241 .trim_matches('\'')
242 .to_string();
243
244 match key {
245 "source_project" => config.source_project = Some(value),
246 "package_name" => config.package_name = Some(value),
247 "two_r_dir" => config.two_r_dir = Some(value),
248 "backend_port" => config.backend_port = Some(value),
249 "frontend_port" => config.frontend_port = Some(value),
250 _ => {}
251 }
252 }
253
254 Some(config)
255 }
256}
257
258fn warn(message: &str) {
259 println!("cargo:warning={message}");
260}
261
262fn log_file(path: &Path) -> Stdio {
263 std::fs::OpenOptions::new()
264 .create(true)
265 .append(true)
266 .open(path)
267 .map(Stdio::from)
268 .unwrap_or_else(|_| Stdio::null())
269}