summaryrefslogtreecommitdiffstats
path: root/src/tools/rust-analyzer/xtask
diff options
context:
space:
mode:
Diffstat (limited to 'src/tools/rust-analyzer/xtask')
-rw-r--r--src/tools/rust-analyzer/xtask/Cargo.toml15
-rw-r--r--src/tools/rust-analyzer/xtask/src/dist.rs170
-rw-r--r--src/tools/rust-analyzer/xtask/src/flags.rs148
-rw-r--r--src/tools/rust-analyzer/xtask/src/install.rs142
-rw-r--r--src/tools/rust-analyzer/xtask/src/main.rs91
-rw-r--r--src/tools/rust-analyzer/xtask/src/metrics.rs200
-rw-r--r--src/tools/rust-analyzer/xtask/src/release.rs96
-rw-r--r--src/tools/rust-analyzer/xtask/src/release/changelog.rs171
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()) }
+}