publish_gpui.rs

  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}