1#![allow(clippy::disallowed_methods, reason = "tooling is exempt")]
2use std::io::{self, Write};
3use std::process::{Command, Output, Stdio};
4
5use anyhow::{Context as _, Result, bail};
6use clap::Parser;
7
8#[derive(Parser)]
9pub struct PublishGpuiArgs {
10 /// Optional pre-release identifier to append to the version (e.g., alpha, test.1). Always bumps the minor version.
11 #[arg(long)]
12 pre_release: Option<String>,
13
14 /// Perform a dry-run and wait for user confirmation before each publish
15 #[arg(long)]
16 dry_run: bool,
17}
18
19pub fn run_publish_gpui(args: PublishGpuiArgs) -> Result<()> {
20 println!(
21 "Starting GPUI publish process{}...",
22 if args.dry_run { " (with dry-run)" } else { "" }
23 );
24
25 let start_time = std::time::Instant::now();
26 check_workspace_root()?;
27 ensure_cargo_set_version()?;
28 check_git_clean()?;
29
30 let version = read_gpui_version()?;
31 println!("Updating GPUI to version: {}", version);
32 publish_dependencies(&version, args.dry_run)?;
33 publish_gpui(&version, args.dry_run)?;
34 println!("GPUI published in {}s", start_time.elapsed().as_secs_f32());
35 Ok(())
36}
37
38fn read_gpui_version() -> Result<String> {
39 let gpui_cargo_toml_path = "crates/gpui/Cargo.toml";
40 let contents = std::fs::read_to_string(gpui_cargo_toml_path)
41 .context("Failed to read crates/gpui/Cargo.toml")?;
42
43 let cargo_toml: toml::Value =
44 toml::from_str(&contents).context("Failed to parse crates/gpui/Cargo.toml")?;
45
46 let version = cargo_toml
47 .get("package")
48 .and_then(|p| p.get("version"))
49 .and_then(|v| v.as_str())
50 .context("Failed to find version in crates/gpui/Cargo.toml")?;
51
52 Ok(version.to_string())
53}
54
55fn publish_dependencies(new_version: &str, dry_run: bool) -> Result<()> {
56 let gpui_dependencies = vec![
57 ("zed-collections", "collections"),
58 ("zed-perf", "perf"),
59 ("zed-util-macros", "util_macros"),
60 ("zed-util", "util"),
61 ("gpui-macros", "gpui_macros"),
62 ("zed-http-client", "http_client"),
63 ("zed-derive-refineable", "derive_refineable"),
64 ("zed-refineable", "refineable"),
65 ("zed-semantic-version", "semantic_version"),
66 ("zed-sum-tree", "sum_tree"),
67 ("zed-media", "media"),
68 ];
69
70 for (crate_name, package_name) in gpui_dependencies {
71 println!(
72 "Publishing dependency: {} (package: {})",
73 crate_name, package_name
74 );
75
76 update_crate_version(crate_name, new_version)?;
77 update_workspace_dependency_version(package_name, new_version)?;
78 publish_crate(crate_name, dry_run)?;
79
80 // println!("Waiting 60s for the rate limit...");
81 // thread::sleep(Duration::from_secs(60));
82 }
83
84 Ok(())
85}
86
87fn publish_gpui(new_version: &str, dry_run: bool) -> Result<()> {
88 update_crate_version("gpui", new_version)?;
89
90 publish_crate("gpui", dry_run)?;
91
92 Ok(())
93}
94
95fn update_crate_version(package_name: &str, new_version: &str) -> Result<()> {
96 let output = run_command(
97 Command::new("cargo")
98 .arg("set-version")
99 .arg("--package")
100 .arg(package_name)
101 .arg(new_version),
102 )?;
103
104 if !output.status.success() {
105 bail!("Failed to set version for package {}", package_name);
106 }
107
108 Ok(())
109}
110
111fn publish_crate(crate_name: &str, dry_run: bool) -> Result<()> {
112 let publish_crate_impl = |crate_name, dry_run| {
113 let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_string());
114
115 let mut command = Command::new(&cargo);
116 command
117 .arg("publish")
118 .arg("--allow-dirty")
119 .args(["-p", crate_name]);
120
121 if dry_run {
122 command.arg("--dry-run");
123 }
124
125 run_command(&mut command)?;
126
127 anyhow::Ok(())
128 };
129
130 if dry_run {
131 publish_crate_impl(crate_name, true)?;
132
133 print!("Press Enter to publish for real (or ctrl-c to abort)...");
134 io::stdout().flush()?;
135
136 let mut input = String::new();
137 io::stdin().read_line(&mut input)?;
138 }
139
140 publish_crate_impl(crate_name, false)?;
141
142 Ok(())
143}
144
145fn update_workspace_dependency_version(package_name: &str, new_version: &str) -> Result<()> {
146 let workspace_cargo_toml_path = "Cargo.toml";
147 let contents = std::fs::read_to_string(workspace_cargo_toml_path)
148 .context("Failed to read workspace Cargo.toml")?;
149
150 let updated = update_dependency_version_in_toml(&contents, package_name, new_version)?;
151
152 std::fs::write(workspace_cargo_toml_path, updated)
153 .context("Failed to write workspace Cargo.toml")?;
154
155 Ok(())
156}
157
158fn update_dependency_version_in_toml(
159 toml_contents: &str,
160 package_name: &str,
161 new_version: &str,
162) -> Result<String> {
163 let mut doc = toml_contents
164 .parse::<toml_edit::DocumentMut>()
165 .context("Failed to parse TOML")?;
166
167 // Navigate to workspace.dependencies.<package_name>
168 let dependency = doc
169 .get_mut("workspace")
170 .and_then(|w| w.get_mut("dependencies"))
171 .and_then(|d| d.get_mut(package_name))
172 .context(format!(
173 "Failed to find {} in workspace dependencies",
174 package_name
175 ))?;
176
177 // Update the version field if it exists
178 if let Some(dep_table) = dependency.as_table_like_mut() {
179 if dep_table.contains_key("version") {
180 dep_table.insert("version", toml_edit::value(new_version));
181 } else {
182 bail!(
183 "No version field found for {} in workspace dependencies",
184 package_name
185 );
186 }
187 } else {
188 bail!("{} is not a table in workspace dependencies", package_name);
189 }
190
191 Ok(doc.to_string())
192}
193
194fn check_workspace_root() -> Result<()> {
195 let cwd = std::env::current_dir().context("Failed to get current directory")?;
196
197 // Check if Cargo.toml exists in the current directory
198 let cargo_toml_path = cwd.join("Cargo.toml");
199 if !cargo_toml_path.exists() {
200 bail!(
201 "Cargo.toml not found in current directory. Please run this command from the workspace root."
202 );
203 }
204
205 // Check if it's a workspace by looking for [workspace] section
206 let contents =
207 std::fs::read_to_string(&cargo_toml_path).context("Failed to read Cargo.toml")?;
208
209 if !contents.contains("[workspace]") {
210 bail!(
211 "Current directory does not appear to be a workspace root. Please run this command from the workspace root."
212 );
213 }
214
215 Ok(())
216}
217
218fn ensure_cargo_set_version() -> Result<()> {
219 let output = run_command(
220 Command::new("which")
221 .arg("cargo-set-version")
222 .stdout(Stdio::piped()),
223 )
224 .context("Failed to check for cargo-set-version")?;
225
226 if !output.status.success() {
227 println!("cargo-set-version not found. Installing cargo-edit...");
228
229 let install_output = run_command(Command::new("cargo").arg("install").arg("cargo-edit"))?;
230
231 if !install_output.status.success() {
232 bail!("Failed to install cargo-edit");
233 }
234 }
235
236 Ok(())
237}
238
239fn check_git_clean() -> Result<()> {
240 let output = run_command(
241 Command::new("git")
242 .args(["status", "--porcelain"])
243 .stdout(Stdio::piped())
244 .stderr(Stdio::piped()),
245 )?;
246
247 if !output.status.success() {
248 bail!("git status command failed");
249 }
250
251 let stdout = String::from_utf8_lossy(&output.stdout);
252 if !stdout.trim().is_empty() {
253 bail!(
254 "Working directory is not clean. Please commit or stash your changes before publishing."
255 );
256 }
257
258 Ok(())
259}
260
261fn run_command(command: &mut Command) -> Result<Output> {
262 let command_str = {
263 let program = command.get_program().to_string_lossy();
264 let args = command
265 .get_args()
266 .map(|arg| arg.to_string_lossy())
267 .collect::<Vec<_>>()
268 .join(" ");
269
270 if args.is_empty() {
271 program.to_string()
272 } else {
273 format!("{} {}", program, args)
274 }
275 };
276 eprintln!("+ {}", command_str);
277
278 let output = command
279 .spawn()
280 .context("failed to spawn child process")?
281 .wait_with_output()
282 .context("failed to wait for child process")?;
283
284 Ok(output)
285}
286
287#[cfg(test)]
288mod tests {
289 use indoc::indoc;
290
291 use super::*;
292
293 #[test]
294 fn test_update_dependency_version_in_toml() {
295 let input = indoc! {r#"
296 [workspace]
297 resolver = "2"
298
299 [workspace.dependencies]
300 # here's a comment
301 collections = { path = "crates/collections", package = "zed-collections", version = "0.1.0" }
302
303 util = { path = "crates/util", package = "zed-util", version = "0.1.0" }
304 "#};
305
306 let result = update_dependency_version_in_toml(input, "collections", "0.2.0").unwrap();
307
308 let output = indoc! {r#"
309 [workspace]
310 resolver = "2"
311
312 [workspace.dependencies]
313 # here's a comment
314 collections = { path = "crates/collections", package = "zed-collections", version = "0.2.0" }
315
316 util = { path = "crates/util", package = "zed-util", version = "0.1.0" }
317 "#};
318
319 assert_eq!(result, output);
320 }
321}