11use crate :: builder:: { Builder , RunConfig , ShouldRun , Step } ;
22use crate :: Config ;
33use crate :: { t, VERSION } ;
4+ use sha2:: Digest ;
45use std:: env:: consts:: EXE_SUFFIX ;
56use std:: fmt:: Write as _;
67use std:: fs:: File ;
@@ -10,6 +11,9 @@ use std::process::Command;
1011use std:: str:: FromStr ;
1112use std:: { fmt, fs, io} ;
1213
14+ #[ cfg( test) ]
15+ mod tests;
16+
1317#[ derive( Clone , Copy , Debug , Eq , PartialEq , Hash ) ]
1418pub enum Profile {
1519 Compiler ,
@@ -19,6 +23,13 @@ pub enum Profile {
1923 User ,
2024}
2125
26+ /// A list of historical hashes of `src/etc/vscode_settings.json`.
27+ /// New entries should be appended whenever this is updated so we can detected
28+ /// outdated vs. user-modified settings files.
29+ static SETTINGS_HASHES : & [ & str ] =
30+ & [ "ea67e259dedf60d4429b6c349a564ffcd1563cf41c920a856d1f5b16b4701ac8" ] ;
31+ static VSCODE_SETTINGS : & str = include_str ! ( "../etc/vscode_settings.json" ) ;
32+
2233impl Profile {
2334 fn include_path ( & self , src_path : & Path ) -> PathBuf {
2435 PathBuf :: from ( format ! ( "{}/src/bootstrap/defaults/config.{}.toml" , src_path. display( ) , self ) )
@@ -155,6 +166,7 @@ pub fn setup(config: &Config, profile: Profile) {
155166
156167 if !config. dry_run ( ) {
157168 t ! ( install_git_hook_maybe( & config) ) ;
169+ t ! ( create_vscode_settings_maybe( & config) ) ;
158170 }
159171
160172 println ! ( ) ;
@@ -351,6 +363,34 @@ pub fn interactive_path() -> io::Result<Profile> {
351363 Ok ( template)
352364}
353365
366+ #[ derive( PartialEq ) ]
367+ enum PromptResult {
368+ Yes , // y/Y/yes
369+ No , // n/N/no
370+ Print , // p/P/print
371+ }
372+
373+ /// Prompt a user for a answer, looping until they enter an accepted input or nothing
374+ fn prompt_user ( prompt : & str ) -> io:: Result < Option < PromptResult > > {
375+ let mut input = String :: new ( ) ;
376+ loop {
377+ print ! ( "{prompt} " ) ;
378+ io:: stdout ( ) . flush ( ) ?;
379+ input. clear ( ) ;
380+ io:: stdin ( ) . read_line ( & mut input) ?;
381+ match input. trim ( ) . to_lowercase ( ) . as_str ( ) {
382+ "y" | "yes" => return Ok ( Some ( PromptResult :: Yes ) ) ,
383+ "n" | "no" => return Ok ( Some ( PromptResult :: No ) ) ,
384+ "p" | "print" => return Ok ( Some ( PromptResult :: Print ) ) ,
385+ "" => return Ok ( None ) ,
386+ _ => {
387+ eprintln ! ( "error: unrecognized option '{}'" , input. trim( ) ) ;
388+ eprintln ! ( "note: press Ctrl+C to exit" ) ;
389+ }
390+ } ;
391+ }
392+ }
393+
354394// install a git hook to automatically run tidy, if they want
355395fn install_git_hook_maybe ( config : & Config ) -> io:: Result < ( ) > {
356396 let git = t ! ( config. git( ) . args( & [ "rev-parse" , "--git-common-dir" ] ) . output( ) . map( |output| {
@@ -363,43 +403,98 @@ fn install_git_hook_maybe(config: &Config) -> io::Result<()> {
363403 return Ok ( ( ) ) ;
364404 }
365405
366- let mut input = String :: new ( ) ;
367- println ! ( ) ;
368406 println ! (
369- "Rust 's CI will automatically fail if it doesn't pass `tidy`, the internal tool for ensuring code quality.
407+ "\n Rust 's CI will automatically fail if it doesn't pass `tidy`, the internal tool for ensuring code quality.
370408If you'd like, x.py can install a git hook for you that will automatically run `test tidy` before
371409pushing your code to ensure your code is up to par. If you decide later that this behavior is
372410undesirable, simply delete the `pre-push` file from .git/hooks."
373411 ) ;
374412
375- let should_install = loop {
376- print ! ( "Would you like to install the git hook?: [y/N] " ) ;
377- io:: stdout ( ) . flush ( ) ?;
378- input. clear ( ) ;
379- io:: stdin ( ) . read_line ( & mut input) ?;
380- break match input. trim ( ) . to_lowercase ( ) . as_str ( ) {
381- "y" | "yes" => true ,
382- "n" | "no" | "" => false ,
383- _ => {
384- eprintln ! ( "error: unrecognized option '{}'" , input. trim( ) ) ;
385- eprintln ! ( "note: press Ctrl+C to exit" ) ;
386- continue ;
387- }
388- } ;
389- } ;
390-
391- if should_install {
392- let src = config. src . join ( "src" ) . join ( "etc" ) . join ( "pre-push.sh" ) ;
393- match fs:: hard_link ( src, & dst) {
394- Err ( e) => eprintln ! (
413+ if prompt_user ( "Would you like to install the git hook?: [y/N]" ) ? != Some ( PromptResult :: Yes ) {
414+ println ! ( "Ok, skipping installation!" ) ;
415+ return Ok ( ( ) ) ;
416+ }
417+ let src = config. src . join ( "src" ) . join ( "etc" ) . join ( "pre-push.sh" ) ;
418+ match fs:: hard_link ( src, & dst) {
419+ Err ( e) => {
420+ eprintln ! (
395421 "error: could not create hook {}: do you already have the git hook installed?\n {}" ,
396422 dst. display( ) ,
397423 e
398- ) ,
399- Ok ( _) => println ! ( "Linked `src/etc/pre-push.sh` to `.git/hooks/pre-push`" ) ,
424+ ) ;
425+ return Err ( e) ;
426+ }
427+ Ok ( _) => println ! ( "Linked `src/etc/pre-push.sh` to `.git/hooks/pre-push`" ) ,
428+ } ;
429+ Ok ( ( ) )
430+ }
431+
432+ /// Create a `.vscode/settings.json` file for rustc development, or just print it
433+ fn create_vscode_settings_maybe ( config : & Config ) -> io:: Result < ( ) > {
434+ let ( current_hash, historical_hashes) = SETTINGS_HASHES . split_last ( ) . unwrap ( ) ;
435+ let vscode_settings = config. src . join ( ".vscode" ) . join ( "settings.json" ) ;
436+ // If None, no settings.json exists
437+ // If Some(true), is a previous version of settings.json
438+ // If Some(false), is not a previous version (i.e. user modified)
439+ // If it's up to date we can just skip this
440+ let mut mismatched_settings = None ;
441+ if let Ok ( current) = fs:: read_to_string ( & vscode_settings) {
442+ let mut hasher = sha2:: Sha256 :: new ( ) ;
443+ hasher. update ( & current) ;
444+ let hash = hex:: encode ( hasher. finalize ( ) . as_slice ( ) ) ;
445+ if hash == * current_hash {
446+ return Ok ( ( ) ) ;
447+ } else if historical_hashes. contains ( & hash. as_str ( ) ) {
448+ mismatched_settings = Some ( true ) ;
449+ } else {
450+ mismatched_settings = Some ( false ) ;
451+ }
452+ }
453+ println ! (
454+ "\n x.py can automatically install the recommended `.vscode/settings.json` file for rustc development"
455+ ) ;
456+ match mismatched_settings {
457+ Some ( true ) => eprintln ! (
458+ "warning: existing `.vscode/settings.json` is out of date, x.py will update it"
459+ ) ,
460+ Some ( false ) => eprintln ! (
461+ "warning: existing `.vscode/settings.json` has been modified by user, x.py will back it up and replace it"
462+ ) ,
463+ _ => ( ) ,
464+ }
465+ let should_create = match prompt_user (
466+ "Would you like to create/update `settings.json`, or only print suggested settings?: [y/p/N]" ,
467+ ) ? {
468+ Some ( PromptResult :: Yes ) => true ,
469+ Some ( PromptResult :: Print ) => false ,
470+ _ => {
471+ println ! ( "Ok, skipping settings!" ) ;
472+ return Ok ( ( ) ) ;
473+ }
474+ } ;
475+ if should_create {
476+ let path = config. src . join ( ".vscode" ) ;
477+ if !path. exists ( ) {
478+ fs:: create_dir ( & path) ?;
479+ }
480+ let verb = match mismatched_settings {
481+ // exists but outdated, we can replace this
482+ Some ( true ) => "Updated" ,
483+ // exists but user modified, back it up
484+ Some ( false ) => {
485+ // exists and is not current version or outdated, so back it up
486+ let mut backup = vscode_settings. clone ( ) ;
487+ backup. set_extension ( "bak" ) ;
488+ eprintln ! ( "warning: copying `settings.json` to `settings.json.bak`" ) ;
489+ fs:: copy ( & vscode_settings, & backup) ?;
490+ "Updated"
491+ }
492+ _ => "Created" ,
400493 } ;
494+ fs:: write ( & vscode_settings, & VSCODE_SETTINGS ) ?;
495+ println ! ( "{verb} `.vscode/settings.json`" ) ;
401496 } else {
402- println ! ( "Ok, skipping installation! " ) ;
497+ println ! ( "\n {VSCODE_SETTINGS} " ) ;
403498 }
404499 Ok ( ( ) )
405500}
0 commit comments