aboutsummaryrefslogtreecommitdiff
path: root/xtask
diff options
context:
space:
mode:
Diffstat (limited to 'xtask')
-rw-r--r--xtask/Cargo.toml2
-rw-r--r--xtask/src/codegen.rs28
-rw-r--r--xtask/src/codegen/gen_assists_docs.rs4
-rw-r--r--xtask/src/codegen/gen_diagnostic_docs.rs74
-rw-r--r--xtask/src/codegen/gen_feature_docs.rs4
-rw-r--r--xtask/src/codegen/gen_features.rs10
-rw-r--r--xtask/src/codegen/gen_lint_completions.rs113
-rw-r--r--xtask/src/codegen/gen_parser_tests.rs2
-rw-r--r--xtask/src/dist.rs33
-rw-r--r--xtask/src/install.rs91
-rw-r--r--xtask/src/lib.rs52
-rw-r--r--xtask/src/main.rs23
-rw-r--r--xtask/src/metrics.rs39
-rw-r--r--xtask/src/not_bash.rs169
-rw-r--r--xtask/src/pre_cache.rs5
-rw-r--r--xtask/src/pre_commit.rs8
-rw-r--r--xtask/src/release.rs71
-rw-r--r--xtask/tests/tidy.rs47
18 files changed, 431 insertions, 344 deletions
diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml
index 01a838825..2ef956485 100644
--- a/xtask/Cargo.toml
+++ b/xtask/Cargo.toml
@@ -18,5 +18,5 @@ quote = "1.0.2"
18ungrammar = "1.1.3" 18ungrammar = "1.1.3"
19walkdir = "2.3.1" 19walkdir = "2.3.1"
20write-json = "0.1.0" 20write-json = "0.1.0"
21fs-err = "2.3" 21xshell = "0.1"
22# Avoid adding more dependencies to this crate 22# Avoid adding more dependencies to this crate
diff --git a/xtask/src/codegen.rs b/xtask/src/codegen.rs
index 1e7894617..adea053b6 100644
--- a/xtask/src/codegen.rs
+++ b/xtask/src/codegen.rs
@@ -9,23 +9,22 @@ mod gen_syntax;
9mod gen_parser_tests; 9mod gen_parser_tests;
10mod gen_assists_docs; 10mod gen_assists_docs;
11mod gen_feature_docs; 11mod gen_feature_docs;
12mod gen_features; 12mod gen_lint_completions;
13mod gen_diagnostic_docs;
13 14
14use std::{ 15use std::{
15 fmt, mem, 16 fmt, mem,
16 path::{Path, PathBuf}, 17 path::{Path, PathBuf},
17}; 18};
19use xshell::{cmd, pushenv, read_file, write_file};
18 20
19use crate::{ 21use crate::{ensure_rustfmt, project_root, Result};
20 ensure_rustfmt,
21 not_bash::{fs2, pushenv, run},
22 project_root, Result,
23};
24 22
25pub use self::{ 23pub use self::{
26 gen_assists_docs::{generate_assists_docs, generate_assists_tests}, 24 gen_assists_docs::{generate_assists_docs, generate_assists_tests},
25 gen_diagnostic_docs::generate_diagnostic_docs,
27 gen_feature_docs::generate_feature_docs, 26 gen_feature_docs::generate_feature_docs,
28 gen_features::generate_features, 27 gen_lint_completions::generate_lint_completions,
29 gen_parser_tests::generate_parser_tests, 28 gen_parser_tests::generate_parser_tests,
30 gen_syntax::generate_syntax, 29 gen_syntax::generate_syntax,
31}; 30};
@@ -43,13 +42,14 @@ pub struct CodegenCmd {
43impl CodegenCmd { 42impl CodegenCmd {
44 pub fn run(self) -> Result<()> { 43 pub fn run(self) -> Result<()> {
45 if self.features { 44 if self.features {
46 generate_features(Mode::Overwrite)?; 45 generate_lint_completions(Mode::Overwrite)?;
47 } 46 }
48 generate_syntax(Mode::Overwrite)?; 47 generate_syntax(Mode::Overwrite)?;
49 generate_parser_tests(Mode::Overwrite)?; 48 generate_parser_tests(Mode::Overwrite)?;
50 generate_assists_tests(Mode::Overwrite)?; 49 generate_assists_tests(Mode::Overwrite)?;
51 generate_assists_docs(Mode::Overwrite)?; 50 generate_assists_docs(Mode::Overwrite)?;
52 generate_feature_docs(Mode::Overwrite)?; 51 generate_feature_docs(Mode::Overwrite)?;
52 generate_diagnostic_docs(Mode::Overwrite)?;
53 Ok(()) 53 Ok(())
54 } 54 }
55} 55}
@@ -57,7 +57,7 @@ impl CodegenCmd {
57/// A helper to update file on disk if it has changed. 57/// A helper to update file on disk if it has changed.
58/// With verify = false, 58/// With verify = false,
59fn update(path: &Path, contents: &str, mode: Mode) -> Result<()> { 59fn update(path: &Path, contents: &str, mode: Mode) -> Result<()> {
60 match fs2::read_to_string(path) { 60 match read_file(path) {
61 Ok(old_contents) if normalize(&old_contents) == normalize(contents) => { 61 Ok(old_contents) if normalize(&old_contents) == normalize(contents) => {
62 return Ok(()); 62 return Ok(());
63 } 63 }
@@ -67,7 +67,7 @@ fn update(path: &Path, contents: &str, mode: Mode) -> Result<()> {
67 anyhow::bail!("`{}` is not up-to-date", path.display()); 67 anyhow::bail!("`{}` is not up-to-date", path.display());
68 } 68 }
69 eprintln!("updating {}", path.display()); 69 eprintln!("updating {}", path.display());
70 fs2::write(path, contents)?; 70 write_file(path, contents)?;
71 return Ok(()); 71 return Ok(());
72 72
73 fn normalize(s: &str) -> String { 73 fn normalize(s: &str) -> String {
@@ -80,10 +80,10 @@ const PREAMBLE: &str = "Generated file, do not edit by hand, see `xtask/src/code
80fn reformat(text: &str) -> Result<String> { 80fn reformat(text: &str) -> Result<String> {
81 let _e = pushenv("RUSTUP_TOOLCHAIN", "stable"); 81 let _e = pushenv("RUSTUP_TOOLCHAIN", "stable");
82 ensure_rustfmt()?; 82 ensure_rustfmt()?;
83 let stdout = run!( 83 let rustfmt_toml = project_root().join("rustfmt.toml");
84 "rustfmt --config-path {} --config fn_single_line=true", project_root().join("rustfmt.toml").display(); 84 let stdout = cmd!("rustfmt --config-path {rustfmt_toml} --config fn_single_line=true")
85 <text.as_bytes() 85 .stdin(text)
86 )?; 86 .read()?;
87 Ok(format!("//! {}\n\n{}\n", PREAMBLE, stdout)) 87 Ok(format!("//! {}\n\n{}\n", PREAMBLE, stdout))
88} 88}
89 89
diff --git a/xtask/src/codegen/gen_assists_docs.rs b/xtask/src/codegen/gen_assists_docs.rs
index ff307e2aa..d7c85ebe9 100644
--- a/xtask/src/codegen/gen_assists_docs.rs
+++ b/xtask/src/codegen/gen_assists_docs.rs
@@ -1,6 +1,6 @@
1//! Generates `assists.md` documentation. 1//! Generates `assists.md` documentation.
2 2
3use std::{fmt, fs, path::Path}; 3use std::{fmt, path::Path};
4 4
5use crate::{ 5use crate::{
6 codegen::{self, extract_comment_blocks_with_empty_lines, reformat, Location, Mode, PREAMBLE}, 6 codegen::{self, extract_comment_blocks_with_empty_lines, reformat, Location, Mode, PREAMBLE},
@@ -39,7 +39,7 @@ impl Assist {
39 return Ok(res); 39 return Ok(res);
40 40
41 fn collect_file(acc: &mut Vec<Assist>, path: &Path) -> Result<()> { 41 fn collect_file(acc: &mut Vec<Assist>, path: &Path) -> Result<()> {
42 let text = fs::read_to_string(path)?; 42 let text = xshell::read_file(path)?;
43 let comment_blocks = extract_comment_blocks_with_empty_lines("Assist", &text); 43 let comment_blocks = extract_comment_blocks_with_empty_lines("Assist", &text);
44 44
45 for block in comment_blocks { 45 for block in comment_blocks {
diff --git a/xtask/src/codegen/gen_diagnostic_docs.rs b/xtask/src/codegen/gen_diagnostic_docs.rs
new file mode 100644
index 000000000..00aaea5b7
--- /dev/null
+++ b/xtask/src/codegen/gen_diagnostic_docs.rs
@@ -0,0 +1,74 @@
1//! Generates `assists.md` documentation.
2
3use std::{fmt, path::PathBuf};
4
5use crate::{
6 codegen::{self, extract_comment_blocks_with_empty_lines, Location, Mode, PREAMBLE},
7 project_root, rust_files, Result,
8};
9
10pub fn generate_diagnostic_docs(mode: Mode) -> Result<()> {
11 let diagnostics = Diagnostic::collect()?;
12 let contents =
13 diagnostics.into_iter().map(|it| it.to_string()).collect::<Vec<_>>().join("\n\n");
14 let contents = format!("//{}\n{}\n", PREAMBLE, contents.trim());
15 let dst = project_root().join("docs/user/generated_diagnostic.adoc");
16 codegen::update(&dst, &contents, mode)?;
17 Ok(())
18}
19
20#[derive(Debug)]
21struct Diagnostic {
22 id: String,
23 location: Location,
24 doc: String,
25}
26
27impl Diagnostic {
28 fn collect() -> Result<Vec<Diagnostic>> {
29 let mut res = Vec::new();
30 for path in rust_files(&project_root()) {
31 collect_file(&mut res, path)?;
32 }
33 res.sort_by(|lhs, rhs| lhs.id.cmp(&rhs.id));
34 return Ok(res);
35
36 fn collect_file(acc: &mut Vec<Diagnostic>, path: PathBuf) -> Result<()> {
37 let text = xshell::read_file(&path)?;
38 let comment_blocks = extract_comment_blocks_with_empty_lines("Diagnostic", &text);
39
40 for block in comment_blocks {
41 let id = block.id;
42 if let Err(msg) = is_valid_diagnostic_name(&id) {
43 panic!("invalid diagnostic name: {:?}:\n {}", id, msg)
44 }
45 let doc = block.contents.join("\n");
46 let location = Location::new(path.clone(), block.line);
47 acc.push(Diagnostic { id, location, doc })
48 }
49
50 Ok(())
51 }
52 }
53}
54
55fn is_valid_diagnostic_name(diagnostic: &str) -> Result<(), String> {
56 let diagnostic = diagnostic.trim();
57 if diagnostic.find(char::is_whitespace).is_some() {
58 return Err("Diagnostic names can't contain whitespace symbols".into());
59 }
60 if diagnostic.chars().any(|c| c.is_ascii_uppercase()) {
61 return Err("Diagnostic names can't contain uppercase symbols".into());
62 }
63 if diagnostic.chars().any(|c| !c.is_ascii()) {
64 return Err("Diagnostic can't contain non-ASCII symbols".into());
65 }
66
67 Ok(())
68}
69
70impl fmt::Display for Diagnostic {
71 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72 writeln!(f, "=== {}\n**Source:** {}\n{}", self.id, self.location, self.doc)
73 }
74}
diff --git a/xtask/src/codegen/gen_feature_docs.rs b/xtask/src/codegen/gen_feature_docs.rs
index 341e67c73..065dd33f1 100644
--- a/xtask/src/codegen/gen_feature_docs.rs
+++ b/xtask/src/codegen/gen_feature_docs.rs
@@ -1,6 +1,6 @@
1//! Generates `assists.md` documentation. 1//! Generates `assists.md` documentation.
2 2
3use std::{fmt, fs, path::PathBuf}; 3use std::{fmt, path::PathBuf};
4 4
5use crate::{ 5use crate::{
6 codegen::{self, extract_comment_blocks_with_empty_lines, Location, Mode, PREAMBLE}, 6 codegen::{self, extract_comment_blocks_with_empty_lines, Location, Mode, PREAMBLE},
@@ -33,7 +33,7 @@ impl Feature {
33 return Ok(res); 33 return Ok(res);
34 34
35 fn collect_file(acc: &mut Vec<Feature>, path: PathBuf) -> Result<()> { 35 fn collect_file(acc: &mut Vec<Feature>, path: PathBuf) -> Result<()> {
36 let text = fs::read_to_string(&path)?; 36 let text = xshell::read_file(&path)?;
37 let comment_blocks = extract_comment_blocks_with_empty_lines("Feature", &text); 37 let comment_blocks = extract_comment_blocks_with_empty_lines("Feature", &text);
38 38
39 for block in comment_blocks { 39 for block in comment_blocks {
diff --git a/xtask/src/codegen/gen_features.rs b/xtask/src/codegen/gen_features.rs
index b58c4a0c9..3cf15ce02 100644
--- a/xtask/src/codegen/gen_features.rs
+++ b/xtask/src/codegen/gen_features.rs
@@ -3,15 +3,13 @@ use std::path::{Path, PathBuf};
3 3
4use quote::quote; 4use quote::quote;
5use walkdir::WalkDir; 5use walkdir::WalkDir;
6use xshell::{cmd, read_file};
6 7
7use crate::{ 8use crate::codegen::{project_root, reformat, update, Mode, Result};
8 codegen::{project_root, reformat, update, Mode, Result},
9 not_bash::{fs2, run},
10};
11 9
12pub fn generate_features(mode: Mode) -> Result<()> { 10pub fn generate_features(mode: Mode) -> Result<()> {
13 if !Path::new("./target/rust").exists() { 11 if !Path::new("./target/rust").exists() {
14 run!("git clone https://github.com/rust-lang/rust ./target/rust")?; 12 cmd!("git clone https://github.com/rust-lang/rust ./target/rust").run()?;
15 } 13 }
16 14
17 let contents = generate_descriptor("./target/rust/src/doc/unstable-book/src".into())?; 15 let contents = generate_descriptor("./target/rust/src/doc/unstable-book/src".into())?;
@@ -34,7 +32,7 @@ fn generate_descriptor(src_dir: PathBuf) -> Result<String> {
34 .map(|entry| { 32 .map(|entry| {
35 let path = entry.path(); 33 let path = entry.path();
36 let feature_ident = path.file_stem().unwrap().to_str().unwrap().replace("-", "_"); 34 let feature_ident = path.file_stem().unwrap().to_str().unwrap().replace("-", "_");
37 let doc = fs2::read_to_string(path).unwrap(); 35 let doc = read_file(path).unwrap();
38 36
39 quote! { LintCompletion { label: #feature_ident, description: #doc } } 37 quote! { LintCompletion { label: #feature_ident, description: #doc } }
40 }); 38 });
diff --git a/xtask/src/codegen/gen_lint_completions.rs b/xtask/src/codegen/gen_lint_completions.rs
new file mode 100644
index 000000000..b97421217
--- /dev/null
+++ b/xtask/src/codegen/gen_lint_completions.rs
@@ -0,0 +1,113 @@
1//! Generates descriptors structure for unstable feature from Unstable Book
2use std::path::{Path, PathBuf};
3
4use quote::quote;
5use walkdir::WalkDir;
6use xshell::{cmd, read_file};
7
8use crate::{
9 codegen::{project_root, reformat, update, Mode, Result},
10 run_rustfmt,
11};
12
13pub fn generate_lint_completions(mode: Mode) -> Result<()> {
14 if !Path::new("./target/rust").exists() {
15 cmd!("git clone --depth=1 https://github.com/rust-lang/rust ./target/rust").run()?;
16 }
17
18 let ts_features = generate_descriptor("./target/rust/src/doc/unstable-book/src".into())?;
19 cmd!("curl http://rust-lang.github.io/rust-clippy/master/lints.json --output ./target/clippy_lints.json").run()?;
20
21 let ts_clippy = generate_descriptor_clippy(&Path::new("./target/clippy_lints.json"))?;
22 let ts = quote! {
23 use crate::completions::attribute::LintCompletion;
24 #ts_features
25 #ts_clippy
26 };
27 let contents = reformat(ts.to_string().as_str())?;
28
29 let destination = project_root().join("crates/completion/src/generated_lint_completions.rs");
30 update(destination.as_path(), &contents, mode)?;
31 run_rustfmt(mode)?;
32
33 Ok(())
34}
35
36fn generate_descriptor(src_dir: PathBuf) -> Result<proc_macro2::TokenStream> {
37 let definitions = ["language-features", "library-features"]
38 .iter()
39 .flat_map(|it| WalkDir::new(src_dir.join(it)))
40 .filter_map(|e| e.ok())
41 .filter(|entry| {
42 // Get all `.md ` files
43 entry.file_type().is_file() && entry.path().extension().unwrap_or_default() == "md"
44 })
45 .map(|entry| {
46 let path = entry.path();
47 let feature_ident = path.file_stem().unwrap().to_str().unwrap().replace("-", "_");
48 let doc = read_file(path).unwrap();
49
50 quote! { LintCompletion { label: #feature_ident, description: #doc } }
51 });
52
53 let ts = quote! {
54 pub(super) const FEATURES: &[LintCompletion] = &[
55 #(#definitions),*
56 ];
57 };
58
59 Ok(ts)
60}
61
62#[derive(Default)]
63struct ClippyLint {
64 help: String,
65 id: String,
66}
67
68fn generate_descriptor_clippy(path: &Path) -> Result<proc_macro2::TokenStream> {
69 let file_content = read_file(path)?;
70 let mut clippy_lints: Vec<ClippyLint> = vec![];
71
72 for line in file_content.lines().map(|line| line.trim()) {
73 if line.starts_with(r#""id":"#) {
74 let clippy_lint = ClippyLint {
75 id: line
76 .strip_prefix(r#""id": ""#)
77 .expect("should be prefixed by id")
78 .strip_suffix(r#"","#)
79 .expect("should be suffixed by comma")
80 .into(),
81 help: String::new(),
82 };
83 clippy_lints.push(clippy_lint)
84 } else if line.starts_with(r#""What it does":"#) {
85 // Typical line to strip: "What is doest": "Here is my useful content",
86 let prefix_to_strip = r#""What it does": ""#;
87 let suffix_to_strip = r#"","#;
88
89 let clippy_lint = clippy_lints.last_mut().expect("clippy lint must already exist");
90 clippy_lint.help = line
91 .strip_prefix(prefix_to_strip)
92 .expect("should be prefixed by what it does")
93 .strip_suffix(suffix_to_strip)
94 .expect("should be suffixed by comma")
95 .into();
96 }
97 }
98
99 let definitions = clippy_lints.into_iter().map(|clippy_lint| {
100 let lint_ident = format!("clippy::{}", clippy_lint.id);
101 let doc = clippy_lint.help;
102
103 quote! { LintCompletion { label: #lint_ident, description: #doc } }
104 });
105
106 let ts = quote! {
107 pub(super) const CLIPPY_LINTS: &[LintCompletion] = &[
108 #(#definitions),*
109 ];
110 };
111
112 Ok(ts)
113}
diff --git a/xtask/src/codegen/gen_parser_tests.rs b/xtask/src/codegen/gen_parser_tests.rs
index 96fdd9216..19ae949d4 100644
--- a/xtask/src/codegen/gen_parser_tests.rs
+++ b/xtask/src/codegen/gen_parser_tests.rs
@@ -124,7 +124,7 @@ fn existing_tests(dir: &Path, ok: bool) -> Result<HashMap<String, (PathBuf, Test
124 let file_name = path.file_name().unwrap().to_str().unwrap(); 124 let file_name = path.file_name().unwrap().to_str().unwrap();
125 file_name[5..file_name.len() - 3].to_string() 125 file_name[5..file_name.len() - 3].to_string()
126 }; 126 };
127 let text = fs::read_to_string(&path)?; 127 let text = xshell::read_file(&path)?;
128 let test = Test { name: name.clone(), text, ok }; 128 let test = Test { name: name.clone(), text, ok };
129 if let Some(old) = res.insert(name, (path, test)) { 129 if let Some(old) = res.insert(name, (path, test)) {
130 println!("Duplicate test: {:?}", old); 130 println!("Duplicate test: {:?}", old);
diff --git a/xtask/src/dist.rs b/xtask/src/dist.rs
index aa7d94967..9e15a5a4c 100644
--- a/xtask/src/dist.rs
+++ b/xtask/src/dist.rs
@@ -1,4 +1,3 @@
1use flate2::{write::GzEncoder, Compression};
2use std::{ 1use std::{
3 env, 2 env,
4 fs::File, 3 fs::File,
@@ -7,11 +6,10 @@ use std::{
7}; 6};
8 7
9use anyhow::Result; 8use anyhow::Result;
9use flate2::{write::GzEncoder, Compression};
10use xshell::{cmd, cp, mkdir_p, pushd, read_file, rm_rf, write_file};
10 11
11use crate::{ 12use crate::{date_iso, project_root};
12 not_bash::{date_iso, fs2, pushd, rm_rf, run},
13 project_root,
14};
15 13
16pub struct DistCmd { 14pub struct DistCmd {
17 pub nightly: bool, 15 pub nightly: bool,
@@ -22,7 +20,7 @@ impl DistCmd {
22 pub fn run(self) -> Result<()> { 20 pub fn run(self) -> Result<()> {
23 let dist = project_root().join("dist"); 21 let dist = project_root().join("dist");
24 rm_rf(&dist)?; 22 rm_rf(&dist)?;
25 fs2::create_dir_all(&dist)?; 23 mkdir_p(&dist)?;
26 24
27 if let Some(version) = self.client_version { 25 if let Some(version) = self.client_version {
28 let release_tag = if self.nightly { "nightly".to_string() } else { date_iso()? }; 26 let release_tag = if self.nightly { "nightly".to_string() } else { date_iso()? };
@@ -34,7 +32,7 @@ impl DistCmd {
34} 32}
35 33
36fn dist_client(version: &str, release_tag: &str) -> Result<()> { 34fn dist_client(version: &str, release_tag: &str) -> Result<()> {
37 let _d = pushd("./editors/code"); 35 let _d = pushd("./editors/code")?;
38 let nightly = release_tag == "nightly"; 36 let nightly = release_tag == "nightly";
39 37
40 let mut patch = Patch::new("./package.json")?; 38 let mut patch = Patch::new("./package.json")?;
@@ -54,20 +52,16 @@ fn dist_client(version: &str, release_tag: &str) -> Result<()> {
54 } 52 }
55 patch.commit()?; 53 patch.commit()?;
56 54
57 run!("npm ci")?; 55 cmd!("npm ci").run()?;
58 run!("npx vsce package -o ../../dist/rust-analyzer.vsix")?; 56 cmd!("npx vsce package -o ../../dist/rust-analyzer.vsix").run()?;
59 Ok(()) 57 Ok(())
60} 58}
61 59
62fn dist_server() -> Result<()> { 60fn dist_server() -> Result<()> {
63 if cfg!(target_os = "linux") { 61 if cfg!(target_os = "linux") {
64 env::set_var("CC", "clang"); 62 env::set_var("CC", "clang");
65 run!(
66 "cargo build --manifest-path ./crates/rust-analyzer/Cargo.toml --bin rust-analyzer --release"
67 )?;
68 } else {
69 run!("cargo build --manifest-path ./crates/rust-analyzer/Cargo.toml --bin rust-analyzer --release")?;
70 } 63 }
64 cmd!("cargo build --manifest-path ./crates/rust-analyzer/Cargo.toml --bin rust-analyzer --release").run()?;
71 65
72 let (src, dst) = if cfg!(target_os = "linux") { 66 let (src, dst) = if cfg!(target_os = "linux") {
73 ("./target/release/rust-analyzer", "./dist/rust-analyzer-linux") 67 ("./target/release/rust-analyzer", "./dist/rust-analyzer-linux")
@@ -82,7 +76,7 @@ fn dist_server() -> Result<()> {
82 let src = Path::new(src); 76 let src = Path::new(src);
83 let dst = Path::new(dst); 77 let dst = Path::new(dst);
84 78
85 fs2::copy(&src, &dst)?; 79 cp(&src, &dst)?;
86 gzip(&src, &dst.with_extension("gz"))?; 80 gzip(&src, &dst.with_extension("gz"))?;
87 81
88 Ok(()) 82 Ok(())
@@ -105,7 +99,7 @@ struct Patch {
105impl Patch { 99impl Patch {
106 fn new(path: impl Into<PathBuf>) -> Result<Patch> { 100 fn new(path: impl Into<PathBuf>) -> Result<Patch> {
107 let path = path.into(); 101 let path = path.into();
108 let contents = fs2::read_to_string(&path)?; 102 let contents = read_file(&path)?;
109 Ok(Patch { path, original_contents: contents.clone(), contents }) 103 Ok(Patch { path, original_contents: contents.clone(), contents })
110 } 104 }
111 105
@@ -115,13 +109,14 @@ impl Patch {
115 self 109 self
116 } 110 }
117 111
118 fn commit(&self) -> io::Result<()> { 112 fn commit(&self) -> Result<()> {
119 fs2::write(&self.path, &self.contents) 113 write_file(&self.path, &self.contents)?;
114 Ok(())
120 } 115 }
121} 116}
122 117
123impl Drop for Patch { 118impl Drop for Patch {
124 fn drop(&mut self) { 119 fn drop(&mut self) {
125 fs2::write(&self.path, &self.original_contents).unwrap(); 120 write_file(&self.path, &self.original_contents).unwrap();
126 } 121 }
127} 122}
diff --git a/xtask/src/install.rs b/xtask/src/install.rs
index fcc4f05e4..78a8af797 100644
--- a/xtask/src/install.rs
+++ b/xtask/src/install.rs
@@ -3,8 +3,7 @@
3use std::{env, path::PathBuf, str}; 3use std::{env, path::PathBuf, str};
4 4
5use anyhow::{bail, format_err, Context, Result}; 5use anyhow::{bail, format_err, Context, Result};
6 6use xshell::{cmd, pushd};
7use crate::not_bash::{pushd, run};
8 7
9// Latest stable, feel free to send a PR if this lags behind. 8// Latest stable, feel free to send a PR if this lags behind.
10const REQUIRED_RUST_VERSION: u32 = 47; 9const REQUIRED_RUST_VERSION: u32 = 47;
@@ -14,8 +13,43 @@ pub struct InstallCmd {
14 pub server: Option<ServerOpt>, 13 pub server: Option<ServerOpt>,
15} 14}
16 15
16#[derive(Clone, Copy)]
17pub enum ClientOpt { 17pub enum ClientOpt {
18 VsCode, 18 VsCode,
19 VsCodeInsiders,
20 VsCodium,
21 VsCodeOss,
22 Any,
23}
24
25impl ClientOpt {
26 pub const fn as_cmds(&self) -> &'static [&'static str] {
27 match self {
28 ClientOpt::VsCode => &["code"],
29 ClientOpt::VsCodeInsiders => &["code-insiders"],
30 ClientOpt::VsCodium => &["codium"],
31 ClientOpt::VsCodeOss => &["code-oss"],
32 ClientOpt::Any => &["code", "code-insiders", "codium", "code-oss"],
33 }
34 }
35}
36
37impl Default for ClientOpt {
38 fn default() -> Self {
39 ClientOpt::Any
40 }
41}
42
43impl std::str::FromStr for ClientOpt {
44 type Err = anyhow::Error;
45
46 fn from_str(s: &str) -> Result<Self, Self::Err> {
47 [ClientOpt::VsCode, ClientOpt::VsCodeInsiders, ClientOpt::VsCodium, ClientOpt::VsCodeOss]
48 .iter()
49 .copied()
50 .find(|c| [s] == c.as_cmds())
51 .ok_or_else(|| anyhow::format_err!("no such client"))
52 }
19} 53}
20 54
21pub struct ServerOpt { 55pub struct ServerOpt {
@@ -75,38 +109,35 @@ fn fix_path_for_mac() -> Result<()> {
75 Ok(()) 109 Ok(())
76} 110}
77 111
78fn install_client(ClientOpt::VsCode: ClientOpt) -> Result<()> { 112fn install_client(client_opt: ClientOpt) -> Result<()> {
79 let _dir = pushd("./editors/code"); 113 let _dir = pushd("./editors/code");
80 114
81 let find_code = |f: fn(&str) -> bool| -> Result<&'static str> { 115 let find_code = |f: fn(&str) -> bool| -> Result<&'static str> {
82 ["code", "code-insiders", "codium", "code-oss"] 116 client_opt.as_cmds().iter().copied().find(|bin| f(bin)).ok_or_else(|| {
83 .iter() 117 format_err!("Can't execute `code --version`. Perhaps it is not in $PATH?")
84 .copied() 118 })
85 .find(|bin| f(bin))
86 .ok_or_else(|| {
87 format_err!("Can't execute `code --version`. Perhaps it is not in $PATH?")
88 })
89 }; 119 };
90 120
91 let installed_extensions = if cfg!(unix) { 121 let installed_extensions = if cfg!(unix) {
92 run!("npm --version").context("`npm` is required to build the VS Code plugin")?; 122 cmd!("npm --version").run().context("`npm` is required to build the VS Code plugin")?;
93 run!("npm install")?; 123 cmd!("npm install").run()?;
94 124
95 run!("npm run package --scripts-prepend-node-path")?; 125 cmd!("npm run package --scripts-prepend-node-path").run()?;
96 126
97 let code = find_code(|bin| run!("{} --version", bin).is_ok())?; 127 let code = find_code(|bin| cmd!("{bin} --version").read().is_ok())?;
98 run!("{} --install-extension rust-analyzer.vsix --force", code)?; 128 cmd!("{code} --install-extension rust-analyzer.vsix --force").run()?;
99 run!("{} --list-extensions", code; echo = false)? 129 cmd!("{code} --list-extensions").read()?
100 } else { 130 } else {
101 run!("cmd.exe /c npm --version") 131 cmd!("cmd.exe /c npm --version")
132 .run()
102 .context("`npm` is required to build the VS Code plugin")?; 133 .context("`npm` is required to build the VS Code plugin")?;
103 run!("cmd.exe /c npm install")?; 134 cmd!("cmd.exe /c npm install").run()?;
104 135
105 run!("cmd.exe /c npm run package")?; 136 cmd!("cmd.exe /c npm run package").run()?;
106 137
107 let code = find_code(|bin| run!("cmd.exe /c {}.cmd --version", bin).is_ok())?; 138 let code = find_code(|bin| cmd!("cmd.exe /c {bin}.cmd --version").read().is_ok())?;
108 run!(r"cmd.exe /c {}.cmd --install-extension rust-analyzer.vsix --force", code)?; 139 cmd!("cmd.exe /c {code}.cmd --install-extension rust-analyzer.vsix --force").run()?;
109 run!("cmd.exe /c {}.cmd --list-extensions", code; echo = false)? 140 cmd!("cmd.exe /c {code}.cmd --list-extensions").read()?
110 }; 141 };
111 142
112 if !installed_extensions.contains("rust-analyzer") { 143 if !installed_extensions.contains("rust-analyzer") {
@@ -122,7 +153,7 @@ fn install_client(ClientOpt::VsCode: ClientOpt) -> Result<()> {
122 153
123fn install_server(opts: ServerOpt) -> Result<()> { 154fn install_server(opts: ServerOpt) -> Result<()> {
124 let mut old_rust = false; 155 let mut old_rust = false;
125 if let Ok(stdout) = run!("cargo --version") { 156 if let Ok(stdout) = cmd!("cargo --version").read() {
126 if !check_version(&stdout, REQUIRED_RUST_VERSION) { 157 if !check_version(&stdout, REQUIRED_RUST_VERSION) {
127 old_rust = true; 158 old_rust = true;
128 } 159 }
@@ -134,12 +165,13 @@ fn install_server(opts: ServerOpt) -> Result<()> {
134 REQUIRED_RUST_VERSION, 165 REQUIRED_RUST_VERSION,
135 ) 166 )
136 } 167 }
137 168 let features = match opts.malloc {
138 let malloc_feature = match opts.malloc { 169 Malloc::System => &[][..],
139 Malloc::System => "", 170 Malloc::Mimalloc => &["--features", "mimalloc"],
140 Malloc::Mimalloc => "--features mimalloc",
141 }; 171 };
142 let res = run!("cargo install --path crates/rust-analyzer --locked --force {}", malloc_feature); 172
173 let cmd = cmd!("cargo install --path crates/rust-analyzer --locked --force {features...}");
174 let res = cmd.run();
143 175
144 if res.is_err() && old_rust { 176 if res.is_err() && old_rust {
145 eprintln!( 177 eprintln!(
@@ -148,7 +180,8 @@ fn install_server(opts: ServerOpt) -> Result<()> {
148 ); 180 );
149 } 181 }
150 182
151 res.map(drop) 183 res?;
184 Ok(())
152} 185}
153 186
154fn check_version(version_output: &str, min_minor_version: u32) -> bool { 187fn check_version(version_output: &str, min_minor_version: u32) -> bool {
diff --git a/xtask/src/lib.rs b/xtask/src/lib.rs
index e790d995f..babec2dbd 100644
--- a/xtask/src/lib.rs
+++ b/xtask/src/lib.rs
@@ -2,7 +2,6 @@
2//! 2//!
3//! See https://github.com/matklad/cargo-xtask/ 3//! See https://github.com/matklad/cargo-xtask/
4 4
5pub mod not_bash;
6pub mod codegen; 5pub mod codegen;
7mod ast_src; 6mod ast_src;
8 7
@@ -19,11 +18,9 @@ use std::{
19}; 18};
20 19
21use walkdir::{DirEntry, WalkDir}; 20use walkdir::{DirEntry, WalkDir};
21use xshell::{cmd, pushd, pushenv};
22 22
23use crate::{ 23use crate::codegen::Mode;
24 codegen::Mode,
25 not_bash::{pushd, pushenv},
26};
27 24
28pub use anyhow::{bail, Context as _, Result}; 25pub use anyhow::{bail, Context as _, Result};
29 26
@@ -53,18 +50,19 @@ pub fn rust_files(path: &Path) -> impl Iterator<Item = PathBuf> {
53} 50}
54 51
55pub fn run_rustfmt(mode: Mode) -> Result<()> { 52pub fn run_rustfmt(mode: Mode) -> Result<()> {
56 let _dir = pushd(project_root()); 53 let _dir = pushd(project_root())?;
57 let _e = pushenv("RUSTUP_TOOLCHAIN", "stable"); 54 let _e = pushenv("RUSTUP_TOOLCHAIN", "stable");
58 ensure_rustfmt()?; 55 ensure_rustfmt()?;
59 match mode { 56 let check = match mode {
60 Mode::Overwrite => run!("cargo fmt"), 57 Mode::Overwrite => &[][..],
61 Mode::Verify => run!("cargo fmt -- --check"), 58 Mode::Verify => &["--", "--check"],
62 }?; 59 };
60 cmd!("cargo fmt {check...}").run()?;
63 Ok(()) 61 Ok(())
64} 62}
65 63
66fn ensure_rustfmt() -> Result<()> { 64fn ensure_rustfmt() -> Result<()> {
67 let out = run!("rustfmt --version")?; 65 let out = cmd!("rustfmt --version").read()?;
68 if !out.contains("stable") { 66 if !out.contains("stable") {
69 bail!( 67 bail!(
70 "Failed to run rustfmt from toolchain 'stable'. \ 68 "Failed to run rustfmt from toolchain 'stable'. \
@@ -75,40 +73,46 @@ fn ensure_rustfmt() -> Result<()> {
75} 73}
76 74
77pub fn run_clippy() -> Result<()> { 75pub fn run_clippy() -> Result<()> {
78 if run!("cargo clippy --version").is_err() { 76 if cmd!("cargo clippy --version").read().is_err() {
79 bail!( 77 bail!(
80 "Failed run cargo clippy. \ 78 "Failed run cargo clippy. \
81 Please run `rustup component add clippy` to install it.", 79 Please run `rustup component add clippy` to install it.",
82 ) 80 )
83 } 81 }
84 82
85 let allowed_lints = [ 83 let allowed_lints = "
86 "clippy::collapsible_if", 84 -A clippy::collapsible_if
87 "clippy::needless_pass_by_value", 85 -A clippy::needless_pass_by_value
88 "clippy::nonminimal_bool", 86 -A clippy::nonminimal_bool
89 "clippy::redundant_pattern_matching", 87 -A clippy::redundant_pattern_matching
90 ]; 88 "
91 run!("cargo clippy --all-features --all-targets -- -A {}", allowed_lints.join(" -A "))?; 89 .split_ascii_whitespace();
90 cmd!("cargo clippy --all-features --all-targets -- {allowed_lints...}").run()?;
92 Ok(()) 91 Ok(())
93} 92}
94 93
95pub fn run_fuzzer() -> Result<()> { 94pub fn run_fuzzer() -> Result<()> {
96 let _d = pushd("./crates/syntax"); 95 let _d = pushd("./crates/syntax")?;
97 let _e = pushenv("RUSTUP_TOOLCHAIN", "nightly"); 96 let _e = pushenv("RUSTUP_TOOLCHAIN", "nightly");
98 if run!("cargo fuzz --help").is_err() { 97 if cmd!("cargo fuzz --help").read().is_err() {
99 run!("cargo install cargo-fuzz")?; 98 cmd!("cargo install cargo-fuzz").run()?;
100 }; 99 };
101 100
102 // Expecting nightly rustc 101 // Expecting nightly rustc
103 let out = run!("rustc --version")?; 102 let out = cmd!("rustc --version").read()?;
104 if !out.contains("nightly") { 103 if !out.contains("nightly") {
105 bail!("fuzz tests require nightly rustc") 104 bail!("fuzz tests require nightly rustc")
106 } 105 }
107 106
108 run!("cargo fuzz run parser")?; 107 cmd!("cargo fuzz run parser").run()?;
109 Ok(()) 108 Ok(())
110} 109}
111 110
111fn date_iso() -> Result<String> {
112 let res = cmd!("date --iso --utc").read()?;
113 Ok(res)
114}
115
112fn is_release_tag(tag: &str) -> bool { 116fn is_release_tag(tag: &str) -> bool {
113 tag.len() == "2020-02-24".len() && tag.starts_with(|c: char| c.is_ascii_digit()) 117 tag.len() == "2020-02-24".len() && tag.starts_with(|c: char| c.is_ascii_digit())
114} 118}
diff --git a/xtask/src/main.rs b/xtask/src/main.rs
index 3f4aa5497..536a67047 100644
--- a/xtask/src/main.rs
+++ b/xtask/src/main.rs
@@ -12,12 +12,12 @@ use std::env;
12 12
13use codegen::CodegenCmd; 13use codegen::CodegenCmd;
14use pico_args::Arguments; 14use pico_args::Arguments;
15use xshell::pushd;
15use xtask::{ 16use xtask::{
16 codegen::{self, Mode}, 17 codegen::{self, Mode},
17 dist::DistCmd, 18 dist::DistCmd,
18 install::{ClientOpt, InstallCmd, Malloc, ServerOpt}, 19 install::{InstallCmd, Malloc, ServerOpt},
19 metrics::MetricsCmd, 20 metrics::MetricsCmd,
20 not_bash::pushd,
21 pre_cache::PreCacheCmd, 21 pre_cache::PreCacheCmd,
22 pre_commit, project_root, 22 pre_commit, project_root,
23 release::{PromoteCmd, ReleaseCmd}, 23 release::{PromoteCmd, ReleaseCmd},
@@ -29,7 +29,7 @@ fn main() -> Result<()> {
29 return pre_commit::run_hook(); 29 return pre_commit::run_hook();
30 } 30 }
31 31
32 let _d = pushd(project_root()); 32 let _d = pushd(project_root())?;
33 33
34 let mut args = Arguments::from_env(); 34 let mut args = Arguments::from_env();
35 let subcommand = args.subcommand()?.unwrap_or_default(); 35 let subcommand = args.subcommand()?.unwrap_or_default();
@@ -46,19 +46,20 @@ USAGE:
46 cargo xtask install [FLAGS] 46 cargo xtask install [FLAGS]
47 47
48FLAGS: 48FLAGS:
49 --client-code Install only VS Code plugin 49 --client[=CLIENT] Install only VS Code plugin.
50 --server Install only the language server 50 CLIENT is one of 'code', 'code-insiders', 'codium', or 'code-oss'
51 --mimalloc Use mimalloc for server 51 --server Install only the language server
52 -h, --help Prints help information 52 --mimalloc Use mimalloc for server
53 -h, --help Prints help information
53 " 54 "
54 ); 55 );
55 return Ok(()); 56 return Ok(());
56 } 57 }
57 let server = args.contains("--server"); 58 let server = args.contains("--server");
58 let client_code = args.contains("--client-code"); 59 let client_code = args.contains("--client");
59 if server && client_code { 60 if server && client_code {
60 eprintln!( 61 eprintln!(
61 "error: The argument `--server` cannot be used with `--client-code`\n\n\ 62 "error: The argument `--server` cannot be used with `--client`\n\n\
62 For more information try --help" 63 For more information try --help"
63 ); 64 );
64 return Ok(()); 65 return Ok(());
@@ -67,10 +68,12 @@ FLAGS:
67 let malloc = 68 let malloc =
68 if args.contains("--mimalloc") { Malloc::Mimalloc } else { Malloc::System }; 69 if args.contains("--mimalloc") { Malloc::Mimalloc } else { Malloc::System };
69 70
71 let client_opt = args.opt_value_from_str("--client")?;
72
70 args.finish()?; 73 args.finish()?;
71 74
72 InstallCmd { 75 InstallCmd {
73 client: if server { None } else { Some(ClientOpt::VsCode) }, 76 client: if server { None } else { Some(client_opt.unwrap_or_default()) },
74 server: if client_code { None } else { Some(ServerOpt { malloc }) }, 77 server: if client_code { None } else { Some(ServerOpt { malloc }) },
75 } 78 }
76 .run() 79 .run()
diff --git a/xtask/src/metrics.rs b/xtask/src/metrics.rs
index 4bade2c7e..e0d1aaf97 100644
--- a/xtask/src/metrics.rs
+++ b/xtask/src/metrics.rs
@@ -7,8 +7,7 @@ use std::{
7}; 7};
8 8
9use anyhow::{bail, format_err, Result}; 9use anyhow::{bail, format_err, Result};
10 10use xshell::{cmd, mkdir_p, pushd, pushenv, read_file, rm_rf};
11use crate::not_bash::{fs2, pushd, pushenv, rm_rf, run};
12 11
13type Unit = String; 12type Unit = String;
14 13
@@ -23,12 +22,13 @@ impl MetricsCmd {
23 rm_rf("./target/release")?; 22 rm_rf("./target/release")?;
24 } 23 }
25 if !Path::new("./target/rustc-perf").exists() { 24 if !Path::new("./target/rustc-perf").exists() {
26 fs2::create_dir_all("./target/rustc-perf")?; 25 mkdir_p("./target/rustc-perf")?;
27 run!("git clone https://github.com/rust-lang/rustc-perf.git ./target/rustc-perf")?; 26 cmd!("git clone https://github.com/rust-lang/rustc-perf.git ./target/rustc-perf")
27 .run()?;
28 } 28 }
29 { 29 {
30 let _d = pushd("./target/rustc-perf"); 30 let _d = pushd("./target/rustc-perf")?;
31 run!("git reset --hard 1d9288b0da7febf2599917da1b57dc241a1af033")?; 31 cmd!("git reset --hard 1d9288b0da7febf2599917da1b57dc241a1af033").run()?;
32 } 32 }
33 33
34 let _env = pushenv("RA_METRICS", "1"); 34 let _env = pushenv("RA_METRICS", "1");
@@ -39,17 +39,20 @@ impl MetricsCmd {
39 metrics.measure_analysis_stats("webrender")?; 39 metrics.measure_analysis_stats("webrender")?;
40 40
41 if !self.dry_run { 41 if !self.dry_run {
42 let _d = pushd("target"); 42 let _d = pushd("target")?;
43 let metrics_token = env::var("METRICS_TOKEN").unwrap(); 43 let metrics_token = env::var("METRICS_TOKEN").unwrap();
44 let repo = format!("https://{}@github.com/rust-analyzer/metrics.git", metrics_token); 44 cmd!(
45 run!("git clone --depth 1 {}", repo)?; 45 "git clone --depth 1 https://{metrics_token}@github.com/rust-analyzer/metrics.git"
46 let _d = pushd("metrics"); 46 )
47 .run()?;
48 let _d = pushd("metrics")?;
47 49
48 let mut file = std::fs::OpenOptions::new().append(true).open("metrics.json")?; 50 let mut file = std::fs::OpenOptions::new().append(true).open("metrics.json")?;
49 writeln!(file, "{}", metrics.json())?; 51 writeln!(file, "{}", metrics.json())?;
50 run!("git add .")?; 52 cmd!("git add .").run()?;
51 run!("git -c user.name=Bot -c [email protected] commit --message 📈")?; 53 cmd!("git -c user.name=Bot -c [email protected] commit --message 📈")
52 run!("git push origin master")?; 54 .run()?;
55 cmd!("git push origin master").run()?;
53 } 56 }
54 eprintln!("{:#?}", metrics); 57 eprintln!("{:#?}", metrics);
55 Ok(()) 58 Ok(())
@@ -59,10 +62,10 @@ impl MetricsCmd {
59impl Metrics { 62impl Metrics {
60 fn measure_build(&mut self) -> Result<()> { 63 fn measure_build(&mut self) -> Result<()> {
61 eprintln!("\nMeasuring build"); 64 eprintln!("\nMeasuring build");
62 run!("cargo fetch")?; 65 cmd!("cargo fetch").run()?;
63 66
64 let time = Instant::now(); 67 let time = Instant::now();
65 run!("cargo build --release --package rust-analyzer --bin rust-analyzer")?; 68 cmd!("cargo build --release --package rust-analyzer --bin rust-analyzer").run()?;
66 let time = time.elapsed(); 69 let time = time.elapsed();
67 self.report("build", time.as_millis() as u64, "ms".into()); 70 self.report("build", time.as_millis() as u64, "ms".into());
68 Ok(()) 71 Ok(())
@@ -78,7 +81,7 @@ impl Metrics {
78 } 81 }
79 fn measure_analysis_stats_path(&mut self, name: &str, path: &str) -> Result<()> { 82 fn measure_analysis_stats_path(&mut self, name: &str, path: &str) -> Result<()> {
80 eprintln!("\nMeasuring analysis-stats/{}", name); 83 eprintln!("\nMeasuring analysis-stats/{}", name);
81 let output = run!("./target/release/rust-analyzer analysis-stats --quiet {}", path)?; 84 let output = cmd!("./target/release/rust-analyzer analysis-stats --quiet {path}").read()?;
82 for (metric, value, unit) in parse_metrics(&output) { 85 for (metric, value, unit) in parse_metrics(&output) {
83 self.report(&format!("analysis-stats/{}/{}", name, metric), value, unit.into()); 86 self.report(&format!("analysis-stats/{}/{}", name, metric), value, unit.into());
84 } 87 }
@@ -118,7 +121,7 @@ impl Metrics {
118 fn new() -> Result<Metrics> { 121 fn new() -> Result<Metrics> {
119 let host = Host::new()?; 122 let host = Host::new()?;
120 let timestamp = SystemTime::now(); 123 let timestamp = SystemTime::now();
121 let revision = run!("git rev-parse HEAD")?; 124 let revision = cmd!("git rev-parse HEAD").read()?;
122 Ok(Metrics { host, timestamp, revision, metrics: BTreeMap::new() }) 125 Ok(Metrics { host, timestamp, revision, metrics: BTreeMap::new() })
123 } 126 }
124 127
@@ -160,7 +163,7 @@ impl Host {
160 return Ok(Host { os, cpu, mem }); 163 return Ok(Host { os, cpu, mem });
161 164
162 fn read_field<'a>(path: &str, field: &str) -> Result<String> { 165 fn read_field<'a>(path: &str, field: &str) -> Result<String> {
163 let text = fs2::read_to_string(path)?; 166 let text = read_file(path)?;
164 167
165 let line = text 168 let line = text
166 .lines() 169 .lines()
diff --git a/xtask/src/not_bash.rs b/xtask/src/not_bash.rs
deleted file mode 100644
index 038898993..000000000
--- a/xtask/src/not_bash.rs
+++ /dev/null
@@ -1,169 +0,0 @@
1//! A bad shell -- small cross platform module for writing glue code
2
3use std::{
4 cell::RefCell,
5 env,
6 ffi::OsString,
7 io::{self, Write},
8 path::{Path, PathBuf},
9 process::{Command, Stdio},
10};
11
12use anyhow::{bail, Context, Result};
13
14pub use fs_err as fs2;
15
16#[macro_export]
17macro_rules! run {
18 ($($expr:expr),*) => {
19 run!($($expr),*; echo = true)
20 };
21 ($($expr:expr),* ; echo = $echo:expr) => {
22 $crate::not_bash::run_process(format!($($expr),*), $echo, None)
23 };
24 ($($expr:expr),* ; <$stdin:expr) => {
25 $crate::not_bash::run_process(format!($($expr),*), false, Some($stdin))
26 };
27}
28pub use crate::run;
29
30pub struct Pushd {
31 _p: (),
32}
33
34pub fn pushd(path: impl Into<PathBuf>) -> Pushd {
35 Env::with(|env| env.pushd(path.into()));
36 Pushd { _p: () }
37}
38
39impl Drop for Pushd {
40 fn drop(&mut self) {
41 Env::with(|env| env.popd())
42 }
43}
44
45pub struct Pushenv {
46 _p: (),
47}
48
49pub fn pushenv(var: &str, value: &str) -> Pushenv {
50 Env::with(|env| env.pushenv(var.into(), value.into()));
51 Pushenv { _p: () }
52}
53
54impl Drop for Pushenv {
55 fn drop(&mut self) {
56 Env::with(|env| env.popenv())
57 }
58}
59
60pub fn rm_rf(path: impl AsRef<Path>) -> io::Result<()> {
61 let path = path.as_ref();
62 if !path.exists() {
63 return Ok(());
64 }
65 if path.is_file() {
66 fs2::remove_file(path)
67 } else {
68 fs2::remove_dir_all(path)
69 }
70}
71
72#[doc(hidden)]
73pub fn run_process(cmd: String, echo: bool, stdin: Option<&[u8]>) -> Result<String> {
74 run_process_inner(&cmd, echo, stdin).with_context(|| format!("process `{}` failed", cmd))
75}
76
77pub fn date_iso() -> Result<String> {
78 run!("date --iso --utc")
79}
80
81fn run_process_inner(cmd: &str, echo: bool, stdin: Option<&[u8]>) -> Result<String> {
82 let mut args = shelx(cmd);
83 let binary = args.remove(0);
84 let current_dir = Env::with(|it| it.cwd().to_path_buf());
85
86 if echo {
87 println!("> {}", cmd)
88 }
89
90 let mut command = Command::new(binary);
91 command.args(args).current_dir(current_dir).stderr(Stdio::inherit());
92 let output = match stdin {
93 None => command.stdin(Stdio::null()).output(),
94 Some(stdin) => {
95 command.stdin(Stdio::piped()).stdout(Stdio::piped());
96 let mut process = command.spawn()?;
97 process.stdin.take().unwrap().write_all(stdin)?;
98 process.wait_with_output()
99 }
100 }?;
101 let stdout = String::from_utf8(output.stdout)?;
102
103 if echo {
104 print!("{}", stdout)
105 }
106
107 if !output.status.success() {
108 bail!("{}", output.status)
109 }
110
111 Ok(stdout.trim().to_string())
112}
113
114// FIXME: some real shell lexing here
115fn shelx(cmd: &str) -> Vec<String> {
116 let mut res = Vec::new();
117 for (string_piece, in_quotes) in cmd.split('\'').zip([false, true].iter().copied().cycle()) {
118 if in_quotes {
119 res.push(string_piece.to_string())
120 } else {
121 if !string_piece.is_empty() {
122 res.extend(string_piece.split_ascii_whitespace().map(|it| it.to_string()))
123 }
124 }
125 }
126 res
127}
128
129struct Env {
130 pushd_stack: Vec<PathBuf>,
131 pushenv_stack: Vec<(OsString, Option<OsString>)>,
132}
133
134impl Env {
135 fn with<F: FnOnce(&mut Env) -> T, T>(f: F) -> T {
136 thread_local! {
137 static ENV: RefCell<Env> = RefCell::new(Env {
138 pushd_stack: vec![env::current_dir().unwrap()],
139 pushenv_stack: vec![],
140 });
141 }
142 ENV.with(|it| f(&mut *it.borrow_mut()))
143 }
144
145 fn pushd(&mut self, dir: PathBuf) {
146 let dir = self.cwd().join(dir);
147 self.pushd_stack.push(dir);
148 env::set_current_dir(self.cwd())
149 .unwrap_or_else(|err| panic!("Failed to set cwd to {}: {}", self.cwd().display(), err));
150 }
151 fn popd(&mut self) {
152 self.pushd_stack.pop().unwrap();
153 env::set_current_dir(self.cwd()).unwrap();
154 }
155 fn pushenv(&mut self, var: OsString, value: OsString) {
156 self.pushenv_stack.push((var.clone(), env::var_os(&var)));
157 env::set_var(var, value)
158 }
159 fn popenv(&mut self) {
160 let (var, value) = self.pushenv_stack.pop().unwrap();
161 match value {
162 None => env::remove_var(var),
163 Some(value) => env::set_var(var, value),
164 }
165 }
166 fn cwd(&self) -> &Path {
167 self.pushd_stack.last().unwrap()
168 }
169}
diff --git a/xtask/src/pre_cache.rs b/xtask/src/pre_cache.rs
index 47ba6ba24..569f88f68 100644
--- a/xtask/src/pre_cache.rs
+++ b/xtask/src/pre_cache.rs
@@ -4,8 +4,7 @@ use std::{
4}; 4};
5 5
6use anyhow::Result; 6use anyhow::Result;
7 7use xshell::rm_rf;
8use crate::not_bash::{fs2, rm_rf};
9 8
10pub struct PreCacheCmd; 9pub struct PreCacheCmd;
11 10
@@ -26,7 +25,7 @@ impl PreCacheCmd {
26 } 25 }
27 } 26 }
28 27
29 fs2::remove_file("./target/.rustc_info.json")?; 28 rm_rf("./target/.rustc_info.json")?;
30 29
31 let to_delete = read_dir("./crates", FileType::is_dir)? 30 let to_delete = read_dir("./crates", FileType::is_dir)?
32 .into_iter() 31 .into_iter()
diff --git a/xtask/src/pre_commit.rs b/xtask/src/pre_commit.rs
index 056f34acf..8f2dbea19 100644
--- a/xtask/src/pre_commit.rs
+++ b/xtask/src/pre_commit.rs
@@ -3,19 +3,21 @@
3use std::{fs, path::PathBuf}; 3use std::{fs, path::PathBuf};
4 4
5use anyhow::{bail, Result}; 5use anyhow::{bail, Result};
6use xshell::cmd;
6 7
7use crate::{not_bash::run, project_root, run_rustfmt, Mode}; 8use crate::{project_root, run_rustfmt, Mode};
8 9
9// FIXME: if there are changed `.ts` files, also reformat TypeScript (by 10// FIXME: if there are changed `.ts` files, also reformat TypeScript (by
10// shelling out to `npm fmt`). 11// shelling out to `npm fmt`).
11pub fn run_hook() -> Result<()> { 12pub fn run_hook() -> Result<()> {
12 run_rustfmt(Mode::Overwrite)?; 13 run_rustfmt(Mode::Overwrite)?;
13 14
14 let diff = run!("git diff --diff-filter=MAR --name-only --cached")?; 15 let diff = cmd!("git diff --diff-filter=MAR --name-only --cached").read()?;
15 16
16 let root = project_root(); 17 let root = project_root();
17 for line in diff.lines() { 18 for line in diff.lines() {
18 run!("git update-index --add {}", root.join(line).display())?; 19 let file = root.join(line);
20 cmd!("git update-index --add {file}").run()?;
19 } 21 }
20 22
21 Ok(()) 23 Ok(())
diff --git a/xtask/src/release.rs b/xtask/src/release.rs
index 3aab29801..3cf0d849f 100644
--- a/xtask/src/release.rs
+++ b/xtask/src/release.rs
@@ -1,8 +1,6 @@
1use crate::{ 1use xshell::{cmd, cp, pushd, read_dir, write_file};
2 codegen, is_release_tag, 2
3 not_bash::{date_iso, fs2, pushd, run}, 3use crate::{codegen, date_iso, is_release_tag, project_root, Mode, Result};
4 project_root, Mode, Result,
5};
6 4
7pub struct ReleaseCmd { 5pub struct ReleaseCmd {
8 pub dry_run: bool, 6 pub dry_run: bool,
@@ -11,10 +9,10 @@ pub struct ReleaseCmd {
11impl ReleaseCmd { 9impl ReleaseCmd {
12 pub fn run(self) -> Result<()> { 10 pub fn run(self) -> Result<()> {
13 if !self.dry_run { 11 if !self.dry_run {
14 run!("git switch release")?; 12 cmd!("git switch release").run()?;
15 run!("git fetch upstream --tags --force")?; 13 cmd!("git fetch upstream --tags --force").run()?;
16 run!("git reset --hard tags/nightly")?; 14 cmd!("git reset --hard tags/nightly").run()?;
17 run!("git push")?; 15 cmd!("git push").run()?;
18 } 16 }
19 codegen::generate_assists_docs(Mode::Overwrite)?; 17 codegen::generate_assists_docs(Mode::Overwrite)?;
20 codegen::generate_feature_docs(Mode::Overwrite)?; 18 codegen::generate_feature_docs(Mode::Overwrite)?;
@@ -23,8 +21,8 @@ impl ReleaseCmd {
23 let changelog_dir = website_root.join("./thisweek/_posts"); 21 let changelog_dir = website_root.join("./thisweek/_posts");
24 22
25 let today = date_iso()?; 23 let today = date_iso()?;
26 let commit = run!("git rev-parse HEAD")?; 24 let commit = cmd!("git rev-parse HEAD").read()?;
27 let changelog_n = fs2::read_dir(changelog_dir.as_path())?.count(); 25 let changelog_n = read_dir(changelog_dir.as_path())?.len();
28 26
29 let contents = format!( 27 let contents = format!(
30 "\ 28 "\
@@ -52,20 +50,27 @@ https://github.com/sponsors/rust-analyzer[GitHub Sponsors].
52 ); 50 );
53 51
54 let path = changelog_dir.join(format!("{}-changelog-{}.adoc", today, changelog_n)); 52 let path = changelog_dir.join(format!("{}-changelog-{}.adoc", today, changelog_n));
55 fs2::write(&path, &contents)?; 53 write_file(&path, &contents)?;
56 54
57 for &adoc in ["manual.adoc", "generated_features.adoc", "generated_assists.adoc"].iter() { 55 for &adoc in [
56 "manual.adoc",
57 "generated_features.adoc",
58 "generated_assists.adoc",
59 "generated_diagnostic.adoc",
60 ]
61 .iter()
62 {
58 let src = project_root().join("./docs/user/").join(adoc); 63 let src = project_root().join("./docs/user/").join(adoc);
59 let dst = website_root.join(adoc); 64 let dst = website_root.join(adoc);
60 fs2::copy(src, dst)?; 65 cp(src, dst)?;
61 } 66 }
62 67
63 let tags = run!("git tag --list"; echo = false)?; 68 let tags = cmd!("git tag --list").read()?;
64 let prev_tag = tags.lines().filter(|line| is_release_tag(line)).last().unwrap(); 69 let prev_tag = tags.lines().filter(|line| is_release_tag(line)).last().unwrap();
65 70
66 let git_log = run!("git log {}..HEAD --merges --reverse", prev_tag; echo = false)?; 71 let git_log = cmd!("git log {prev_tag}..HEAD --merges --reverse").read()?;
67 let git_log_dst = website_root.join("git.log"); 72 let git_log_dst = website_root.join("git.log");
68 fs2::write(git_log_dst, &git_log)?; 73 write_file(git_log_dst, &git_log)?;
69 74
70 Ok(()) 75 Ok(())
71 } 76 }
@@ -77,27 +82,25 @@ pub struct PromoteCmd {
77 82
78impl PromoteCmd { 83impl PromoteCmd {
79 pub fn run(self) -> Result<()> { 84 pub fn run(self) -> Result<()> {
80 let _dir = pushd("../rust-rust-analyzer"); 85 let _dir = pushd("../rust-rust-analyzer")?;
81 run!("git switch master")?; 86 cmd!("git switch master").run()?;
82 run!("git fetch upstream")?; 87 cmd!("git fetch upstream").run()?;
83 run!("git reset --hard upstream/master")?; 88 cmd!("git reset --hard upstream/master").run()?;
84 run!("git submodule update --recursive")?; 89 cmd!("git submodule update --recursive").run()?;
85 90
86 let branch = format!("rust-analyzer-{}", date_iso()?); 91 let branch = format!("rust-analyzer-{}", date_iso()?);
87 run!("git switch -c {}", branch)?; 92 cmd!("git switch -c {branch}").run()?;
88 { 93 {
89 let _dir = pushd("src/tools/rust-analyzer"); 94 let _dir = pushd("src/tools/rust-analyzer")?;
90 run!("git fetch origin")?; 95 cmd!("git fetch origin").run()?;
91 run!("git reset --hard origin/release")?; 96 cmd!("git reset --hard origin/release").run()?;
92 } 97 }
93 run!("git add src/tools/rust-analyzer")?; 98 cmd!("git add src/tools/rust-analyzer").run()?;
94 run!("git commit -m':arrow_up: rust-analyzer'")?; 99 cmd!("git commit -m':arrow_up: rust-analyzer'").run()?;
95 if !self.dry_run { 100 if !self.dry_run {
96 run!("git push")?; 101 cmd!("git push").run()?;
97 run!( 102 cmd!("xdg-open https://github.com/matklad/rust/pull/new/{branch}?body=r%3F%20%40ghost")
98 "xdg-open https://github.com/matklad/rust/pull/new/{}?body=r%3F%20%40ghost", 103 .run()?;
99 branch
100 )?;
101 } 104 }
102 Ok(()) 105 Ok(())
103 } 106 }
diff --git a/xtask/tests/tidy.rs b/xtask/tests/tidy.rs
index b3bb9d543..9de60c76c 100644
--- a/xtask/tests/tidy.rs
+++ b/xtask/tests/tidy.rs
@@ -3,9 +3,9 @@ use std::{
3 path::{Path, PathBuf}, 3 path::{Path, PathBuf},
4}; 4};
5 5
6use xshell::{cmd, read_file};
6use xtask::{ 7use xtask::{
7 codegen::{self, Mode}, 8 codegen::{self, Mode},
8 not_bash::{fs2, run},
9 project_root, run_rustfmt, rust_files, 9 project_root, run_rustfmt, rust_files,
10}; 10};
11 11
@@ -42,20 +42,20 @@ fn smoke_test_docs_generation() {
42 // We don't commit docs to the repo, so we can just overwrite in tests. 42 // We don't commit docs to the repo, so we can just overwrite in tests.
43 codegen::generate_assists_docs(Mode::Overwrite).unwrap(); 43 codegen::generate_assists_docs(Mode::Overwrite).unwrap();
44 codegen::generate_feature_docs(Mode::Overwrite).unwrap(); 44 codegen::generate_feature_docs(Mode::Overwrite).unwrap();
45 codegen::generate_diagnostic_docs(Mode::Overwrite).unwrap();
45} 46}
46 47
47#[test] 48#[test]
48fn check_lsp_extensions_docs() { 49fn check_lsp_extensions_docs() {
49 let expected_hash = { 50 let expected_hash = {
50 let lsp_ext_rs = 51 let lsp_ext_rs =
51 fs2::read_to_string(project_root().join("crates/rust-analyzer/src/lsp_ext.rs")) 52 read_file(project_root().join("crates/rust-analyzer/src/lsp_ext.rs")).unwrap();
52 .unwrap();
53 stable_hash(lsp_ext_rs.as_str()) 53 stable_hash(lsp_ext_rs.as_str())
54 }; 54 };
55 55
56 let actual_hash = { 56 let actual_hash = {
57 let lsp_extensions_md = 57 let lsp_extensions_md =
58 fs2::read_to_string(project_root().join("docs/dev/lsp-extensions.md")).unwrap(); 58 read_file(project_root().join("docs/dev/lsp-extensions.md")).unwrap();
59 let text = lsp_extensions_md 59 let text = lsp_extensions_md
60 .lines() 60 .lines()
61 .find_map(|line| line.strip_prefix("lsp_ext.rs hash:")) 61 .find_map(|line| line.strip_prefix("lsp_ext.rs hash:"))
@@ -83,7 +83,7 @@ Please adjust docs/dev/lsp-extensions.md.
83fn rust_files_are_tidy() { 83fn rust_files_are_tidy() {
84 let mut tidy_docs = TidyDocs::default(); 84 let mut tidy_docs = TidyDocs::default();
85 for path in rust_files(&project_root().join("crates")) { 85 for path in rust_files(&project_root().join("crates")) {
86 let text = fs2::read_to_string(&path).unwrap(); 86 let text = read_file(&path).unwrap();
87 check_todo(&path, &text); 87 check_todo(&path, &text);
88 check_trailing_ws(&path, &text); 88 check_trailing_ws(&path, &text);
89 deny_clippy(&path, &text); 89 deny_clippy(&path, &text);
@@ -94,8 +94,10 @@ fn rust_files_are_tidy() {
94 94
95#[test] 95#[test]
96fn check_merge_commits() { 96fn check_merge_commits() {
97 let stdout = run!("git rev-list --merges --invert-grep --author 'bors\\[bot\\]' HEAD~19.."; echo = false) 97 let stdout =
98 .unwrap(); 98 dbg!(cmd!("git rev-list --merges --invert-grep --author 'bors\\[bot\\]' HEAD~19.."))
99 .read()
100 .unwrap();
99 if !stdout.is_empty() { 101 if !stdout.is_empty() {
100 panic!( 102 panic!(
101 " 103 "
@@ -129,6 +131,14 @@ https://github.blog/2015-06-08-how-to-undo-almost-anything-with-git/#redo-after-
129} 131}
130 132
131fn deny_clippy(path: &PathBuf, text: &String) { 133fn deny_clippy(path: &PathBuf, text: &String) {
134 let ignore = &[
135 // The documentation in string literals may contain anything for its own purposes
136 "completion/src/generated_lint_completions.rs",
137 ];
138 if ignore.iter().any(|p| path.ends_with(p)) {
139 return;
140 }
141
132 if text.contains("[\u{61}llow(clippy") { 142 if text.contains("[\u{61}llow(clippy") {
133 panic!( 143 panic!(
134 "\n\nallowing lints is forbidden: {}. 144 "\n\nallowing lints is forbidden: {}.
@@ -168,7 +178,7 @@ Zlib OR Apache-2.0 OR MIT
168 .filter(|it| !it.is_empty()) 178 .filter(|it| !it.is_empty())
169 .collect::<Vec<_>>(); 179 .collect::<Vec<_>>();
170 180
171 let meta = run!("cargo metadata --format-version 1"; echo = false).unwrap(); 181 let meta = cmd!("cargo metadata --format-version 1").read().unwrap();
172 let mut licenses = meta 182 let mut licenses = meta
173 .split(|c| c == ',' || c == '{' || c == '}') 183 .split(|c| c == ',' || c == '{' || c == '}')
174 .filter(|it| it.contains(r#""license""#)) 184 .filter(|it| it.contains(r#""license""#))
@@ -177,6 +187,25 @@ Zlib OR Apache-2.0 OR MIT
177 .collect::<Vec<_>>(); 187 .collect::<Vec<_>>();
178 licenses.sort(); 188 licenses.sort();
179 licenses.dedup(); 189 licenses.dedup();
190 if licenses != expected {
191 let mut diff = String::new();
192
193 diff += &format!("New Licenses:\n");
194 for &l in licenses.iter() {
195 if !expected.contains(&l) {
196 diff += &format!(" {}\n", l)
197 }
198 }
199
200 diff += &format!("\nMissing Licenses:\n");
201 for &l in expected.iter() {
202 if !licenses.contains(&l) {
203 diff += &format!(" {}\n", l)
204 }
205 }
206
207 panic!("different set of licenses!\n{}", diff);
208 }
180 assert_eq!(licenses, expected); 209 assert_eq!(licenses, expected);
181} 210}
182 211
@@ -193,7 +222,7 @@ fn check_todo(path: &Path, text: &str) {
193 // `ast::make`. 222 // `ast::make`.
194 "ast/make.rs", 223 "ast/make.rs",
195 // The documentation in string literals may contain anything for its own purposes 224 // The documentation in string literals may contain anything for its own purposes
196 "completion/generated_features.rs", 225 "completion/src/generated_lint_completions.rs",
197 ]; 226 ];
198 if need_todo.iter().any(|p| path.ends_with(p)) { 227 if need_todo.iter().any(|p| path.ends_with(p)) {
199 return; 228 return;