Skip to content

Commit

Permalink
bootstrap: Add support for ./x setup emacs
Browse files Browse the repository at this point in the history
Add support for automatically setting up the recommended
LSP config for Emacs.

Additionally, refactor setup.rs to make it easier to add support
for more editors in the future.
  • Loading branch information
mrkajetanp committed Oct 1, 2024
1 parent e6eb451 commit c53d4c7
Show file tree
Hide file tree
Showing 6 changed files with 161 additions and 89 deletions.
182 changes: 125 additions & 57 deletions src/bootstrap/src/core/build_steps/setup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,21 +35,6 @@ pub enum Profile {

static PROFILE_DIR: &str = "src/bootstrap/defaults";

/// A list of historical hashes of `src/etc/rust_analyzer_settings.json`.
/// New entries should be appended whenever this is updated so we can detect
/// outdated vs. user-modified settings files.
static SETTINGS_HASHES: &[&str] = &[
"ea67e259dedf60d4429b6c349a564ffcd1563cf41c920a856d1f5b16b4701ac8",
"56e7bf011c71c5d81e0bf42e84938111847a810eee69d906bba494ea90b51922",
"af1b5efe196aed007577899db9dae15d6dbc923d6fa42fa0934e68617ba9bbe0",
"3468fea433c25fff60be6b71e8a215a732a7b1268b6a83bf10d024344e140541",
"47d227f424bf889b0d899b9cc992d5695e1b78c406e183cd78eafefbe5488923",
"b526bd58d0262dd4dda2bff5bc5515b705fb668a46235ace3e057f807963a11a",
"828666b021d837a33e78d870b56d34c88a5e2c85de58b693607ec574f0c27000",
"811fb3b063c739d261fd8590dd30242e117908f5a095d594fa04585daa18ec4d",
];
static RUST_ANALYZER_SETTINGS: &str = include_str!("../../../../etc/rust_analyzer_settings.json");

impl Profile {
fn include_path(&self, src_path: &Path) -> PathBuf {
PathBuf::from(format!("{}/{PROFILE_DIR}/config.{}.toml", src_path.display(), self))
Expand Down Expand Up @@ -533,46 +518,123 @@ undesirable, simply delete the `pre-push` file from .git/hooks."
Ok(())
}

/// Sets up or displays `src/etc/rust_analyzer_settings.json`
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub struct Vscode;
/// Handles editor-specific setup differences
#[derive(Clone, Debug, Eq, PartialEq)]
enum EditorKind {
Vscode,
Emacs,
}

impl Step for Vscode {
type Output = ();
const DEFAULT: bool = true;
fn should_run(run: ShouldRun<'_>) -> ShouldRun<'_> {
run.alias("vscode")
impl EditorKind {
/// A list of historical hashes of each LSP settings file
/// New entries should be appended whenever this is updated so we can detect
/// outdated vs. user-modified settings files.
fn hashes(&self) -> Vec<&str> {
match self {
EditorKind::Vscode => vec![
"ea67e259dedf60d4429b6c349a564ffcd1563cf41c920a856d1f5b16b4701ac8",
"56e7bf011c71c5d81e0bf42e84938111847a810eee69d906bba494ea90b51922",
"af1b5efe196aed007577899db9dae15d6dbc923d6fa42fa0934e68617ba9bbe0",
"3468fea433c25fff60be6b71e8a215a732a7b1268b6a83bf10d024344e140541",
"47d227f424bf889b0d899b9cc992d5695e1b78c406e183cd78eafefbe5488923",
"b526bd58d0262dd4dda2bff5bc5515b705fb668a46235ace3e057f807963a11a",
"828666b021d837a33e78d870b56d34c88a5e2c85de58b693607ec574f0c27000",
"811fb3b063c739d261fd8590dd30242e117908f5a095d594fa04585daa18ec4d",
],
EditorKind::Emacs => vec![
"51068d4747a13732440d1a8b8f432603badb1864fa431d83d0fd4f8fa57039e0",
"d29af4d949bbe2371eac928a3c31cf9496b1701aa1c45f11cd6c759865ad5c45",
],
}
}
fn make_run(run: RunConfig<'_>) {
if run.builder.config.dry_run() {
return;

fn settings_path(&self, config: &Config) -> PathBuf {
config.src.join(self.settings_short_path())
}

fn settings_short_path(&self) -> PathBuf {
self.settings_folder().join(match self {
EditorKind::Vscode => "settings.json",
EditorKind::Emacs => ".dir-locals.el",
})
}

fn settings_folder(&self) -> PathBuf {
match self {
EditorKind::Vscode => PathBuf::new().join(".vscode"),
EditorKind::Emacs => PathBuf::new(),
}
if let [cmd] = &run.paths[..] {
if cmd.assert_single_path().path.as_path().as_os_str() == "vscode" {
run.builder.ensure(Vscode);
}
}

fn settings_template(&self) -> &str {
match self {
EditorKind::Vscode => include_str!("../../../../etc/rust_analyzer_settings.json"),
EditorKind::Emacs => include_str!("../../../../etc/rust_analyzer_eglot.el"),
}
}
fn run(self, builder: &Builder<'_>) -> Self::Output {
let config = &builder.config;
if config.dry_run() {
return;

fn backup_extension(&self) -> &str {
match self {
EditorKind::Vscode => "json.bak",
EditorKind::Emacs => "el.bak",
}
while !t!(create_vscode_settings_maybe(config)) {}
}
}

/// Create a `.vscode/settings.json` file for rustc development, or just print it
/// Helper macro for implementing the necessary Step to support editor LSP setup
/// The first argument must match the argument set up in `flags.rs`
/// The second argument must match the name of some `EditorKind` variant
/// After using the macro, the editor needs to be registered in `builder.rs` with describe!()
macro_rules! impl_editor_support {
( $($editor:ident, $kind:ident),+ ) => {$(
#[doc = concat!(" Sets up or displays the LSP config for ", stringify!($editor), ".")]
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub struct $kind;

impl Step for $kind {
type Output = ();
const DEFAULT: bool = true;
fn should_run(run: ShouldRun<'_>) -> ShouldRun<'_> {
run.alias(stringify!($editor))
}
fn make_run(run: RunConfig<'_>) {
if run.builder.config.dry_run() {
return;
}
if let [cmd] = &run.paths[..] {
if cmd.assert_single_path().path.as_path().as_os_str() == stringify!($editor) {
run.builder.ensure($kind);
}
}
}
fn run(self, builder: &Builder<'_>) -> Self::Output {
let config = &builder.config;
if config.dry_run() {
return;
}
while !t!(create_editor_settings_maybe(config, EditorKind::$kind)) {}
}
}
)+};
}

impl_editor_support!(vscode, Vscode);
impl_editor_support!(emacs, Emacs);

/// Create the recommended editor LSP config file for rustc development, or just print it
/// If this method should be re-called, it returns `false`.
fn create_vscode_settings_maybe(config: &Config) -> io::Result<bool> {
let (current_hash, historical_hashes) = SETTINGS_HASHES.split_last().unwrap();
let vscode_settings = config.src.join(".vscode").join("settings.json");
// If None, no settings.json exists
fn create_editor_settings_maybe(config: &Config, editor: EditorKind) -> io::Result<bool> {
let hashes = editor.hashes();
let (current_hash, historical_hashes) = hashes.split_last().unwrap();
let settings_path = editor.settings_path(config);
let settings_short_path = editor.settings_short_path();
let settings_filename = settings_short_path.to_str().unwrap();
// If None, no settings file exists
// If Some(true), is a previous version of settings.json
// If Some(false), is not a previous version (i.e. user modified)
// If it's up to date we can just skip this
let mut mismatched_settings = None;
if let Ok(current) = fs::read_to_string(&vscode_settings) {
if let Ok(current) = fs::read_to_string(&settings_path) {
let mut hasher = sha2::Sha256::new();
hasher.update(&current);
let hash = hex_encode(hasher.finalize().as_slice());
Expand All @@ -585,20 +647,23 @@ fn create_vscode_settings_maybe(config: &Config) -> io::Result<bool> {
}
}
println!(
"\nx.py can automatically install the recommended `.vscode/settings.json` file for rustc development"
"\nx.py can automatically install the recommended `{settings_filename}` file for rustc development"
);

match mismatched_settings {
Some(true) => eprintln!(
"WARNING: existing `.vscode/settings.json` is out of date, x.py will update it"
"WARNING: existing `{}` is out of date, x.py will update it",
settings_filename
),
Some(false) => eprintln!(
"WARNING: existing `.vscode/settings.json` has been modified by user, x.py will back it up and replace it"
"WARNING: existing `{}` has been modified by user, x.py will back it up and replace it",
settings_filename
),
_ => (),
}
let should_create = match prompt_user(
"Would you like to create/update settings.json? (Press 'p' to preview values): [y/N]",
)? {
let should_create = match prompt_user(&format!(
"Would you like to create/update `{settings_filename}`? (Press 'p' to preview values): [y/N]"
))? {
Some(PromptResult::Yes) => true,
Some(PromptResult::Print) => false,
_ => {
Expand All @@ -607,28 +672,31 @@ fn create_vscode_settings_maybe(config: &Config) -> io::Result<bool> {
}
};
if should_create {
let path = config.src.join(".vscode");
if !path.exists() {
fs::create_dir(&path)?;
let settings_folder_path = config.src.join(editor.settings_folder());
if !settings_folder_path.exists() {
fs::create_dir(settings_folder_path)?;
}
let verb = match mismatched_settings {
// exists but outdated, we can replace this
Some(true) => "Updated",
// exists but user modified, back it up
Some(false) => {
// exists and is not current version or outdated, so back it up
let mut backup = vscode_settings.clone();
backup.set_extension("json.bak");
eprintln!("WARNING: copying `settings.json` to `settings.json.bak`");
fs::copy(&vscode_settings, &backup)?;
let backup = settings_path.clone().with_extension(editor.backup_extension());
eprintln!(
"WARNING: copying `{}` to `{}`",
settings_path.file_name().unwrap().to_str().unwrap(),
backup.file_name().unwrap().to_str().unwrap(),
);
fs::copy(&settings_path, &backup)?;
"Updated"
}
_ => "Created",
};
fs::write(&vscode_settings, RUST_ANALYZER_SETTINGS)?;
println!("{verb} `.vscode/settings.json`");
fs::write(&settings_path, editor.settings_template())?;
println!("{verb} `{}`", settings_filename);
} else {
println!("\n{RUST_ANALYZER_SETTINGS}");
println!("\n{}", editor.settings_template());
}
Ok(should_create)
}
9 changes: 5 additions & 4 deletions src/bootstrap/src/core/build_steps/setup/tests.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
use sha2::Digest;

use super::{RUST_ANALYZER_SETTINGS, SETTINGS_HASHES};
use super::EditorKind;
use crate::utils::helpers::hex_encode;

#[test]
fn check_matching_settings_hash() {
let editor = EditorKind::Vscode;
let mut hasher = sha2::Sha256::new();
hasher.update(&RUST_ANALYZER_SETTINGS);
hasher.update(&editor.settings_template());
let hash = hex_encode(hasher.finalize().as_slice());
assert_eq!(
&hash,
SETTINGS_HASHES.last().unwrap(),
"Update `SETTINGS_HASHES` with the new hash of `src/etc/rust_analyzer_settings.json`"
editor.hashes().last().unwrap(),
"Update `EditorKind::hashes()` with the new hash of `src/etc/rust_analyzer_settings.json`"
);
}
4 changes: 3 additions & 1 deletion src/bootstrap/src/core/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1000,7 +1000,9 @@ impl<'a> Builder<'a> {
run::GenerateWindowsSys,
run::GenerateCompletions,
),
Kind::Setup => describe!(setup::Profile, setup::Hook, setup::Link, setup::Vscode),
Kind::Setup => {
describe!(setup::Profile, setup::Hook, setup::Link, setup::Vscode, setup::Emacs)
}
Kind::Clean => describe!(clean::CleanAll, clean::Rustc, clean::Std),
Kind::Vendor => describe!(vendor::Vendor),
// special-cased in Build::build()
Expand Down
3 changes: 2 additions & 1 deletion src/bootstrap/src/core/config/flags.rs
Original file line number Diff line number Diff line change
Expand Up @@ -450,11 +450,12 @@ Arguments:
To only set up the git hook, VS Code config or toolchain link, you may use
./x.py setup hook
./x.py setup vscode
./x.py setup emacs
./x.py setup link", Profile::all_for_help(" ").trim_end()))]
Setup {
/// Either the profile for `config.toml` or another setup action.
/// May be omitted to set up interactively
#[arg(value_name = "<PROFILE>|hook|vscode|link")]
#[arg(value_name = "<PROFILE>|hook|vscode|emacs|link")]
profile: Option<PathBuf>,
},
/// Suggest a subset of tests to run, based on modified files
Expand Down
2 changes: 1 addition & 1 deletion src/etc/completions/x.py.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2741,7 +2741,7 @@ _x.py() {
return 0
;;
x.py__setup)
opts="-v -i -j -h --verbose --incremental --config --build-dir --build --host --target --exclude --skip --include-default-paths --rustc-error-format --on-fail --dry-run --dump-bootstrap-shims --stage --keep-stage --keep-stage-std --src --jobs --warnings --error-format --json-output --color --bypass-bootstrap-lock --rust-profile-generate --rust-profile-use --llvm-profile-use --llvm-profile-generate --enable-bolt-settings --skip-stage0-validation --reproducible-artifact --set --help [<PROFILE>|hook|vscode|link] [PATHS]... [ARGS]..."
opts="-v -i -j -h --verbose --incremental --config --build-dir --build --host --target --exclude --skip --include-default-paths --rustc-error-format --on-fail --dry-run --dump-bootstrap-shims --stage --keep-stage --keep-stage-std --src --jobs --warnings --error-format --json-output --color --bypass-bootstrap-lock --rust-profile-generate --rust-profile-use --llvm-profile-use --llvm-profile-generate --enable-bolt-settings --skip-stage0-validation --reproducible-artifact --set --help [<PROFILE>|hook|vscode|emacs|link] [PATHS]... [ARGS]..."
if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
Expand Down
50 changes: 25 additions & 25 deletions src/etc/rust_analyzer_eglot.el
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,28 @@
.((eglot-workspace-configuration
. (:rust-analyzer
( :check ( :invocationLocation "root"
:invocationStrategy "once"
:overrideCommand ["python3"
"x.py"
"check"
"--json-output"])
:linkedProjects ["Cargo.toml"
"src/tools/x/Cargo.toml"
"src/bootstrap/Cargo.toml"
"src/tools/rust-analyzer/Cargo.toml"
"compiler/rustc_codegen_cranelift/Cargo.toml"
"compiler/rustc_codegen_gcc/Cargo.toml"]
:rustfmt ( :overrideCommand ["build/host/rustfmt/bin/rustfmt"
"--edition=2021"])
:procMacro ( :server "build/host/stage0/libexec/rust-analyzer-proc-macro-srv"
:enable t)
:cargo ( :buildScripts ( :enable t
:invocationLocation "root"
:invocationStrategy "once"
:overrideCommand ["python3"
"x.py"
"check"
"--json-output"])
:sysrootSrc "./library"
:extraEnv (:RUSTC_BOOTSTRAP "1"))
:rustc ( :source "./Cargo.toml" )))))))
:invocationStrategy "once"
:overrideCommand ["python3"
"x.py"
"check"
"--json-output"])
:linkedProjects ["Cargo.toml"
"src/tools/x/Cargo.toml"
"src/bootstrap/Cargo.toml"
"src/tools/rust-analyzer/Cargo.toml"
"compiler/rustc_codegen_cranelift/Cargo.toml"
"compiler/rustc_codegen_gcc/Cargo.toml"]
:rustfmt ( :overrideCommand ["build/host/rustfmt/bin/rustfmt"
"--edition=2021"])
:procMacro ( :server "build/host/stage0/libexec/rust-analyzer-proc-macro-srv"
:enable t)
:cargo ( :buildScripts ( :enable t
:invocationLocation "root"
:invocationStrategy "once"
:overrideCommand ["python3"
"x.py"
"check"
"--json-output"])
:sysrootSrc "./library"
:extraEnv (:RUSTC_BOOTSTRAP "1"))
:rustc ( :source "./Cargo.toml" )))))))

0 comments on commit c53d4c7

Please sign in to comment.