diff options
Diffstat (limited to 'src/tools/rust-analyzer/xtask')
-rw-r--r-- | src/tools/rust-analyzer/xtask/Cargo.toml | 15 | ||||
-rw-r--r-- | src/tools/rust-analyzer/xtask/src/dist.rs | 170 | ||||
-rw-r--r-- | src/tools/rust-analyzer/xtask/src/flags.rs | 148 | ||||
-rw-r--r-- | src/tools/rust-analyzer/xtask/src/install.rs | 142 | ||||
-rw-r--r-- | src/tools/rust-analyzer/xtask/src/main.rs | 91 | ||||
-rw-r--r-- | src/tools/rust-analyzer/xtask/src/metrics.rs | 200 | ||||
-rw-r--r-- | src/tools/rust-analyzer/xtask/src/release.rs | 96 | ||||
-rw-r--r-- | src/tools/rust-analyzer/xtask/src/release/changelog.rs | 171 |
8 files changed, 1033 insertions, 0 deletions
diff --git a/src/tools/rust-analyzer/xtask/Cargo.toml b/src/tools/rust-analyzer/xtask/Cargo.toml new file mode 100644 index 000000000..95d44e9b9 --- /dev/null +++ b/src/tools/rust-analyzer/xtask/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "xtask" +version = "0.1.0" +publish = false +license = "MIT OR Apache-2.0" +edition = "2021" +rust-version = "1.57" + +[dependencies] +anyhow = "1.0.57" +flate2 = "1.0.24" +write-json = "0.1.2" +xshell = "0.2.2" +xflags = "0.2.4" +# Avoid adding more dependencies to this crate diff --git a/src/tools/rust-analyzer/xtask/src/dist.rs b/src/tools/rust-analyzer/xtask/src/dist.rs new file mode 100644 index 000000000..686aec4ae --- /dev/null +++ b/src/tools/rust-analyzer/xtask/src/dist.rs @@ -0,0 +1,170 @@ +use std::{ + env, + fs::File, + io, + path::{Path, PathBuf}, +}; + +use flate2::{write::GzEncoder, Compression}; +use xshell::{cmd, Shell}; + +use crate::{date_iso, flags, project_root}; + +const VERSION_STABLE: &str = "0.3"; +const VERSION_NIGHTLY: &str = "0.4"; +const VERSION_DEV: &str = "0.5"; // keep this one in sync with `package.json` + +impl flags::Dist { + pub(crate) fn run(self, sh: &Shell) -> anyhow::Result<()> { + let stable = sh.var("GITHUB_REF").unwrap_or_default().as_str() == "refs/heads/release"; + + let project_root = project_root(); + let target = Target::get(&project_root); + let dist = project_root.join("dist"); + sh.remove_path(&dist)?; + sh.create_dir(&dist)?; + + if let Some(patch_version) = self.client_patch_version { + let version = if stable { + format!("{}.{}", VERSION_STABLE, patch_version) + } else { + // A hack to make VS Code prefer nightly over stable. + format!("{}.{}", VERSION_NIGHTLY, patch_version) + }; + dist_server(sh, &format!("{version}-standalone"), &target)?; + let release_tag = if stable { date_iso(sh)? } else { "nightly".to_string() }; + dist_client(sh, &version, &release_tag, &target)?; + } else { + dist_server(sh, "0.0.0-standalone", &target)?; + } + Ok(()) + } +} + +fn dist_client( + sh: &Shell, + version: &str, + release_tag: &str, + target: &Target, +) -> anyhow::Result<()> { + let bundle_path = Path::new("editors").join("code").join("server"); + sh.create_dir(&bundle_path)?; + sh.copy_file(&target.server_path, &bundle_path)?; + if let Some(symbols_path) = &target.symbols_path { + sh.copy_file(symbols_path, &bundle_path)?; + } + + let _d = sh.push_dir("./editors/code"); + + let mut patch = Patch::new(sh, "./package.json")?; + patch + .replace( + &format!(r#""version": "{}.0-dev""#, VERSION_DEV), + &format!(r#""version": "{}""#, version), + ) + .replace(r#""releaseTag": null"#, &format!(r#""releaseTag": "{}""#, release_tag)) + .replace(r#""$generated-start": {},"#, "") + .replace(",\n \"$generated-end\": {}", "") + .replace(r#""enabledApiProposals": [],"#, r#""#); + patch.commit(sh)?; + + Ok(()) +} + +fn dist_server(sh: &Shell, release: &str, target: &Target) -> anyhow::Result<()> { + let _e = sh.push_env("CFG_RELEASE", release); + let _e = sh.push_env("CARGO_PROFILE_RELEASE_LTO", "thin"); + + // Uncomment to enable debug info for releases. Note that: + // * debug info is split on windows and macs, so it does nothing for those platforms, + // * on Linux, this blows up the binary size from 8MB to 43MB, which is unreasonable. + // let _e = sh.push_env("CARGO_PROFILE_RELEASE_DEBUG", "1"); + + if target.name.contains("-linux-") { + env::set_var("CC", "clang"); + } + + let target_name = &target.name; + cmd!(sh, "cargo build --manifest-path ./crates/rust-analyzer/Cargo.toml --bin rust-analyzer --target {target_name} --release").run()?; + + let dst = Path::new("dist").join(&target.artifact_name); + gzip(&target.server_path, &dst.with_extension("gz"))?; + + Ok(()) +} + +fn gzip(src_path: &Path, dest_path: &Path) -> anyhow::Result<()> { + let mut encoder = GzEncoder::new(File::create(dest_path)?, Compression::best()); + let mut input = io::BufReader::new(File::open(src_path)?); + io::copy(&mut input, &mut encoder)?; + encoder.finish()?; + Ok(()) +} + +struct Target { + name: String, + server_path: PathBuf, + symbols_path: Option<PathBuf>, + artifact_name: String, +} + +impl Target { + fn get(project_root: &Path) -> Self { + let name = match env::var("RA_TARGET") { + Ok(target) => target, + _ => { + if cfg!(target_os = "linux") { + "x86_64-unknown-linux-gnu".to_string() + } else if cfg!(target_os = "windows") { + "x86_64-pc-windows-msvc".to_string() + } else if cfg!(target_os = "macos") { + "x86_64-apple-darwin".to_string() + } else { + panic!("Unsupported OS, maybe try setting RA_TARGET") + } + } + }; + let out_path = project_root.join("target").join(&name).join("release"); + let (exe_suffix, symbols_path) = if name.contains("-windows-") { + (".exe".into(), Some(out_path.join("rust_analyzer.pdb"))) + } else { + (String::new(), None) + }; + let server_path = out_path.join(format!("rust-analyzer{}", exe_suffix)); + let artifact_name = format!("rust-analyzer-{}{}", name, exe_suffix); + Self { name, server_path, symbols_path, artifact_name } + } +} + +struct Patch { + path: PathBuf, + original_contents: String, + contents: String, +} + +impl Patch { + fn new(sh: &Shell, path: impl Into<PathBuf>) -> anyhow::Result<Patch> { + let path = path.into(); + let contents = sh.read_file(&path)?; + Ok(Patch { path, original_contents: contents.clone(), contents }) + } + + fn replace(&mut self, from: &str, to: &str) -> &mut Patch { + assert!(self.contents.contains(from)); + self.contents = self.contents.replace(from, to); + self + } + + fn commit(&self, sh: &Shell) -> anyhow::Result<()> { + sh.write_file(&self.path, &self.contents)?; + Ok(()) + } +} + +impl Drop for Patch { + fn drop(&mut self) { + // FIXME: find a way to bring this back + let _ = &self.original_contents; + // write_file(&self.path, &self.original_contents).unwrap(); + } +} diff --git a/src/tools/rust-analyzer/xtask/src/flags.rs b/src/tools/rust-analyzer/xtask/src/flags.rs new file mode 100644 index 000000000..993c64cce --- /dev/null +++ b/src/tools/rust-analyzer/xtask/src/flags.rs @@ -0,0 +1,148 @@ +#![allow(unreachable_pub)] + +use crate::install::{ClientOpt, Malloc, ServerOpt}; + +xflags::xflags! { + src "./src/flags.rs" + + /// Run custom build command. + cmd xtask { + default cmd help { + /// Print help information. + optional -h, --help + } + + /// Install rust-analyzer server or editor plugin. + cmd install { + /// Install only VS Code plugin. + optional --client + /// One of 'code', 'code-exploration', 'code-insiders', 'codium', or 'code-oss'. + optional --code-bin name: String + + /// Install only the language server. + optional --server + /// Use mimalloc allocator for server + optional --mimalloc + /// Use jemalloc allocator for server + optional --jemalloc + } + + cmd fuzz-tests {} + + cmd release { + optional --dry-run + } + cmd promote { + optional --dry-run + } + cmd dist { + optional --client-patch-version version: String + } + cmd metrics { + optional --dry-run + } + /// Builds a benchmark version of rust-analyzer and puts it into `./target`. + cmd bb + required suffix: String + {} + } +} + +// generated start +// The following code is generated by `xflags` macro. +// Run `env UPDATE_XFLAGS=1 cargo build` to regenerate. +#[derive(Debug)] +pub struct Xtask { + pub subcommand: XtaskCmd, +} + +#[derive(Debug)] +pub enum XtaskCmd { + Help(Help), + Install(Install), + FuzzTests(FuzzTests), + Release(Release), + Promote(Promote), + Dist(Dist), + Metrics(Metrics), + Bb(Bb), +} + +#[derive(Debug)] +pub struct Help { + pub help: bool, +} + +#[derive(Debug)] +pub struct Install { + pub client: bool, + pub code_bin: Option<String>, + pub server: bool, + pub mimalloc: bool, + pub jemalloc: bool, +} + +#[derive(Debug)] +pub struct FuzzTests; + +#[derive(Debug)] +pub struct Release { + pub dry_run: bool, +} + +#[derive(Debug)] +pub struct Promote { + pub dry_run: bool, +} + +#[derive(Debug)] +pub struct Dist { + pub client_patch_version: Option<String>, +} + +#[derive(Debug)] +pub struct Metrics { + pub dry_run: bool, +} + +#[derive(Debug)] +pub struct Bb { + pub suffix: String, +} + +impl Xtask { + pub const HELP: &'static str = Self::HELP_; + + #[allow(dead_code)] + pub fn from_env() -> xflags::Result<Self> { + Self::from_env_() + } + + #[allow(dead_code)] + pub fn from_vec(args: Vec<std::ffi::OsString>) -> xflags::Result<Self> { + Self::from_vec_(args) + } +} +// generated end + +impl Install { + pub(crate) fn server(&self) -> Option<ServerOpt> { + if self.client && !self.server { + return None; + } + let malloc = if self.mimalloc { + Malloc::Mimalloc + } else if self.jemalloc { + Malloc::Jemalloc + } else { + Malloc::System + }; + Some(ServerOpt { malloc }) + } + pub(crate) fn client(&self) -> Option<ClientOpt> { + if !self.client && self.server { + return None; + } + Some(ClientOpt { code_bin: self.code_bin.clone() }) + } +} diff --git a/src/tools/rust-analyzer/xtask/src/install.rs b/src/tools/rust-analyzer/xtask/src/install.rs new file mode 100644 index 000000000..ae978d551 --- /dev/null +++ b/src/tools/rust-analyzer/xtask/src/install.rs @@ -0,0 +1,142 @@ +//! Installs rust-analyzer language server and/or editor plugin. + +use std::{env, path::PathBuf, str}; + +use anyhow::{bail, format_err, Context, Result}; +use xshell::{cmd, Shell}; + +use crate::flags; + +impl flags::Install { + pub(crate) fn run(self, sh: &Shell) -> Result<()> { + if cfg!(target_os = "macos") { + fix_path_for_mac(sh).context("Fix path for mac")?; + } + if let Some(server) = self.server() { + install_server(sh, server).context("install server")?; + } + if let Some(client) = self.client() { + install_client(sh, client).context("install client")?; + } + Ok(()) + } +} + +#[derive(Clone)] +pub(crate) struct ClientOpt { + pub(crate) code_bin: Option<String>, +} + +const VS_CODES: &[&str] = &["code", "code-exploration", "code-insiders", "codium", "code-oss"]; + +pub(crate) struct ServerOpt { + pub(crate) malloc: Malloc, +} + +pub(crate) enum Malloc { + System, + Mimalloc, + Jemalloc, +} + +fn fix_path_for_mac(sh: &Shell) -> Result<()> { + let mut vscode_path: Vec<PathBuf> = { + const COMMON_APP_PATH: &str = + r"/Applications/Visual Studio Code.app/Contents/Resources/app/bin"; + const ROOT_DIR: &str = ""; + let home_dir = sh.var("HOME").map_err(|err| { + format_err!("Failed getting HOME from environment with error: {}.", err) + })?; + + [ROOT_DIR, &home_dir] + .into_iter() + .map(|dir| dir.to_string() + COMMON_APP_PATH) + .map(PathBuf::from) + .filter(|path| path.exists()) + .collect() + }; + + if !vscode_path.is_empty() { + let vars = sh.var_os("PATH").context("Could not get PATH variable from env.")?; + + let mut paths = env::split_paths(&vars).collect::<Vec<_>>(); + paths.append(&mut vscode_path); + let new_paths = env::join_paths(paths).context("build env PATH")?; + sh.set_var("PATH", &new_paths); + } + + Ok(()) +} + +fn install_client(sh: &Shell, client_opt: ClientOpt) -> Result<()> { + let _dir = sh.push_dir("./editors/code"); + + // Package extension. + if cfg!(unix) { + cmd!(sh, "npm --version").run().context("`npm` is required to build the VS Code plugin")?; + cmd!(sh, "npm ci").run()?; + + cmd!(sh, "npm run package --scripts-prepend-node-path").run()?; + } else { + cmd!(sh, "cmd.exe /c npm --version") + .run() + .context("`npm` is required to build the VS Code plugin")?; + cmd!(sh, "cmd.exe /c npm ci").run()?; + + cmd!(sh, "cmd.exe /c npm run package").run()?; + }; + + // Find the appropriate VS Code binary. + let lifetime_extender; + let candidates: &[&str] = match client_opt.code_bin.as_deref() { + Some(it) => { + lifetime_extender = [it]; + &lifetime_extender[..] + } + None => VS_CODES, + }; + let code = candidates + .iter() + .copied() + .find(|&bin| { + if cfg!(unix) { + cmd!(sh, "{bin} --version").read().is_ok() + } else { + cmd!(sh, "cmd.exe /c {bin}.cmd --version").read().is_ok() + } + }) + .ok_or_else(|| { + format_err!("Can't execute `{} --version`. Perhaps it is not in $PATH?", candidates[0]) + })?; + + // Install & verify. + let installed_extensions = if cfg!(unix) { + cmd!(sh, "{code} --install-extension rust-analyzer.vsix --force").run()?; + cmd!(sh, "{code} --list-extensions").read()? + } else { + cmd!(sh, "cmd.exe /c {code}.cmd --install-extension rust-analyzer.vsix --force").run()?; + cmd!(sh, "cmd.exe /c {code}.cmd --list-extensions").read()? + }; + + if !installed_extensions.contains("rust-analyzer") { + bail!( + "Could not install the Visual Studio Code extension. \ + Please make sure you have at least NodeJS 12.x together with the latest version of VS Code installed and try again. \ + Note that installing via xtask install does not work for VS Code Remote, instead you’ll need to install the .vsix manually." + ); + } + + Ok(()) +} + +fn install_server(sh: &Shell, opts: ServerOpt) -> Result<()> { + let features = match opts.malloc { + Malloc::System => &[][..], + Malloc::Mimalloc => &["--features", "mimalloc"], + Malloc::Jemalloc => &["--features", "jemalloc"], + }; + + let cmd = cmd!(sh, "cargo install --path crates/rust-analyzer --locked --force --features force-always-assert {features...}"); + cmd.run()?; + Ok(()) +} diff --git a/src/tools/rust-analyzer/xtask/src/main.rs b/src/tools/rust-analyzer/xtask/src/main.rs new file mode 100644 index 000000000..335ac324a --- /dev/null +++ b/src/tools/rust-analyzer/xtask/src/main.rs @@ -0,0 +1,91 @@ +//! See <https://github.com/matklad/cargo-xtask/>. +//! +//! This binary defines various auxiliary build commands, which are not +//! expressible with just `cargo`. Notably, it provides tests via `cargo test -p xtask` +//! for code generation and `cargo xtask install` for installation of +//! rust-analyzer server and client. +//! +//! This binary is integrated into the `cargo` command line by using an alias in +//! `.cargo/config`. + +#![warn(rust_2018_idioms, unused_lifetimes, semicolon_in_expressions_from_macros)] + +mod flags; + +mod install; +mod release; +mod dist; +mod metrics; + +use anyhow::bail; +use std::{ + env, + path::{Path, PathBuf}, +}; +use xshell::{cmd, Shell}; + +fn main() -> anyhow::Result<()> { + let sh = &Shell::new()?; + sh.change_dir(project_root()); + + let flags = flags::Xtask::from_env()?; + match flags.subcommand { + flags::XtaskCmd::Help(_) => { + println!("{}", flags::Xtask::HELP); + Ok(()) + } + flags::XtaskCmd::Install(cmd) => cmd.run(sh), + flags::XtaskCmd::FuzzTests(_) => run_fuzzer(sh), + flags::XtaskCmd::Release(cmd) => cmd.run(sh), + flags::XtaskCmd::Promote(cmd) => cmd.run(sh), + flags::XtaskCmd::Dist(cmd) => cmd.run(sh), + flags::XtaskCmd::Metrics(cmd) => cmd.run(sh), + flags::XtaskCmd::Bb(cmd) => { + { + let _d = sh.push_dir("./crates/rust-analyzer"); + cmd!(sh, "cargo build --release --features jemalloc").run()?; + } + sh.copy_file( + "./target/release/rust-analyzer", + format!("./target/rust-analyzer-{}", cmd.suffix), + )?; + Ok(()) + } + } +} + +fn project_root() -> PathBuf { + Path::new( + &env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| env!("CARGO_MANIFEST_DIR").to_owned()), + ) + .ancestors() + .nth(1) + .unwrap() + .to_path_buf() +} + +fn run_fuzzer(sh: &Shell) -> anyhow::Result<()> { + let _d = sh.push_dir("./crates/syntax"); + let _e = sh.push_env("RUSTUP_TOOLCHAIN", "nightly"); + if cmd!(sh, "cargo fuzz --help").read().is_err() { + cmd!(sh, "cargo install cargo-fuzz").run()?; + }; + + // Expecting nightly rustc + let out = cmd!(sh, "rustc --version").read()?; + if !out.contains("nightly") { + bail!("fuzz tests require nightly rustc") + } + + cmd!(sh, "cargo fuzz run parser").run()?; + Ok(()) +} + +fn date_iso(sh: &Shell) -> anyhow::Result<String> { + let res = cmd!(sh, "date -u +%Y-%m-%d").read()?; + Ok(res) +} + +fn is_release_tag(tag: &str) -> bool { + tag.len() == "2020-02-24".len() && tag.starts_with(|c: char| c.is_ascii_digit()) +} diff --git a/src/tools/rust-analyzer/xtask/src/metrics.rs b/src/tools/rust-analyzer/xtask/src/metrics.rs new file mode 100644 index 000000000..ebeb87346 --- /dev/null +++ b/src/tools/rust-analyzer/xtask/src/metrics.rs @@ -0,0 +1,200 @@ +use std::{ + collections::BTreeMap, + env, fs, + io::Write as _, + path::Path, + time::{Instant, SystemTime, UNIX_EPOCH}, +}; + +use anyhow::{bail, format_err}; +use xshell::{cmd, Shell}; + +use crate::flags; + +type Unit = String; + +impl flags::Metrics { + pub(crate) fn run(self, sh: &Shell) -> anyhow::Result<()> { + let mut metrics = Metrics::new(sh)?; + if !self.dry_run { + sh.remove_path("./target/release")?; + } + if !Path::new("./target/rustc-perf").exists() { + sh.create_dir("./target/rustc-perf")?; + cmd!(sh, "git clone https://github.com/rust-lang/rustc-perf.git ./target/rustc-perf") + .run()?; + } + { + let _d = sh.push_dir("./target/rustc-perf"); + let revision = &metrics.perf_revision; + cmd!(sh, "git reset --hard {revision}").run()?; + } + + let _env = sh.push_env("RA_METRICS", "1"); + + { + // https://github.com/rust-lang/rust-analyzer/issues/9997 + let _d = sh.push_dir("target/rustc-perf/collector/benchmarks/webrender"); + cmd!(sh, "cargo update -p url --precise 1.6.1").run()?; + } + metrics.measure_build(sh)?; + metrics.measure_analysis_stats_self(sh)?; + metrics.measure_analysis_stats(sh, "ripgrep")?; + metrics.measure_analysis_stats(sh, "webrender")?; + metrics.measure_analysis_stats(sh, "diesel/diesel")?; + + if !self.dry_run { + let _d = sh.push_dir("target"); + let metrics_token = env::var("METRICS_TOKEN").unwrap(); + cmd!( + sh, + "git clone --depth 1 https://{metrics_token}@github.com/rust-analyzer/metrics.git" + ) + .run()?; + + { + let mut file = + fs::File::options().append(true).open("target/metrics/metrics.json")?; + writeln!(file, "{}", metrics.json())?; + } + + let _d = sh.push_dir("metrics"); + cmd!(sh, "git add .").run()?; + cmd!(sh, "git -c user.name=Bot -c user.email=dummy@example.com commit --message 📈") + .run()?; + cmd!(sh, "git push origin master").run()?; + } + eprintln!("{metrics:#?}"); + Ok(()) + } +} + +impl Metrics { + fn measure_build(&mut self, sh: &Shell) -> anyhow::Result<()> { + eprintln!("\nMeasuring build"); + cmd!(sh, "cargo fetch").run()?; + + let time = Instant::now(); + cmd!(sh, "cargo build --release --package rust-analyzer --bin rust-analyzer").run()?; + let time = time.elapsed(); + self.report("build", time.as_millis() as u64, "ms".into()); + Ok(()) + } + fn measure_analysis_stats_self(&mut self, sh: &Shell) -> anyhow::Result<()> { + self.measure_analysis_stats_path(sh, "self", ".") + } + fn measure_analysis_stats(&mut self, sh: &Shell, bench: &str) -> anyhow::Result<()> { + self.measure_analysis_stats_path( + sh, + bench, + &format!("./target/rustc-perf/collector/benchmarks/{}", bench), + ) + } + fn measure_analysis_stats_path( + &mut self, + sh: &Shell, + name: &str, + path: &str, + ) -> anyhow::Result<()> { + eprintln!("\nMeasuring analysis-stats/{name}"); + let output = + cmd!(sh, "./target/release/rust-analyzer -q analysis-stats --memory-usage {path}") + .read()?; + for (metric, value, unit) in parse_metrics(&output) { + self.report(&format!("analysis-stats/{name}/{metric}"), value, unit.into()); + } + Ok(()) + } +} + +fn parse_metrics(output: &str) -> Vec<(&str, u64, &str)> { + output + .lines() + .filter_map(|it| { + let entry = it.split(':').collect::<Vec<_>>(); + match entry.as_slice() { + ["METRIC", name, value, unit] => Some((*name, value.parse().unwrap(), *unit)), + _ => None, + } + }) + .collect() +} + +#[derive(Debug)] +struct Metrics { + host: Host, + timestamp: SystemTime, + revision: String, + perf_revision: String, + metrics: BTreeMap<String, (u64, Unit)>, +} + +#[derive(Debug)] +struct Host { + os: String, + cpu: String, + mem: String, +} + +impl Metrics { + fn new(sh: &Shell) -> anyhow::Result<Metrics> { + let host = Host::new(sh)?; + let timestamp = SystemTime::now(); + let revision = cmd!(sh, "git rev-parse HEAD").read()?; + let perf_revision = "c52ee623e231e7690a93be88d943016968c1036b".into(); + Ok(Metrics { host, timestamp, revision, perf_revision, metrics: BTreeMap::new() }) + } + + fn report(&mut self, name: &str, value: u64, unit: Unit) { + self.metrics.insert(name.into(), (value, unit)); + } + + fn json(&self) -> String { + let mut buf = String::new(); + self.to_json(write_json::object(&mut buf)); + buf + } + + fn to_json(&self, mut obj: write_json::Object<'_>) { + self.host.to_json(obj.object("host")); + let timestamp = self.timestamp.duration_since(UNIX_EPOCH).unwrap(); + obj.number("timestamp", timestamp.as_secs() as f64); + obj.string("revision", &self.revision); + obj.string("perf_revision", &self.perf_revision); + let mut metrics = obj.object("metrics"); + for (k, (value, unit)) in &self.metrics { + metrics.array(k).number(*value as f64).string(unit); + } + } +} + +impl Host { + fn new(sh: &Shell) -> anyhow::Result<Host> { + if cfg!(not(target_os = "linux")) { + bail!("can only collect metrics on Linux "); + } + + let os = read_field(sh, "/etc/os-release", "PRETTY_NAME=")?.trim_matches('"').to_string(); + + let cpu = read_field(sh, "/proc/cpuinfo", "model name")? + .trim_start_matches(':') + .trim() + .to_string(); + + let mem = read_field(sh, "/proc/meminfo", "MemTotal:")?; + + return Ok(Host { os, cpu, mem }); + + fn read_field(sh: &Shell, path: &str, field: &str) -> anyhow::Result<String> { + let text = sh.read_file(path)?; + + text.lines() + .find_map(|it| it.strip_prefix(field)) + .map(|it| it.trim().to_string()) + .ok_or_else(|| format_err!("can't parse {}", path)) + } + } + fn to_json(&self, mut obj: write_json::Object<'_>) { + obj.string("os", &self.os).string("cpu", &self.cpu).string("mem", &self.mem); + } +} diff --git a/src/tools/rust-analyzer/xtask/src/release.rs b/src/tools/rust-analyzer/xtask/src/release.rs new file mode 100644 index 000000000..17ada5156 --- /dev/null +++ b/src/tools/rust-analyzer/xtask/src/release.rs @@ -0,0 +1,96 @@ +mod changelog; + +use xshell::{cmd, Shell}; + +use crate::{date_iso, flags, is_release_tag, project_root}; + +impl flags::Release { + pub(crate) fn run(self, sh: &Shell) -> anyhow::Result<()> { + if !self.dry_run { + cmd!(sh, "git switch release").run()?; + cmd!(sh, "git fetch upstream --tags --force").run()?; + cmd!(sh, "git reset --hard tags/nightly").run()?; + // The `release` branch sometimes has a couple of cherry-picked + // commits for patch releases. If that's the case, just overwrite + // it. As we are setting `release` branch to an up-to-date `nightly` + // tag, this shouldn't be problematic in general. + // + // Note that, as we tag releases, we don't worry about "losing" + // commits -- they'll be kept alive by the tag. More generally, we + // don't care about historic releases all that much, it's fine even + // to delete old tags. + cmd!(sh, "git push --force").run()?; + } + + // Generates bits of manual.adoc. + cmd!(sh, "cargo test -p ide-assists -p ide-diagnostics -p rust-analyzer -- sourcegen_") + .run()?; + + let website_root = project_root().join("../rust-analyzer.github.io"); + { + let _dir = sh.push_dir(&website_root); + cmd!(sh, "git switch src").run()?; + cmd!(sh, "git pull").run()?; + } + let changelog_dir = website_root.join("./thisweek/_posts"); + + let today = date_iso(sh)?; + let commit = cmd!(sh, "git rev-parse HEAD").read()?; + let changelog_n = sh + .read_dir(changelog_dir.as_path())? + .into_iter() + .filter_map(|p| p.file_stem().map(|s| s.to_string_lossy().to_string())) + .filter_map(|s| s.splitn(5, '-').last().map(|n| n.replace('-', "."))) + .filter_map(|s| s.parse::<f32>().ok()) + .map(|n| 1 + n.floor() as usize) + .max() + .unwrap_or_default(); + + for adoc in [ + "manual.adoc", + "generated_assists.adoc", + "generated_config.adoc", + "generated_diagnostic.adoc", + "generated_features.adoc", + ] { + let src = project_root().join("./docs/user/").join(adoc); + let dst = website_root.join(adoc); + + let contents = sh.read_file(src)?; + sh.write_file(dst, contents)?; + } + + let tags = cmd!(sh, "git tag --list").read()?; + let prev_tag = tags.lines().filter(|line| is_release_tag(line)).last().unwrap(); + + let contents = changelog::get_changelog(sh, changelog_n, &commit, prev_tag, &today)?; + let path = changelog_dir.join(format!("{}-changelog-{}.adoc", today, changelog_n)); + sh.write_file(&path, &contents)?; + + Ok(()) + } +} + +impl flags::Promote { + pub(crate) fn run(self, sh: &Shell) -> anyhow::Result<()> { + let _dir = sh.push_dir("../rust-rust-analyzer"); + cmd!(sh, "git switch master").run()?; + cmd!(sh, "git fetch upstream").run()?; + cmd!(sh, "git reset --hard upstream/master").run()?; + + let date = date_iso(sh)?; + let branch = format!("rust-analyzer-{date}"); + cmd!(sh, "git switch -c {branch}").run()?; + cmd!(sh, "git subtree pull -P src/tools/rust-analyzer rust-analyzer master").run()?; + + if !self.dry_run { + cmd!(sh, "git push -u origin {branch}").run()?; + cmd!( + sh, + "xdg-open https://github.com/matklad/rust/pull/new/{branch}?body=r%3F%20%40ghost" + ) + .run()?; + } + Ok(()) + } +} diff --git a/src/tools/rust-analyzer/xtask/src/release/changelog.rs b/src/tools/rust-analyzer/xtask/src/release/changelog.rs new file mode 100644 index 000000000..2647f7794 --- /dev/null +++ b/src/tools/rust-analyzer/xtask/src/release/changelog.rs @@ -0,0 +1,171 @@ +use std::fmt::Write; +use std::{env, iter}; + +use anyhow::bail; +use xshell::{cmd, Shell}; + +pub(crate) fn get_changelog( + sh: &Shell, + changelog_n: usize, + commit: &str, + prev_tag: &str, + today: &str, +) -> anyhow::Result<String> { + let token = match env::var("GITHUB_TOKEN") { + Ok(token) => token, + Err(_) => bail!("Please obtain a personal access token from https://github.com/settings/tokens and set the `GITHUB_TOKEN` environment variable."), + }; + + let git_log = cmd!(sh, "git log {prev_tag}..HEAD --reverse").read()?; + let mut features = String::new(); + let mut fixes = String::new(); + let mut internal = String::new(); + let mut others = String::new(); + for line in git_log.lines() { + let line = line.trim_start(); + if let Some(pr_num) = parse_pr_number(&line) { + let accept = "Accept: application/vnd.github.v3+json"; + let authorization = format!("Authorization: token {}", token); + let pr_url = "https://api.github.com/repos/rust-lang/rust-analyzer/issues"; + + // we don't use an HTTPS client or JSON parser to keep the build times low + let pr = pr_num.to_string(); + let pr_json = + cmd!(sh, "curl -s -H {accept} -H {authorization} {pr_url}/{pr}").read()?; + let pr_title = cmd!(sh, "jq .title").stdin(&pr_json).read()?; + let pr_title = unescape(&pr_title[1..pr_title.len() - 1]); + let pr_comment = cmd!(sh, "jq .body").stdin(pr_json).read()?; + + let comments_json = + cmd!(sh, "curl -s -H {accept} -H {authorization} {pr_url}/{pr}/comments").read()?; + let pr_comments = cmd!(sh, "jq .[].body").stdin(comments_json).read()?; + + let l = iter::once(pr_comment.as_str()) + .chain(pr_comments.lines()) + .rev() + .find_map(|it| { + let it = unescape(&it[1..it.len() - 1]); + it.lines().find_map(parse_changelog_line) + }) + .into_iter() + .next() + .unwrap_or_else(|| parse_title_line(&pr_title)); + let s = match l.kind { + PrKind::Feature => &mut features, + PrKind::Fix => &mut fixes, + PrKind::Internal => &mut internal, + PrKind::Other => &mut others, + PrKind::Skip => continue, + }; + writeln!(s, "* pr:{}[] {}", pr_num, l.message.as_deref().unwrap_or(&pr_title)).unwrap(); + } + } + + let contents = format!( + "\ += Changelog #{} +:sectanchors: +:page-layout: post + +Commit: commit:{}[] + +Release: release:{}[] + +== New Features + +{} + +== Fixes + +{} + +== Internal Improvements + +{} + +== Others + +{} +", + changelog_n, commit, today, features, fixes, internal, others + ); + Ok(contents) +} + +#[derive(Clone, Copy)] +enum PrKind { + Feature, + Fix, + Internal, + Other, + Skip, +} + +struct PrInfo { + message: Option<String>, + kind: PrKind, +} + +fn unescape(s: &str) -> String { + s.replace(r#"\""#, "").replace(r#"\n"#, "\n").replace(r#"\r"#, "") +} + +fn parse_pr_number(s: &str) -> Option<u32> { + const BORS_PREFIX: &str = "Merge #"; + const HOMU_PREFIX: &str = "Auto merge of #"; + if s.starts_with(BORS_PREFIX) { + let s = &s[BORS_PREFIX.len()..]; + s.parse().ok() + } else if s.starts_with(HOMU_PREFIX) { + let s = &s[HOMU_PREFIX.len()..]; + if let Some(space) = s.find(' ') { + s[..space].parse().ok() + } else { + None + } + } else { + None + } +} + +fn parse_changelog_line(s: &str) -> Option<PrInfo> { + let parts = s.splitn(3, ' ').collect::<Vec<_>>(); + if parts.len() < 2 || parts[0] != "changelog" { + return None; + } + let message = parts.get(2).map(|it| it.to_string()); + let kind = match parts[1].trim_end_matches(':') { + "feature" => PrKind::Feature, + "fix" => PrKind::Fix, + "internal" => PrKind::Internal, + "skip" => PrKind::Skip, + _ => { + let kind = PrKind::Other; + let message = format!("{} {}", parts[1], message.unwrap_or_default()); + return Some(PrInfo { kind, message: Some(message) }); + } + }; + let res = PrInfo { message, kind }; + Some(res) +} + +fn parse_title_line(s: &str) -> PrInfo { + let lower = s.to_ascii_lowercase(); + const PREFIXES: [(&str, PrKind); 5] = [ + ("feat: ", PrKind::Feature), + ("feature: ", PrKind::Feature), + ("fix: ", PrKind::Fix), + ("internal: ", PrKind::Internal), + ("minor: ", PrKind::Skip), + ]; + + for &(prefix, kind) in &PREFIXES { + if lower.starts_with(prefix) { + let message = match &kind { + PrKind::Skip => None, + _ => Some(s[prefix.len()..].to_string()), + }; + return PrInfo { message, kind }; + } + } + PrInfo { kind: PrKind::Other, message: Some(s.to_string()) } +} |