diff --git a/index.html b/index.html new file mode 100644 index 000000000..1ecf2cb54 --- /dev/null +++ b/index.html @@ -0,0 +1 @@ +main diff --git a/main/.lock b/main/.lock new file mode 100644 index 000000000..e69de29bb diff --git a/main/archive/all.html b/main/archive/all.html new file mode 100644 index 000000000..4f8659be1 --- /dev/null +++ b/main/archive/all.html @@ -0,0 +1 @@ +
pub enum ArchiveError {
+ HttpError(StatusCode),
+ MissingHeaderError(&'static HeaderName),
+ UnexpectedContentLengthError(u64),
+ IoError(Error),
+ AttohttpcError(Error),
+ ZipError(ZipError),
+}
Error type for this crate
+pub enum Origin {
+ Local,
+ Remote,
+}
Metadata describing whether an archive comes from a local or remote origin.
+pub(crate) fn content_length(headers: &HeaderMap) -> Result<u64, ArchiveError>
Determines the length of an HTTP response’s content in bytes, using
+the HTTP "Content-Length"
header.
pub fn fetch_native(
+ url: &str,
+ cache_file: &Path
+) -> Result<Box<dyn Archive>, ArchiveError>
Fetch a remote archive in the native OS-preferred format from the specified +URL and store its results at the specified file path.
+On Windows, the preferred format is zip. On Unixes, the preferred format +is tarball.
+pub fn load_native(source: File) -> Result<Box<dyn Archive>, ArchiveError>
Load an archive in the native OS-preferred format from the specified file.
+On Windows, the preferred format is zip. On Unixes, the preferred format +is tarball.
+This crate provides types for fetching and unpacking compressed +archives in tarball or zip format.
+"Content-Length"
header.pub struct Tarball {
+ compressed_size: u64,
+ data: Box<dyn Read>,
+ origin: Origin,
+}
A Node installation tarball.
+compressed_size: u64
§data: Box<dyn Read>
§origin: Origin
pub struct Zip {
+ compressed_size: u64,
+ data: Box<dyn Read>,
+ origin: Origin,
+}
compressed_size: u64
§data: Box<dyn Read>
§origin: Origin
pub struct Tarball {
+ compressed_size: u64,
+ data: Box<dyn Read>,
+ origin: Origin,
+}
A Node installation tarball.
+compressed_size: u64
§data: Box<dyn Read>
§origin: Origin
pub trait Archive {
+ // Required methods
+ fn compressed_size(&self) -> u64;
+ fn unpack(
+ self: Box<Self>,
+ dest: &Path,
+ progress: &mut dyn FnMut(&(), usize)
+ ) -> Result<(), ArchiveError>;
+ fn origin(&self) -> Origin;
+}
Unpacks the zip archive to the specified destination folder.
+pub struct Zip {
+ compressed_size: u64,
+ data: Box<dyn Read>,
+ origin: Origin,
+}
compressed_size: u64
§data: Box<dyn Read>
§origin: Origin
This crate provides utilities for operating on the filesystem.
+This crate provides an adapter for the std::io::Read
trait to
+allow reporting incremental progress to a callback function.
pub struct ProgressRead<R: Read, T, F: FnMut(&T, usize) -> T> {
+ pub(crate) source: R,
+ pub(crate) accumulator: T,
+ pub(crate) progress: F,
+}
A reader that reports incremental progress while reading.
+source: R
§accumulator: T
§progress: F
Read some bytes from the underlying reader into the specified buffer, +and report progress to the progress callback. The progress callback is +passed the current value of the accumulator as its first argument and +the number of bytes read as its second argument. The result of the +progress callback is stored as the updated value of the accumulator, +to be passed to the next invocation of the callback.
+read
, except that it reads into a slice of buffers. Read morecan_vector
)buf
. Read morebuf
. Read morebuf
. Read moreread_buf
)read_buf
)cursor
. Read moreRead
. Read moreU::from(self)
.","Calls U::from(self)
.","Load an archive in the native OS-preferred format from the …","","","","","Provides types and functions for fetching and unpacking a …","","","","","","","","","Unpacks the zip archive to the specified destination …","Provides types and functions for fetching and unpacking a …","A Node installation tarball.","","","","","","Initiate fetching of a tarball from the given URL, …","Returns the argument unchanged.","Calls U::from(self)
.","Loads a tarball from the specified file.","","","","","","","","","","","","","Initiate fetching of a Node zip archive from the given …","Returns the argument unchanged.","Calls U::from(self)
.","Loads a cached Node zip archive from the specified file.","","","","","",""],"i":[0,0,5,5,5,1,5,0,1,0,5,0,5,5,1,5,1,1,1,9,22,23,0,22,23,0,5,5,5,5,5,5,1,5,1,0,9,22,23,5,0,1,5,5,1,5,1,5,1,9,0,0,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,0,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23],"f":[0,0,0,0,0,0,0,0,0,0,0,0,0,[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[1,1],[[-1,-2],2,[],[]],[-1,3,[]],0,0,[4,[[6,[3,5]]]],0,0,[[7,8],[[6,[[10,[9]],5]]]],[[5,11],12],[[5,11],12],[13,5],[-1,-1,[]],[14,5],[15,5],[-1,-1,[]],[-1,-2,[],[]],[-1,-2,[],[]],[16,[[6,[[10,[9]],5]]]],[-1,1,[]],0,0,[5,[[18,[17]]]],0,[-1,-2,[],[]],[-1,19,[]],[-1,[[6,[-2]]],[],[]],[-1,[[6,[-2]]],[],[]],[-1,[[6,[-2]]],[],[]],[-1,[[6,[-2]]],[],[]],[-1,20,[]],[-1,20,[]],[[[10,[-1]],8,21],[[6,[2,5]]],[]],0,0,[-1,-2,[],[]],[-1,-2,[],[]],[22,3],0,0,[[7,8],[[6,[[10,[9]],5]]]],[-1,-1,[]],[-1,-2,[],[]],[16,[[6,[[10,[9]],5]]]],[22,1],0,[-1,[[6,[-2]]],[],[]],[-1,[[6,[-2]]],[],[]],[-1,20,[]],[[[10,[22]],8,21],[[6,[2,5]]]],0,[-1,-2,[],[]],[-1,-2,[],[]],[23,3],0,0,[[7,8],[[6,[[10,[9]],5]]]],[-1,-1,[]],[-1,-2,[],[]],[16,[[6,[[10,[9]],5]]]],[23,1],0,[-1,[[6,[-2]]],[],[]],[-1,[[6,[-2]]],[],[]],[-1,20,[]],[[[10,[23]],8,21],[[6,[2,5]]]]],"c":[],"p":[[4,"Origin",0],[15,"tuple"],[15,"u64"],[3,"HeaderMap",83],[4,"ArchiveError",0],[4,"Result",84],[15,"str"],[3,"Path",85],[8,"Archive",0],[3,"Box",86],[3,"Formatter",87],[6,"Result",87],[3,"Error",88],[3,"Error",89],[4,"ZipError",90],[3,"File",91],[8,"Error",92],[4,"Option",93],[3,"String",94],[3,"TypeId",95],[8,"FnMut",96],[3,"Tarball",51],[3,"Zip",67]],"b":[[26,"impl-Display-for-ArchiveError"],[27,"impl-Debug-for-ArchiveError"],[28,"impl-From%3CError%3E-for-ArchiveError"],[30,"impl-From%3CError%3E-for-ArchiveError"],[31,"impl-From%3CZipError%3E-for-ArchiveError"]]},\
+"fs_utils":{"doc":"This crate provides utilities for operating on the …","t":"F","n":["ensure_containing_dir_exists"],"q":[[0,"fs_utils"],[1,"std::io::error"],[2,"std::path"],[3,"core::convert"]],"d":["This creates the parent directory of the input path, …"],"i":[0],"f":[[-1,[[2,[1]]],[[4,[3]]]]],"c":[],"p":[[15,"tuple"],[6,"Result",1],[3,"Path",2],[8,"AsRef",3]],"b":[]},\
+"progress_read":{"doc":"This crate provides an adapter for the std::io::Read
trait …","t":"DMLLLLLMLLMLLL","n":["ProgressRead","accumulator","borrow","borrow_mut","from","into","new","progress","read","seek","source","try_from","try_into","type_id"],"q":[[0,"progress_read"],[14,"std::io"],[15,"core::ops::function"],[16,"std::io::error"],[17,"std::io"],[18,"core::any"]],"d":["A reader that reports incremental progress while reading.","","","","Returns the argument unchanged.","Calls U::from(self)
.","Construct a new progress reader with the specified …","","Read some bytes from the underlying reader into the …","","","","",""],"i":[0,1,1,1,1,1,1,1,1,1,1,1,1,1],"f":[0,0,[-1,-2,[],[]],[-1,-2,[],[]],[-1,-1,[]],[-1,-2,[],[]],[[-1,-2,-3],[[1,[-1,-2,-3]]],2,[],3],0,[[[1,[-1,-2,-3]],[5,[4]]],[[7,[6]]],2,[],3],[[[1,[-1,-2,-3]],8],[[7,[9]]],[2,10],[],3],0,[-1,[[11,[-2]]],[],[]],[-1,[[11,[-2]]],[],[]],[-1,12,[]]],"c":[],"p":[[3,"ProgressRead",0],[8,"Read",14],[8,"FnMut",15],[15,"u8"],[15,"slice"],[15,"usize"],[6,"Result",16],[4,"SeekFrom",14],[15,"u64"],[8,"Seek",14],[4,"Result",17],[3,"TypeId",18]],"b":[]},\
+"test_support":{"doc":"Utilities to use with acceptance tests in Volta.","t":"AOAANDENNNNDLLLLLLLLLLLLLLLLFMMMMMMMMMMMMFMLLLLLLLLLLFLLLLLLLLLLMFLLLLLLLLLLLLLLLLLLLLLLLFNNHIEHRLLLKLFFFLKKKKFLLLLDDLLMLLLLLLLLLMMLMLLLMLLLLLLLLLLLLLMFFLMLLLLLLLLL","n":["matchers","ok_or_panic","paths","process","Exact","Execs","MatchKind","NotPresent","Partial","PartialN","Unordered","ZipAll","_with_stderr","borrow","borrow","borrow","borrow_mut","borrow_mut","borrow_mut","clone","clone","clone_into","clone_into","diff_lines","eq","equivalent","equivalent","equivalent","execs","expect_either_contains","expect_exit_code","expect_json","expect_neither_contains","expect_stderr","expect_stderr_contains","expect_stderr_not_contains","expect_stderr_unordered","expect_stdout","expect_stdout_contains","expect_stdout_contains_n","expect_stdout_not_contains","find_mismatch","first","fmt","fmt","fmt","from","from","from","into","into","into","into_iter","lines_match","match_json","match_output","match_status","match_std","match_stderr","match_stdout","matches","matches","matches","next","second","substitute_macros","to_owned","to_owned","to_string","try_from","try_from","try_from","try_into","try_into","try_into","type_id","type_id","type_id","with_either_contains","with_json","with_status","with_stderr","with_stderr_contains","with_stderr_does_not_contain","with_stderr_unordered","with_stdout","with_stdout_contains","with_stdout_contains_n","with_stdout_does_not_contain","zip_all","Dir","File","NEXT_ID","PathExt","Remove","SMOKE_TEST_DIR","TASK_ID","at","borrow","borrow_mut","ensure_empty","from","global_root","home","init","into","mkdir_p","rm","rm_contents","rm_rf","root","to_str","try_from","try_into","type_id","ProcessBuilder","ProcessError","arg","args","args","args_replace","borrow","borrow","borrow_mut","borrow_mut","build_command","clone","clone_into","cwd","cwd","desc","env","env","env_remove","exec","exec_with_output","exit","fmt","fmt","fmt","fmt","from","from","get_args","get_cwd","get_env","get_envs","get_program","into","into","output","process","process_error","program","program","to_owned","to_string","to_string","try_from","try_from","try_into","try_into","type_id","type_id"],"q":[[0,"test_support"],[4,"test_support::matchers"],[90,"test_support::paths"],[115,"test_support::process"],[164,"alloc::string"],[165,"core::str::iter"],[166,"alloc::string"],[167,"serde_json::value"],[168,"core::option"],[169,"core::fmt"],[170,"core::fmt"],[171,"std::process"],[172,"core::iter::traits::iterator"],[173,"core::result"],[174,"core::any"],[175,"std::path"],[176,"std::path"],[177,"core::convert"],[178,"std::process"],[179,"std::process"]],"d":["","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","Returns the argument unchanged.","Returns the argument unchanged.","Returns the argument unchanged.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","","Compare a line with an expected pattern.","","","","","","","","","","","","","","","","","","","","","","","","","Verify that either stdout or stderr contains the given …","Verify the JSON output matches the given JSON. Typically …","Verify the exit code from the process.","Verify that stderr is equal to the given lines. See …","Verify that stderr contains the given contiguous lines …","Verify that stderr does not contain the given contiguous …","Verify that all of the stderr output is equal to the given …","Verify that stdout is equal to the given lines. See …","Verify that stdout contains the given contiguous lines …","Verify that stdout contains the given contiguous lines …","Verify that stdout does not contain the given contiguous …","","","","","","","","","","","","","Returns the argument unchanged.","","","","Calls U::from(self)
.","","","","","","","","","","A builder object for an external process, similar to …","","(chainable) Add an arg to the args list.","(chainable) Add many args to the args list.","A list of arguments to pass to the program.","(chainable) Replace args with new args list","","","","","Converts ProcessBuilder into a std::process::Command
","","","(chainable) Set the current working directory of the …","Which directory to run the program from.","","(chainable) Set an environment variable for the process.","Any environment variables that should be set for the …","(chainable) Unset an environment variable for the process.","Run the process, waiting for completion, and mapping …","Execute the process, returning the stdio output, or an …","","","","","","Returns the argument unchanged.","Returns the argument unchanged.","Get the program arguments","Get the current working directory for the process","Get an environment variable as the process will see it …","Get all environment variables explicitly set or unset for …","Get the executable name.","Calls U::from(self)
.","Calls U::from(self)
.","","A helper function to create a ProcessBuilder
.","","(chainable) Set the executable for the process.","The program to execute.","","","","","","","","",""],"i":[0,0,0,0,4,0,0,4,4,4,4,0,1,19,1,4,19,1,4,1,4,1,4,1,4,4,4,4,0,1,1,1,1,1,1,1,1,1,1,1,1,0,19,1,1,4,19,1,4,19,1,4,19,0,1,1,1,1,1,1,1,1,1,19,19,0,1,4,1,19,1,4,19,1,4,19,1,4,1,1,1,1,1,1,1,1,1,1,1,0,25,25,0,0,0,0,0,25,25,25,35,25,0,0,0,25,35,35,35,35,0,25,25,25,25,0,0,18,18,18,18,18,31,18,31,18,18,18,18,18,31,18,18,18,18,18,31,18,18,31,31,18,31,18,18,18,18,18,18,31,31,0,0,18,18,18,18,31,18,31,18,31,18,31],"f":[0,0,0,0,0,0,0,0,0,0,0,0,[[1,2],3],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[1,1],[4,4],[[-1,-2],3,[],[]],[[-1,-2],3,[],[]],[[1,5,5,6],[[8,[7]]]],[[4,4],6],[[-1,-2],6,[],[]],[[-1,-2],6,[],[]],[[-1,-2],6,[],[]],[[],1],0,0,0,0,0,0,0,0,0,0,0,0,[[9,9],[[10,[[3,[9,9]]]]]],0,[[1,11],12],[[1,11],12],[[4,11],12],[-1,-1,[]],[-1,-1,[]],[-1,-1,[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[[13,13],6],[[1,9,13],14],[[1,15],14],[[1,15],14],[[1,[10,[7]],[17,[16]],13,[17,[16]],4],14],[[1,15],14],[[1,15],14],[[1,18],14],[[1,18],14],[[1,15],14],[[[19,[-1,-2]]],[[10,[[3,[[10,[-3]],[10,[-3]]]]]]],20,20,[]],0,[13,7],[-1,-2,[],[]],[-1,-2,[],[]],[-1,7,[]],[-1,[[21,[-2]]],[],[]],[-1,[[21,[-2]]],[],[]],[-1,[[21,[-2]]],[],[]],[-1,[[21,[-2]]],[],[]],[-1,[[21,[-2]]],[],[]],[-1,[[21,[-2]]],[],[]],[-1,22,[]],[-1,22,[]],[-1,22,[]],[[1,-1],1,2],[[1,13],1],[[1,23],1],[[1,-1],1,2],[[1,-1],1,2],[[1,-1],1,2],[[1,-1],1,2],[[1,-1],1,2],[[1,-1],1,2],[[1,-1,24],1,2],[[1,-1],1,2],[[-1,-2],[[19,[-1,-2]]],20,20],0,0,0,0,0,0,0,[[25,26],3],[-1,-2,[],[]],[-1,-2,[],[]],[-1,3,[]],[-1,-1,[]],[[],27],[[],27],[[],3],[-1,-2,[],[]],[-1,3,[]],[-1,3,[]],[-1,3,[]],[-1,3,[]],[[],27],[25,13],[-1,[[21,[-2]]],[],[]],[-1,[[21,[-2]]],[],[]],[-1,22,[]],0,0,[[18,-1],18,[[29,[28]]]],[[18,[17,[-1]]],18,[[29,[28]]]],0,[[18,[17,[-1]]],18,[[29,[28]]]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[18,30],[18,18],[[-1,-2],3,[],[]],[[18,-1],18,[[29,[28]]]],0,0,[[18,13,-1],18,[[29,[28]]]],0,[[18,13],18],[18,[[21,[3,31]]]],[18,[[21,[15,31]]]],0,[[18,11],12],[[18,11],12],[[31,11],12],[[31,11],12],[-1,-1,[]],[-1,-1,[]],[18,[[17,[32]]]],[18,[[10,[26]]]],[[18,13],[[10,[32]]]],[18,[[33,[7,[10,[32]]]]]],[18,32],[-1,-2,[],[]],[-1,-2,[],[]],0,[-1,18,[[29,[28]]]],[[13,[10,[34]],[10,[15]]],31],[[18,-1],18,[[29,[28]]]],0,[-1,-2,[],[]],[-1,7,[]],[-1,7,[]],[-1,[[21,[-2]]],[],[]],[-1,[[21,[-2]]],[],[]],[-1,[[21,[-2]]],[],[]],[-1,[[21,[-2]]],[],[]],[-1,22,[]],[-1,22,[]]],"c":[],"p":[[3,"Execs",4],[8,"ToString",164],[15,"tuple"],[4,"MatchKind",4],[3,"Lines",165],[15,"bool"],[3,"String",164],[3,"Vec",166],[4,"Value",167],[4,"Option",168],[3,"Formatter",169],[6,"Result",169],[15,"str"],[6,"MatchResult",170],[3,"Output",171],[15,"u8"],[15,"slice"],[3,"ProcessBuilder",115],[3,"ZipAll",4],[8,"Iterator",172],[4,"Result",173],[3,"TypeId",174],[15,"i32"],[15,"usize"],[4,"Remove",90],[3,"Path",175],[3,"PathBuf",175],[3,"OsStr",176],[8,"AsRef",177],[3,"Command",171],[3,"ProcessError",115],[3,"OsString",176],[3,"HashMap",178],[3,"ExitStatus",171],[8,"PathExt",90]],"b":[[43,"impl-Debug-for-Execs"],[44,"impl-Display-for-Execs"],[60,"impl-HamcrestMatcher%3C%26mut+ProcessBuilder%3E-for-Execs"],[61,"impl-HamcrestMatcher%3CProcessBuilder%3E-for-Execs"],[62,"impl-HamcrestMatcher%3COutput%3E-for-Execs"],[137,"impl-Debug-for-ProcessBuilder"],[138,"impl-Display-for-ProcessBuilder"],[139,"impl-Display-for-ProcessError"],[140,"impl-Debug-for-ProcessError"]]},\
+"validate_npm_package_name":{"doc":"A Rust implementation of the validation rules from the …","t":"RRHNHHNNELLFLLLLLLLLLFMMM","n":["BLACKLIST","BUILTINS","ENCODE_URI_SET","Invalid","SCOPED_PACKAGE","SPECIAL_CHARS","Valid","ValidForOldPackages","Validity","borrow","borrow_mut","done","eq","fmt","from","into","try_from","try_into","type_id","valid_for_new_packages","valid_for_old_packages","validate","errors","warnings","warnings"],"q":[[0,"validate_npm_package_name"],[22,"validate_npm_package_name::Validity"],[25,"alloc::string"],[26,"alloc::vec"],[27,"core::fmt"],[28,"core::fmt"],[29,"core::any"]],"d":["","","The set of characters to encode, matching the characters …","Not valid for new or old packages","","","Valid for new and old packages","Valid only for old packages","","","","","","","Returns the argument unchanged.","Calls U::from(self)
.","","","","","","","","",""],"i":[0,0,0,3,0,0,3,3,0,3,3,0,3,3,3,3,3,3,3,3,3,0,10,11,10],"f":[0,0,0,0,0,0,0,0,0,[-1,-2,[],[]],[-1,-2,[],[]],[[[2,[1]],[2,[1]]],3],[[3,3],4],[[3,5],6],[-1,-1,[]],[-1,-2,[],[]],[-1,[[7,[-2]]],[],[]],[-1,[[7,[-2]]],[],[]],[-1,8,[]],[3,4],[3,4],[9,3],0,0,0],"c":[],"p":[[3,"String",25],[3,"Vec",26],[4,"Validity",0],[15,"bool"],[3,"Formatter",27],[6,"Result",27],[4,"Result",28],[3,"TypeId",29],[15,"str"],[13,"Invalid",22],[13,"ValidForOldPackages",22]],"b":[]},\
+"volta":{"doc":"","t":"AAAFNNNNNNNENNDNLLLLLLLLLMLLLLLLLLLLLMLLFLLLLLLLLLLMMMLLIAAAAAAKAAAADLLLLLMLLLLLMLMLLLLLLDLLLLLLLLLLMLLLLLLDLLLLLLLLLLMLLLLLLNNNNNNEENDDNNNNNEDDENNNNNNEENNLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLMMLLLLLLLLLMLLLLLLLLLLLLLLLALLLLLLLLLLMMLLLALLMMMLLLLLLLLLALLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLMMMLLLLLLLLLLMMMMMMMHHHFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFNNENNNNNNNENLLLLLLLLLLLLLLLFLLLLLLLLLLLMMMMMMMMMMDLLLLLLLLLLMLLLLLLDLLLLMMMLLLLLLMMMMLLMLLLLLLLMDLLLLLLLLLALLLLLLLFFFFFFFFFDLLLLLLLLLLMLLLLLLRRDMLLLLLLLLLLLLLLLLDLLMLLLLLLLLLLLLLLEINNLLFLLKLLLL","n":["cli","command","common","main","Completions","Fetch","Install","List","Pin","Run","Setup","Subcommand","Uninstall","Use","Volta","Which","augment_args","augment_args_for_update","augment_subcommands","augment_subcommands_for_update","borrow","borrow","borrow_mut","borrow_mut","command","command","command_for_update","from","from","from_arg_matches","from_arg_matches","from_arg_matches_mut","from_arg_matches_mut","group_id","has_subcommand","into","into","quiet","run","run","styles","try_from","try_from","try_into","try_into","type_id","type_id","update_from_arg_matches","update_from_arg_matches","update_from_arg_matches_mut","update_from_arg_matches_mut","verbose","version","very_verbose","vzip","vzip","Command","completions","fetch","install","list","pin","run","run","setup","uninstall","use","which","Completions","augment_args","augment_args_for_update","borrow","borrow_mut","fmt","force","from","from_arg_matches","from_arg_matches_mut","group_id","into","out_file","run","shell","try_from","try_into","type_id","update_from_arg_matches","update_from_arg_matches_mut","vzip","Fetch","augment_args","augment_args_for_update","borrow","borrow_mut","from","from_arg_matches","from_arg_matches_mut","group_id","into","run","tools","try_from","try_into","type_id","update_from_arg_matches","update_from_arg_matches_mut","vzip","Install","augment_args","augment_args_for_update","borrow","borrow_mut","from","from_arg_matches","from_arg_matches_mut","group_id","into","run","tools","try_from","try_into","type_id","update_from_arg_matches","update_from_arg_matches_mut","vzip","All","Current","Default","Default","Default","Fetched","Filter","Format","Human","List","Node","Node","None","None","Npm","Npm","Package","PackageDetails","PackageManager","PackageManagerKind","PackageOrTool","Plain","Pnpm","Pnpm","Project","Project","Source","Subcommand","Yarn","Yarn","allowed_with","augment_args","augment_args_for_update","borrow","borrow","borrow","borrow","borrow","borrow","borrow","borrow","borrow","borrow","borrow_mut","borrow_mut","borrow_mut","borrow_mut","borrow_mut","borrow_mut","borrow_mut","borrow_mut","borrow_mut","borrow_mut","clone","clone","clone","clone","clone","clone","clone_into","clone_into","clone_into","clone_into","clone_into","clone_into","cmp","compare","current","default","eq","eq","equivalent","equivalent","equivalent","equivalent","fmt","fmt","fmt","format","from","from","from","from","from","from","from","from","from","from","from_arg_matches","from_arg_matches_mut","from_inventory_and_project","from_str","group_id","human","into","into","into","into","into","into","into","into","into","into","kind","name","new","output_format","partial_cmp","plain","run","source","source","source","subcommand","to_owned","to_owned","to_owned","to_owned","to_owned","to_owned","to_possible_value","to_string","to_string","toolchain","try_from","try_from","try_from","try_from","try_from","try_from","try_from","try_from","try_from","try_from","try_into","try_into","try_into","try_into","try_into","try_into","try_into","try_into","try_into","try_into","type_id","type_id","type_id","type_id","type_id","type_id","type_id","type_id","type_id","type_id","update_from_arg_matches","update_from_arg_matches_mut","value_variants","version","version","version","vzip","vzip","vzip","vzip","vzip","vzip","vzip","vzip","vzip","vzip","details","name","node","path","tools","tools","name","INDENTATION","NO_RUNTIME","TEXT_WIDTH","display_active","display_all","display_node","display_npms","display_package_managers","display_packages","display_tool","format","format_package","format_package_list","format_package_manager","format_package_manager_kind","format_package_manager_list_condensed","format_package_manager_list_verbose","format_runtime","format_runtime_list","format_tool","format_tool_list","list_package_source","wrap","describe_package_managers","describe_packages","describe_runtimes","describe_tool_set","display_node","display_package","display_package_manager","display_tool","format","package_source","Active","All","Lookup","Node","Npm","PackageManagers","Packages","Pnpm","Runtime","Tool","Toolchain","Yarn","active","active_tool","all","borrow","borrow","borrow_mut","borrow_mut","from","from","into","into","node","npm","package_or_tool","pnpm","tool_source","try_from","try_from","try_into","try_into","type_id","type_id","version_from_spec","version_source","vzip","vzip","yarn","host_packages","kind","managers","name","package_managers","package_managers","packages","packages","runtime","runtimes","Pin","augment_args","augment_args_for_update","borrow","borrow_mut","from","from_arg_matches","from_arg_matches_mut","group_id","into","run","tools","try_from","try_into","type_id","update_from_arg_matches","update_from_arg_matches_mut","vzip","Run","augment_args","augment_args_for_update","borrow","borrow_mut","bundled_npm","command_and_args","envs","fmt","from","from_arg_matches","from_arg_matches_mut","group_id","into","no_pnpm","no_yarn","node","npm","parse_envs","parse_platform","pnpm","run","try_from","try_into","type_id","update_from_arg_matches","update_from_arg_matches_mut","vzip","yarn","Setup","augment_args","augment_args_for_update","borrow","borrow_mut","from","from_arg_matches","from_arg_matches_mut","group_id","into","os","run","try_from","try_into","type_id","update_from_arg_matches","update_from_arg_matches_mut","vzip","add_bash_profiles","add_fish_profile","add_zsh_profile","determine_profiles","format_home","read_profile_without_volta","setup_environment","write_profile_fish","write_profile_sh","Uninstall","augment_args","augment_args_for_update","borrow","borrow_mut","from","from_arg_matches","from_arg_matches_mut","group_id","into","run","tool","try_from","try_into","type_id","update_from_arg_matches","update_from_arg_matches_mut","vzip","ADVICE","USAGE","Use","anything","augment_args","augment_args_for_update","borrow","borrow_mut","from","from_arg_matches","from_arg_matches_mut","group_id","into","run","try_from","try_into","type_id","update_from_arg_matches","update_from_arg_matches_mut","vzip","Which","augment_args","augment_args_for_update","binary","borrow","borrow_mut","from","from_arg_matches","from_arg_matches_mut","group_id","into","run","try_from","try_into","type_id","update_from_arg_matches","update_from_arg_matches_mut","vzip","Error","IntoResult","Tool","Volta","borrow","borrow_mut","ensure_layout","from","into","into_result","try_from","try_into","type_id","vzip"],"q":[[0,"volta"],[4,"volta::cli"],[56,"volta::command"],[68,"volta::command::completions"],[89,"volta::command::fetch"],[107,"volta::command::install"],[125,"volta::command::list"],[297,"volta::command::list::Package"],[303,"volta::command::list::Subcommand"],[304,"volta::command::list::human"],[327,"volta::command::list::plain"],[337,"volta::command::list::toolchain"],[376,"volta::command::list::toolchain::Toolchain"],[386,"volta::command::pin"],[404,"volta::command::run"],[433,"volta::command::setup"],[451,"volta::command::setup::os"],[460,"volta::command::uninstall"],[478,"volta::command::use"],[498,"volta::command::which"],[516,"volta::common"],[530,"clap_builder::builder::command"],[531,"clap_builder::parser::matches::arg_matches"],[532,"clap_builder"],[533,"core::result"],[534,"clap_builder::util::id"],[535,"core::option"],[536,"volta_core::session"],[537,"volta_core::error"],[538,"volta_core::error"],[539,"core::any"],[540,"core::fmt"],[541,"core::fmt"],[542,"volta_core::project"],[543,"alloc::vec"],[544,"volta_core::tool::package::metadata"],[545,"clap_builder::builder::possible_value"],[546,"alloc::string"],[547,"alloc::boxed"],[548,"core::convert"],[549,"node_semver"],[550,"volta_core::platform"],[551,"core::ops::function"],[552,"std::collections::hash::map"],[553,"volta_core::platform"],[554,"std::path"]],"d":["","","","The entry point for the volta
CLI.","Generates Volta completions","Fetches a tool to the local machine","Installs a tool in your toolchain","Displays the current toolchain","Pins your project’s runtime or package manager","Run a command with custom Node, npm, pnpm, and/or Yarn …","Enables Volta for the current user / shell","","Uninstalls a tool from your toolchain","","","Locates the actual binary that will be called by Volta","","","","","","","","","","","","Returns the argument unchanged.","Returns the argument unchanged.","","","","","","","Calls U::from(self)
.","Calls U::from(self)
.","Prevents unnecessary output","","","","","","","","","","","","","","Enables verbose diagnostics","Prints the current version of Volta","Enables trace-level diagnostics.","","","A Volta command.","","","","","","","Executes the command. Returns Ok(true)
if the process …","","","","","","","","","","","Write over an existing file, if any.","Returns the argument unchanged.","","","","Calls U::from(self)
.","File to write generated completions to","","Shell to generate completions for","","","","","","","","","","","","Returns the argument unchanged.","","","","Calls U::from(self)
.","","Tools to fetch, like node
, yarn@latest
or …","","","","","","","","","","","","Returns the argument unchanged.","","","","Calls U::from(self)
.","","Tools to install, like node
, yarn@latest
or …","","","","","","","Show every item in the toolchain.","Display only the currently active tool(s).","","Show only the user’s default tool(s).","The item is the user’s default.","","How (if at all) should the list query be narrowed?","","","","","Show locally cached Node versions.","Do not filter at all. Show all tool(s) matching the query.","The item is one that has been fetched but is not installed …","","Show locally cached npm versions.","","A package and its associated tools, for displaying to the …","","","Show locally cached versions of a package or a package …","","","Show locally cached pnpm versions.","","The item is from a project. The wrapped PathBuf
is the …","The source of a given item, from the perspective of a user.","Which tool should we look up?","","Show locally cached Yarn versions.","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","Show the currently-active tool(s).","Show your default tool(s).","","","","","","","","","","Specify the output format.","Returns the argument unchanged.","Returns the argument unchanged.","Returns the argument unchanged.","Returns the argument unchanged.","Returns the argument unchanged.","Returns the argument unchanged.","Returns the argument unchanged.","Returns the argument unchanged.","Returns the argument unchanged.","Returns the argument unchanged.","","","","","","Define the “human” format style for list commands.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","","The name of the package.","","","","Define the “plain” format style for list commands.","","","","","The tool to lookup - all
, node
, npm
, yarn
, pnpm
, or the …","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","The package’s own version.","","","","","","","","","","","","","","","The version of Node the package is installed against.","","The names of the tools associated with the package.","The names of the tools associated with the package.","","","","","Format the output for Toolchain::Active
.","Format the output for Toolchain::All
.","Format a set of Toolchain::Node
s.","Format a set of Toolchain::PackageManager
s for …","Format a set of Toolchain::PackageManager
s.","Format a set of Toolchain::Package
s and their associated …","Format a single Toolchain::Tool
with associated …","","Format a single Toolchain::Package
and its associated …","format a list of Toolchain::Package
s and their associated …","format a single Toolchain::PackageManager
.","format the title for a kind of package manager","format a list of Toolchain::PackageManager
s in condensed …","format a list of Toolchain::PackageManager
s in verbose form","format a single version of Toolchain::Node
.","format a list of Toolchain::Node
s.","Format a single Toolchain::Package
without detail …","Format a list of Toolchain::Package
s without detail …","List a the source from a Toolchain::Package
.","Wrap and indent the output","","","","","","","","","","","","","Lightweight rule for which item to get the Source
for.","","Look up the npm package manager","","","Look up the pnpm package manager","Look up the Node runtime","","","Look up the Yarn package manager","","Determine the Source
for a given kind of tool (Lookup
).","","","","","","Returns the argument unchanged.","Returns the argument unchanged.","Calls U::from(self)
.","Calls U::from(self)
.","","","","","Look up the Source
for a tool with a given name.","","","","","","","","","","","","","","","","","","","","","","","","","","","Returns the argument unchanged.","","","","Calls U::from(self)
.","","Tools to pin, like node@lts
or yarn@^1.14
.","","","","","","","","","","","","Forces npm to be the version bundled with Node","The command to run, along with any arguments","Set an environment variable (can be used multiple times)","","Returns the argument unchanged.","","","","Calls U::from(self)
.","Disables pnpm","Disables Yarn","Set the custom Node version","Set the custom npm version","Convert the environment variable settings passed to the …","Builds a CliPlatform from the provided cli options","Set the custon pnpm version","","","","","","","","Set the custom Yarn version","","","","","","Returns the argument unchanged.","","","","Calls U::from(self)
.","","","","","","","","","Add bash profile scripts, if necessary","Add fish profile scripts, if necessary","Add zsh profile script, if necessary","Returns a list of profile files to modify / create.","","","","","","","","","","","Returns the argument unchanged.","","","","Calls U::from(self)
.","","The tool to uninstall, like ember-cli-update
, typescript
, …","","","","","","","","","","","","","","","Returns the argument unchanged.","","","","Calls U::from(self)
.","","","","","","","","","","","The binary to find, e.g. node
or npm
","","","Returns the argument unchanged.","","","","Calls U::from(self)
.","","","","","","","","","","","","","","","Returns the argument unchanged.","Calls U::from(self)
.","","","","",""],"i":[0,0,0,0,7,7,7,7,7,7,7,0,7,7,0,7,4,4,7,7,4,7,4,7,4,4,4,4,7,4,7,4,7,4,7,4,7,4,4,7,0,4,7,4,7,4,7,4,7,4,7,4,4,4,4,7,0,0,0,0,0,0,0,57,0,0,0,0,0,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,0,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,0,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,28,23,32,23,22,32,0,0,24,0,0,28,23,22,26,28,0,0,0,0,28,24,26,28,32,22,0,0,26,28,22,30,30,58,32,23,24,22,25,26,27,30,28,58,32,23,24,22,25,26,27,30,28,24,22,25,26,27,28,24,22,25,26,27,28,26,26,30,30,22,26,26,26,26,26,22,22,26,30,58,32,23,24,22,25,26,27,30,28,30,30,32,28,30,0,58,32,23,24,22,25,26,27,30,28,27,58,32,30,26,0,30,32,25,27,30,24,22,25,26,27,28,24,22,26,0,58,32,23,24,22,25,26,27,30,28,58,32,23,24,22,25,26,27,30,28,58,32,23,24,22,25,26,27,30,28,30,30,24,58,25,27,58,32,23,24,22,25,26,27,30,28,59,60,59,60,59,60,61,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,39,39,0,39,43,39,39,43,43,39,0,43,39,43,39,39,43,39,43,39,43,39,43,39,39,39,39,0,39,43,39,43,39,43,43,43,39,43,39,62,63,63,62,64,65,64,65,64,65,0,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,0,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,0,49,49,49,49,49,49,49,49,49,0,49,49,49,49,49,49,49,0,0,0,0,0,0,0,0,0,0,53,53,53,53,53,53,53,53,53,53,53,53,53,53,53,53,53,0,0,0,54,54,54,54,54,54,54,54,54,54,54,54,54,54,54,54,54,0,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,0,0,56,56,56,56,0,56,56,66,56,56,56,56],"f":[0,0,0,[[],1],0,0,0,0,0,0,0,0,0,0,0,0,[2,2],[2,2],[2,2],[2,2],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[[],2],0,[[],2],[-1,-1,[]],[-1,-1,[]],[3,[[6,[4,5]]]],[3,[[6,[7,5]]]],[3,[[6,[4,5]]]],[3,[[6,[7,5]]]],[[],[[9,[8]]]],[10,11],[-1,-2,[],[]],[-1,-2,[],[]],0,[[4,12],[[14,[13]]]],[[7,12],[[14,[13]]]],[[],15],[-1,[[6,[-2]]],[],[]],[-1,[[6,[-2]]],[],[]],[-1,[[6,[-2]]],[],[]],[-1,[[6,[-2]]],[],[]],[-1,16,[]],[-1,16,[]],[[4,3],[[6,[1,5]]]],[[7,3],[[6,[1,5]]]],[[4,3],[[6,[1,5]]]],[[7,3],[[6,[1,5]]]],0,0,0,[-1,-2,[],[]],[-1,-2,[],[]],0,0,0,0,0,0,0,[[-1,12],[[14,[13]]],[]],0,0,0,0,0,[2,2],[2,2],[-1,-2,[],[]],[-1,-2,[],[]],[[17,18],19],0,[-1,-1,[]],[3,[[6,[17,5]]]],[3,[[6,[17,5]]]],[[],[[9,[8]]]],[-1,-2,[],[]],0,[[17,12],[[14,[13]]]],0,[-1,[[6,[-2]]],[],[]],[-1,[[6,[-2]]],[],[]],[-1,16,[]],[[17,3],[[6,[1,5]]]],[[17,3],[[6,[1,5]]]],[-1,-2,[],[]],0,[2,2],[2,2],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-1,[]],[3,[[6,[20,5]]]],[3,[[6,[20,5]]]],[[],[[9,[8]]]],[-1,-2,[],[]],[[20,12],[[14,[13]]]],0,[-1,[[6,[-2]]],[],[]],[-1,[[6,[-2]]],[],[]],[-1,16,[]],[[20,3],[[6,[1,5]]]],[[20,3],[[6,[1,5]]]],[-1,-2,[],[]],0,[2,2],[2,2],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-1,[]],[3,[[6,[21,5]]]],[3,[[6,[21,5]]]],[[],[[9,[8]]]],[-1,-2,[],[]],[[21,12],[[14,[13]]]],0,[-1,[[6,[-2]]],[],[]],[-1,[[6,[-2]]],[],[]],[-1,16,[]],[[21,3],[[6,[1,5]]]],[[21,3],[[6,[1,5]]]],[-1,-2,[],[]],0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,[[22,23],11],[2,2],[2,2],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[24,24],[22,22],[25,25],[26,26],[27,27],[28,28],[[-1,-2],1,[],[]],[[-1,-2],1,[],[]],[[-1,-2],1,[],[]],[[-1,-2],1,[],[]],[[-1,-2],1,[],[]],[[-1,-2],1,[],[]],[[26,26],29],[[-1,-2],29,[],[]],0,0,[[22,22],11],[[26,26],11],[[-1,-2],11,[],[]],[[-1,-2],11,[],[]],[[-1,-2],11,[],[]],[[-1,-2],11,[],[]],[[22,18],19],[[22,18],19],[[26,18],19],0,[-1,-1,[]],[-1,-1,[]],[-1,-1,[]],[-1,-1,[]],[-1,-1,[]],[-1,-1,[]],[-1,-1,[]],[-1,-1,[]],[-1,-1,[]],[-1,-1,[]],[3,[[6,[30,5]]]],[3,[[6,[30,5]]]],[[[9,[31]]],[[14,[[33,[32]]]]]],[10,[[6,[28]]]],[[],[[9,[8]]]],0,[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],0,0,[[34,22],32],[30,24],[[26,26],[[9,[29]]]],0,[[30,12],[[14,[13]]]],[[10,[9,[31]]],22],0,0,0,[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[24,[[9,[35]]]],[-1,36,[]],[-1,36,[]],0,[-1,[[6,[-2]]],[],[]],[-1,[[6,[-2]]],[],[]],[-1,[[6,[-2]]],[],[]],[-1,[[6,[-2]]],[],[]],[-1,[[6,[-2]]],[],[]],[-1,[[6,[-2]]],[],[]],[-1,[[6,[-2]]],[],[]],[-1,[[6,[-2]]],[],[]],[-1,[[6,[-2]]],[],[]],[-1,[[6,[-2]]],[],[]],[-1,[[6,[-2]]],[],[]],[-1,[[6,[-2]]],[],[]],[-1,[[6,[-2]]],[],[]],[-1,[[6,[-2]]],[],[]],[-1,[[6,[-2]]],[],[]],[-1,[[6,[-2]]],[],[]],[-1,[[6,[-2]]],[],[]],[-1,[[6,[-2]]],[],[]],[-1,[[6,[-2]]],[],[]],[-1,[[6,[-2]]],[],[]],[-1,16,[]],[-1,16,[]],[-1,16,[]],[-1,16,[]],[-1,16,[]],[-1,16,[]],[-1,16,[]],[-1,16,[]],[-1,16,[]],[-1,16,[]],[[30,3],[[6,[1,5]]]],[[30,3],[[6,[1,5]]]],[[],[[37,[24]]]],0,0,0,[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],0,0,0,0,0,0,0,0,0,0,[[[9,[[38,[25]]]],[37,[27]],[37,[32]]],36],[[[37,[25]],[37,[27]],[37,[32]]],36],[[[37,[25]]],36],[[[37,[27]]],36],[[26,[37,[27]]],36],[[[37,[32]]],36],[[10,[37,[32]]],36],[39,[[9,[36]]]],[32,36],[[[37,[32]]],36],[27,36],[26,36],[[[37,[27]]],36],[[[37,[27]]],36],[25,36],[[[37,[25]]],36],[32,36],[[[37,[32]]],36],[32,36],[-1,36,[[40,[10]]]],[[[37,[27]]],[[9,[36]]]],[[[37,[32]]],[[9,[36]]]],[[[37,[25]]],[[9,[36]]]],[[10,[37,[32]]],36],[[22,41],36],[32,36],[27,36],[[10,32],[[9,[36]]]],[39,[[9,[36]]]],[32,36],0,0,0,0,0,0,0,0,0,0,0,0,[[[9,[31]],[9,[42]]],[[14,[39]]]],[[43,[9,[31]],[9,[42]]],[[9,[[1,[22,41]]]]]],[[[9,[31]],[9,[42]]],[[14,[39]]]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-1,[]],[-1,-1,[]],[-1,-2,[],[]],[-1,-2,[],[]],[[[9,[31]],[9,[42]],23],[[14,[39]]]],[[[9,[31]],[9,[42]],23],[[14,[39]]]],[[10,[9,[31]],23],[[14,[39]]]],[[[9,[31]],[9,[42]],23],[[14,[39]]]],[[10,[9,[31]]],[[14,[22]]]],[-1,[[6,[-2]]],[],[]],[-1,[[6,[-2]]],[],[]],[-1,[[6,[-2]]],[],[]],[-1,[[6,[-2]]],[],[]],[-1,16,[]],[-1,16,[]],[43,[[0,[44]]]],[[43,[9,[31]],[9,[42]],41],22],[-1,-2,[],[]],[-1,-2,[],[]],[[[9,[31]],[9,[42]],23],[[14,[39]]]],0,0,0,0,0,0,0,0,0,0,0,[2,2],[2,2],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-1,[]],[3,[[6,[45,5]]]],[3,[[6,[45,5]]]],[[],[[9,[8]]]],[-1,-2,[],[]],[[45,12],[[14,[13]]]],0,[-1,[[6,[-2]]],[],[]],[-1,[[6,[-2]]],[],[]],[-1,16,[]],[[45,3],[[6,[1,5]]]],[[45,3],[[6,[1,5]]]],[-1,-2,[],[]],0,[2,2],[2,2],[-1,-2,[],[]],[-1,-2,[],[]],0,0,0,[[46,18],19],[-1,-1,[]],[3,[[6,[46,5]]]],[3,[[6,[46,5]]]],[[],[[9,[8]]]],[-1,-2,[],[]],0,0,0,0,[46,[[47,[10,10]]]],[[46,12],[[14,[48]]]],0,[[46,12],[[14,[13]]]],[-1,[[6,[-2]]],[],[]],[-1,[[6,[-2]]],[],[]],[-1,16,[]],[[46,3],[[6,[1,5]]]],[[46,3],[[6,[1,5]]]],[-1,-2,[],[]],0,0,[2,2],[2,2],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-1,[]],[3,[[6,[49,5]]]],[3,[[6,[49,5]]]],[[],[[9,[8]]]],[-1,-2,[],[]],0,[[49,12],[[14,[13]]]],[-1,[[6,[-2]]],[],[]],[-1,[[6,[-2]]],[],[]],[-1,16,[]],[[49,3],[[6,[1,5]]]],[[49,3],[[6,[1,5]]]],[-1,-2,[],[]],[[50,10,[33,[51]]],1],[[50,10,[33,[51]]],1],[[50,10,[33,[51]]],1],[[],[[14,[[33,[51]]]]]],[50,36],[50,[[9,[36]]]],[[],[[14,[1]]]],[[50,36,10],[[52,[1]]]],[[50,36,10],[[52,[1]]]],0,[2,2],[2,2],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-1,[]],[3,[[6,[53,5]]]],[3,[[6,[53,5]]]],[[],[[9,[8]]]],[-1,-2,[],[]],[[53,12],[[14,[13]]]],0,[-1,[[6,[-2]]],[],[]],[-1,[[6,[-2]]],[],[]],[-1,16,[]],[[53,3],[[6,[1,5]]]],[[53,3],[[6,[1,5]]]],[-1,-2,[],[]],0,0,0,0,[2,2],[2,2],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-1,[]],[3,[[6,[54,5]]]],[3,[[6,[54,5]]]],[[],[[9,[8]]]],[-1,-2,[],[]],[[54,12],[[14,[13]]]],[-1,[[6,[-2]]],[],[]],[-1,[[6,[-2]]],[],[]],[-1,16,[]],[[54,3],[[6,[1,5]]]],[[54,3],[[6,[1,5]]]],[-1,-2,[],[]],0,[2,2],[2,2],0,[-1,-2,[],[]],[-1,-2,[],[]],[-1,-1,[]],[3,[[6,[55,5]]]],[3,[[6,[55,5]]]],[[],[[9,[8]]]],[-1,-2,[],[]],[[55,12],[[14,[13]]]],[-1,[[6,[-2]]],[],[]],[-1,[[6,[-2]]],[],[]],[-1,16,[]],[[55,3],[[6,[1,5]]]],[[55,3],[[6,[1,5]]]],[-1,-2,[],[]],0,0,0,0,[-1,-2,[],[]],[-1,-2,[],[]],[[],[[6,[1,56]]]],[-1,-1,[]],[-1,-2,[],[]],[-1,[[6,[-2,56]]],[],[]],[-1,[[6,[-2]]],[],[]],[-1,[[6,[-2]]],[],[]],[-1,16,[]],[-1,-2,[],[]]],"c":[],"p":[[15,"tuple"],[3,"Command",530],[3,"ArgMatches",531],[3,"Volta",4],[6,"Error",532],[4,"Result",533],[4,"Subcommand",4],[3,"Id",534],[4,"Option",535],[15,"str"],[15,"bool"],[3,"Session",536],[4,"ExitCode",537],[6,"Fallible",537],[3,"Styles",538],[3,"TypeId",539],[3,"Completions",68],[3,"Formatter",540],[6,"Result",540],[3,"Fetch",89],[3,"Install",107],[4,"Source",125],[4,"Filter",125],[4,"Format",125],[3,"Node",125],[4,"PackageManagerKind",125],[3,"PackageManager",125],[4,"Subcommand",125],[4,"Ordering",541],[3,"List",125],[3,"Project",542],[4,"Package",125],[3,"Vec",543],[3,"PackageConfig",544],[3,"PossibleValue",545],[3,"String",546],[15,"slice"],[3,"Box",547],[4,"Toolchain",337],[8,"AsRef",548],[3,"Version",549],[3,"PlatformSpec",550],[4,"Lookup",337],[8,"Fn",551],[3,"Pin",386],[3,"Run",404],[3,"HashMap",552],[3,"CliPlatform",550],[3,"Setup",433],[3,"Path",553],[3,"PathBuf",553],[6,"Result",554],[3,"Uninstall",460],[3,"Use",478],[3,"Which",498],[4,"Error",516],[8,"Command",56],[3,"PackageDetails",125],[13,"Default",297],[13,"Project",297],[13,"PackageOrTool",303],[13,"Tool",376],[13,"PackageManagers",376],[13,"Active",376],[13,"All",376],[8,"IntoResult",516]],"b":[[200,"impl-Display-for-Source"],[201,"impl-Debug-for-Source"]]},\
+"volta_core":{"doc":"The main implementation crate for the core of Volta.","t":"RAAAAAAAAAAAAAAAAAAAAFNNNNNNNNNNINNNNNNNNNNNNNNENNNENNGNNNNNDNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNDNNNNNNNNNNLLLLLLLLLLLLLLLLLLLMLLLALMFALMLLLLLLLLLLLLLLKMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNNNNNNNNNNNNNNNNNNNNNENNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNRNNNNNNNNNNNNNNNNNNNNNNNRNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNLLLLLLLLLLLLMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMFFFFNNNDDEDNNLLLLLLMLLLLLLLLLLLLLLLLLLLLLMMMLLLLLLFLLLLLLMMMMLLLLMLLLLLLLLLLLLFLLLLMMMMMMFFFFFFFFFFFFFNDNDDNEEDNDLLLLLLLLLLLLLLLMMLLLLLLLLLLLMLLLLLLLLLLLLLMMLLLLLLLLMMLLLLOLMLMMLMMAMALLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLMDDDDDDDMMMLLLLLLLLLLLLLLLLLLLLLMMMMLLLLLLLMMLLLLLLLLLLLLLLMMMMMMMMMLLLLLLLMMLLLLLLLLLLLLLLLLLLLLLMLLLLLLLMRNNERRERNNHHNNRDLLLLLLFLLLLLLLLLLLLLLLFLLLMLLLLLLMLLLLLLLLLLLLLLLMMMMFFFFFFFFFFHHFFAFFFFRNREEDRRNNRRNRNNNRRLLLLLLLLMLLLLLLLLLLMFLLLLLLLLLLLLLLLLLFFFFNDNNDNENDDNNEDDLLLLLLLLLLLLLLLLFLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLALLLLLLLLLMMMMMMMMLMMMMMALLLLLLLLLLLLLLLLLLLLLLLLLLMLLLLLLLLLLMMMMDLLLLLMMLMLLLLLMDLLLLLLLLLDDDLLLLLLMLFLLLLLLLLLLLLLLFFFFLMLLMMLLLLLMMMALLLLLLLLLLLLMLMGDENNNDDNMMLLLLLLLLLMMLLMMMLLLLLLLLLLLMMLMMLLLLLLLLLLLLLLFMLLLLMRRAFFFFAFFFAAAAAFADMLLFFLLLLFMFLLLLNNENDNNNNNDNDNDNNNDENDNLLLLLLLLLLLLLLLLLLLLLLMMMMLLLLLLLLLLLLLLLLLLLLLLLLLLLLLMLLLLLLLLMLLLLLLMMMMMMMLLLLLLLLLLLLLLLLLLLLLLLLMLLLLLLLLFFFFFHFFENENDNENDRRRRRRRNRNDNNDLLLLLLLLLLLLLLMMMLLLLLLLLLLLLLLLLFLLLLLLLFFMMMMMMLLLLLLLLLLLLLLLLLLLLLLLLLLLLFFFFFFENNNNNNNNNNNNNNNDNNNNNNNNLLLLLLLLLLLLLLLLLLMLLLLLLMLLLLLMLLLLLLMLLLLLLLLLNNNNELLFFLLLLLLFLAFLLLLFFRHFFRRFFFFFFFFRHDDMLLLLLMLMLLLLLLLLLLLLNCCNECCCCNCNRCNCDCCNEICNLLLLLFFFFKFLLLLLLFFFFKLLCLAAAKAAFLALLLLLLLLLLMLLARRRRDDLLLLLLLLLALLLLLLLLLFALMLAFMLLLLLLLLLMLLDLLLFFFLLFFFFFLLLFLMLDDDDLLLLLLLLLLMMLLLLLLLLLLMMFMLLLLLLLLLLLLMMLLLLFFFFFFFFFDDLLLLLLLALLLLLLLLLLLLLAFLLLLLLLLMLLFFFFFFFFFFDDDENNDDEDNNNMMLLLLLLLLLLLLLLAMLLLLLLLLALLLLLFAMMMMAMMMMMLLLMMFLMMLLLFMMLLLLLLLLLLLLLLAFMMMMLLLLLFFFFNENNLLLLLLLLLLLLLLLLFFFLLLLLLLLLLLDDDDDMMLLLLLLLLLLLLFMLLLLLLLLLLLLLLLLLLLLLLLLLMMMMMMMMLMMMALLLLLLLLLLLLLLLLLLMMMLLLLLLLMDLLFLLLLLLLLLLFFFFDLLLLLALLLLLLLAFLLLLMLFFFFFFFFFFRDDDDDLLLLLLLLLLLLLLLMMMFFLLLLLLLLLLLLLLLMFFFMMMLLLLLLLLLLLLLLLLMMMLLLLLHHFDLLLLLALLLLLALLAFLLLLMLFFFFFFDDDDMLLLLLLLLLLLMLLLLLLLLLLMMLLLLLLLLLLLLLLLLFFFFFFFFFDDLLLLLLLLLLLLLMLALLLLMLLLLLLLLDDLLLLLLLLLLLLLLLLLLLLLLLMMLMMLLLLLLLLLLLMNNNNNNNEELLLLLLLLLLLLLALLAFFALLFLLLLLLALLDLLFLLLLLLLFFFDLLFLLLLFLLLLL","n":["VOLTA_FEATURE_PNPM","command","error","event","fs","hook","inventory","layout","log","monitor","platform","project","run","session","shim","signal","style","sync","tool","toolchain","version","create_command","BinaryAlreadyInstalled","BinaryExecError","BinaryNotFound","BuildPathError","BypassError","CannotFetchPackage","CannotPinPackage","CompletionsOutFileError","ConfigurationError","ContainingDirError","Context","CouldNotDetermineTool","CouldNotStartMigration","CreateDirError","CreateLayoutFileError","CreateSharedLinkError","CreateTempDirError","CreateTempFileError","CurrentDirError","DeleteDirectoryError","DeleteFileError","DeprecatedCommandError","DownloadToolNetworkError","EnvironmentError","Err","ErrorKind","ExecutableNotFound","ExecuteHookError","ExecutionFailure","ExitCode","ExtensionCycleError","ExtensionPathError","Fallible","FileSystemError","HookCommandFailed","HookMultipleFieldsSpecified","HookNoFieldsSpecified","HookPathError","Inner","InstalledPackageNameError","InvalidArguments","InvalidHookCommand","InvalidHookOutput","InvalidInvocation","InvalidInvocationOfBareVersion","InvalidRegistryFormat","InvalidToolName","LockAcquireError","NetworkError","NoBundledNpm","NoCommandLinePnpm","NoCommandLineYarn","NoDefaultNodeVersion","NoDefaultPnpm","NoDefaultYarn","NoHomeEnvironmentVar","NoInstallDir","NoLocalDataDir","NoPinnedNodeVersion","NoPlatform","NoProjectNodeInManifest","NoProjectPnpm","NoProjectYarn","NoShellProfile","NoVersionMatch","NodeVersionNotFound","NotInPackage","NotYetImplemented","NpmLinkMissingPackage","NpmLinkWrongManager","NpmVersionNotFound","NpxNotAvailable","Ok","PackageInstallFailed","PackageManifestParseError","PackageManifestReadError","PackageNotFound","PackageParseError","PackageReadError","PackageUnpackError","PackageWriteError","ParseBinConfigError","ParseHooksError","ParseNodeIndexCacheError","ParseNodeIndexError","ParseNodeIndexExpiryError","ParseNpmManifestError","ParsePackageConfigError","ParsePlatformError","ParseToolSpecError","PersistInventoryError","PnpmVersionNotFound","ProjectLocalBinaryExecError","ProjectLocalBinaryNotFound","PublishHookBothUrlAndBin","PublishHookNeitherUrlNorBin","ReadBinConfigDirError","ReadBinConfigError","ReadDefaultNpmError","ReadDirError","ReadHooksError","ReadNodeIndexCacheError","ReadNodeIndexExpiryError","ReadNpmManifestError","ReadPackageConfigError","ReadPlatformError","RegistryFetchError","RunShimDirectly","SetToolExecutable","SetupToolImageError","ShimCreateError","ShimRemoveError","StringifyBinConfigError","StringifyPackageConfigError","StringifyPlatformError","Success","Unimplemented","UnknownError","UnpackArchiveError","UpgradePackageNotFound","UpgradePackageWrongManager","VersionParseError","VoltaError","WriteBinConfigError","WriteDefaultNpmError","WriteLauncherError","WriteNodeIndexCacheError","WriteNodeIndexExpiryError","WritePackageConfigError","WritePlatformError","Yarn2NotSupported","YarnLatestFetchError","YarnVersionNotFound","borrow","borrow","borrow","borrow_mut","borrow_mut","borrow_mut","clone","clone_into","exit","exit_code","fmt","fmt","fmt","fmt","from","from","from","from","from_source","inner","into","into","into","kind","kind","kind","report_error","reporter","source","source","to_owned","to_string","try_from","try_from","try_from","try_into","try_into","try_into","type_id","type_id","type_id","vzip","vzip","vzip","with_context","action","action","advice","bin_dir","bin_name","command","command","command","command","command","command","command","command","command","command","dir","dir","dir","dir","directory","duplicate","env_profile","errors","existing_package","feature","file","file","file","file","file","file","file","file","file","file","file","file","file","file","file","file","file","file","file","format","from_url","from_url","from_url","from_url","in_dir","in_dir","manager","manager","matching","matching","matching","matching","name","name","name","name","name","name","new_package","package","package","package","package","package","package","package","package","package","package","path","path","path","paths","tool","tool","tool","tool","tool","tool","tool","tool","tool","tool_spec","version","version","version","version","version","version","BinaryAlreadyInstalled","BinaryExecError","BinaryNotFound","BuildPathError","BypassError","CannotFetchPackage","CannotPinPackage","CompletionsOutFileError","ContainingDirError","CouldNotDetermineTool","CouldNotStartMigration","CreateDirError","CreateLayoutFileError","CreateSharedLinkError","CreateTempDirError","CreateTempFileError","CurrentDirError","DeleteDirectoryError","DeleteFileError","DeprecatedCommandError","DownloadToolNetworkError","ErrorKind","ExecuteHookError","ExtensionCycleError","ExtensionPathError","HookCommandFailed","HookMultipleFieldsSpecified","HookNoFieldsSpecified","HookPathError","InstalledPackageNameError","InvalidHookCommand","InvalidHookOutput","InvalidInvocation","InvalidInvocationOfBareVersion","InvalidRegistryFormat","InvalidToolName","LockAcquireError","NoBundledNpm","NoCommandLinePnpm","NoCommandLineYarn","NoDefaultNodeVersion","NoDefaultPnpm","NoDefaultYarn","NoHomeEnvironmentVar","NoInstallDir","NoLocalDataDir","NoPinnedNodeVersion","NoPlatform","NoProjectNodeInManifest","NoProjectPnpm","NoProjectYarn","NoShellProfile","NodeVersionNotFound","NotInPackage","NpmLinkMissingPackage","NpmLinkWrongManager","NpmVersionNotFound","NpxNotAvailable","PERMISSIONS_CTA","PackageInstallFailed","PackageManifestParseError","PackageManifestReadError","PackageNotFound","PackageParseError","PackageReadError","PackageUnpackError","PackageWriteError","ParseBinConfigError","ParseHooksError","ParseNodeIndexCacheError","ParseNodeIndexError","ParseNodeIndexExpiryError","ParseNpmManifestError","ParsePackageConfigError","ParsePlatformError","ParseToolSpecError","PersistInventoryError","PnpmVersionNotFound","ProjectLocalBinaryExecError","ProjectLocalBinaryNotFound","PublishHookBothUrlAndBin","PublishHookNeitherUrlNorBin","REPORT_BUG_CTA","ReadBinConfigDirError","ReadBinConfigError","ReadDefaultNpmError","ReadDirError","ReadHooksError","ReadNodeIndexCacheError","ReadNodeIndexExpiryError","ReadNpmManifestError","ReadPackageConfigError","ReadPlatformError","RegistryFetchError","RunShimDirectly","SetToolExecutable","SetupToolImageError","ShimCreateError","ShimRemoveError","StringifyBinConfigError","StringifyPackageConfigError","StringifyPlatformError","Unimplemented","UnpackArchiveError","UpgradePackageNotFound","UpgradePackageWrongManager","VersionParseError","WriteBinConfigError","WriteDefaultNpmError","WriteLauncherError","WriteNodeIndexCacheError","WriteNodeIndexExpiryError","WritePackageConfigError","WritePlatformError","Yarn2NotSupported","YarnLatestFetchError","YarnVersionNotFound","borrow","borrow_mut","exit_code","fmt","fmt","from","into","to_string","try_from","try_into","type_id","vzip","action","action","advice","bin_dir","bin_name","command","command","command","command","command","command","command","command","command","command","dir","dir","dir","dir","directory","duplicate","env_profile","errors","existing_package","feature","file","file","file","file","file","file","file","file","file","file","file","file","file","file","file","file","file","file","file","format","from_url","from_url","from_url","from_url","in_dir","in_dir","manager","manager","matching","matching","matching","matching","name","name","name","name","name","name","new_package","package","package","package","package","package","package","package","package","package","package","path","path","path","paths","tool","tool","tool","tool","tool","tool","tool","tool","tool","tool_spec","version","version","version","version","version","version","collect_arguments","compose_error_details","report_error","write_error_log","Args","End","Error","ErrorEnv","Event","EventKind","EventLog","Start","ToolEnd","add_event","add_event_args","add_event_end","add_event_error","add_event_start","add_event_tool_end","argv","borrow","borrow","borrow","borrow","borrow_mut","borrow_mut","borrow_mut","borrow_mut","deserialize","deserialize","deserialize","eq","eq","equivalent","equivalent","equivalent","equivalent","equivalent","equivalent","equivalent","equivalent","event","events","exec_path","fmt","fmt","from","from","from","from","get_error_env","init","into","into","into","into","into_event","name","path","platform","platform_version","publish","serialize","serialize","serialize","timestamp","try_from","try_from","try_from","try_from","try_into","try_into","try_into","try_into","type_id","type_id","type_id","type_id","unix_timestamp","vzip","vzip","vzip","vzip","argv","env","error","exit_code","exit_code","exit_code","create_staging_dir","create_staging_file","dir_entry_match","ok_if_not_found","read_dir_eager","read_file","remove_dir_if_exists","remove_file_if_exists","rename","set_executable","symlink_dir","symlink_file","touch","Bin","EventHooks","Github","HookConfig","LazyHookConfig","Npm","Publish","RegistryFormat","ToolHooks","Url","YarnHooks","borrow","borrow","borrow","borrow","borrow","borrow","borrow","borrow_mut","borrow_mut","borrow_mut","borrow_mut","borrow_mut","borrow_mut","borrow_mut","current","distro","distro","eq","eq","equivalent","equivalent","equivalent","equivalent","equivalent","equivalent","equivalent","equivalent","events","events","fmt","fmt","from","from","from","from","from","from","from","from_file","from_paths","from_str","get","index","index","init","into","into","into","into","into","into","into","latest","latest","merge","merge","merge","merge","merge_hooks","node","node","npm","npm","phantom","pnpm","pnpm","publish","serial","settings","tool","try_from","try_from","try_from","try_from","try_from","try_from","try_from","try_from","try_from","try_into","try_into","try_into","try_into","try_into","try_into","try_into","type_id","type_id","type_id","type_id","type_id","type_id","type_id","vzip","vzip","vzip","vzip","vzip","vzip","vzip","yarn","yarn","RawEventHooks","RawHookConfig","RawIndexHook","RawPublishHook","RawResolveHook","RawToolHooks","RawYarnHooks","bin","bin","bin","borrow","borrow","borrow","borrow","borrow","borrow","borrow","borrow_mut","borrow_mut","borrow_mut","borrow_mut","borrow_mut","borrow_mut","borrow_mut","deserialize","deserialize","deserialize","deserialize","deserialize","deserialize","deserialize","distro","distro","events","format","from","from","from","from","from","from","from","index","index","into","into","into","into","into","into","into","into_distro_hook","into_hook","into_hook_config","into_index_hook","into_metadata_hook","into_tool_hooks","into_yarn_hooks","latest","latest","node","npm","phantom","pnpm","prefix","prefix","publish","serialize","serialize","serialize","serialize","serialize","serialize","serialize","template","template","try_from","try_from","try_from","try_from","try_from","try_from","try_from","try_into","try_into","try_into","try_into","try_into","try_into","try_into","type_id","type_id","type_id","type_id","type_id","type_id","type_id","url","vzip","vzip","vzip","vzip","vzip","vzip","vzip","yarn","ARCH_TEMPLATE","Bin","Bin","DistroHook","EXTENSION_TEMPLATE","FILENAME_TEMPLATE","MetadataHook","OS_TEMPLATE","Prefix","Prefix","REL_PATH","REL_PATH_PARENT","Template","Template","VERSION_TEMPLATE","YarnIndexHook","borrow","borrow","borrow","borrow_mut","borrow_mut","borrow_mut","calculate_extension","eq","eq","eq","equivalent","equivalent","equivalent","equivalent","equivalent","equivalent","equivalent","equivalent","equivalent","equivalent","equivalent","equivalent","execute_binary","fmt","fmt","fmt","format","from","from","from","into","into","into","metadata","resolve","resolve","resolve","try_from","try_from","try_from","try_into","try_into","try_into","type_id","type_id","type_id","vzip","vzip","vzip","base_path","bin","base_path","bin","node_available","node_versions","npm_available","npm_versions","package_configs","pnpm_available","pnpm_versions","read_versions","yarn_available","yarn_versions","VOLTA_HOME","VOLTA_INSTALL","default_install_dir","env_paths","unix","volta_home","volta_install","default_home_dir","env_paths","ALLOWED_PREFIXES","Default","ERROR_PREFIX","LogContext","LogVerbosity","Logger","MIGRATION_ERROR_PREFIX","MIGRATION_WARNING_PREFIX","Migration","Quiet","SHIM_ERROR_PREFIX","SHIM_WARNING_PREFIX","Shim","VOLTA_LOGLEVEL","Verbose","VeryVerbose","Volta","WARNING_PREFIX","WRAP_INDENT","borrow","borrow","borrow","borrow_mut","borrow_mut","borrow_mut","clone","clone_into","context","enabled","flush","fmt","from","from","from","init","into","into","into","level","level_from_env","log","log_error","log_warning","new","to_owned","try_from","try_from","try_from","try_into","try_into","try_into","type_id","type_id","type_id","vzip","vzip","vzip","wrap_content","send_events","spawn_process","write_events_file","Binary","CliPlatform","CommandLine","Default","Image","Inherit","InheritOption","None","Platform","PlatformSpec","Project","Some","Source","Sourced","System","as_binary","as_default","as_project","as_ref","borrow","borrow","borrow","borrow","borrow","borrow","borrow_mut","borrow_mut","borrow_mut","borrow_mut","borrow_mut","borrow_mut","build_path_error","checkout","clone","clone","clone","clone","clone","clone","clone_into","clone_into","clone_into","clone_into","clone_into","clone_into","cloned","cmp","compare","current","default","eq","equivalent","equivalent","equivalent","equivalent","fmt","from","from","from","from","from","from","image","inherit","into","into","into","into","into","into","map","merge","node","node","node","node","npm","npm","npm","npm","partial_cmp","pnpm","pnpm","pnpm","pnpm","source","system","to_owned","to_owned","to_owned","to_owned","to_owned","to_owned","to_string","try_from","try_from","try_from","try_from","try_from","try_from","try_from","try_into","try_into","try_into","try_into","try_into","try_into","type_id","type_id","type_id","type_id","type_id","type_id","value","vzip","vzip","vzip","vzip","vzip","vzip","with_binary","with_command_line","with_default","with_project","yarn","yarn","yarn","yarn","Image","bins","borrow","borrow_mut","from","into","node","npm","path","pnpm","resolve_npm","try_from","try_into","type_id","vzip","yarn","System","borrow","borrow_mut","from","into","path","try_from","try_into","type_id","vzip","LazyProject","PartialPlatform","Project","borrow","borrow","borrow","borrow_mut","borrow_mut","borrow_mut","dependencies","find_bin","find_closest_root","for_current_dir","for_dir","from","from","from","from_file","get","get_mut","has_direct_bin","has_direct_dependency","init","into","into","into","is_dependency","is_node_modules","is_node_root","is_project_root","manifest_file","manifest_file","merge","needs_yarn_run","node","npm","pin_node","pin_npm","pin_pnpm","pin_yarn","platform","platform","pnpm","project","serial","try_from","try_from","try_from","try_into","try_into","try_into","type_id","type_id","type_id","vzip","vzip","vzip","workspace_manifests","workspace_roots","yarn","DependencyMapIterator","Manifest","ManifestKey","Node","Npm","Pnpm","RawManifest","ToolchainSpec","Yarn","a","b","borrow","borrow","borrow","borrow","borrow_mut","borrow_mut","borrow_mut","borrow_mut","default","dependencies","dependency_maps","deserialize","deserialize","dev_dependencies","extends","extends","fmt","from","from","from","from","from_file","from_file","into","into","into","into","node","npm","parse_split","platform","pnpm","serialize","to_string","try_from","try_from","try_from","try_from","try_into","try_into","try_into","try_into","type_id","type_id","type_id","type_id","update_manifest","volta","vzip","vzip","vzip","vzip","yarn","RECURSION_ENV_VAR","VOLTA_BYPASS","binary","debug_active_image","debug_no_platform","execute_shim","execute_tool","executor","format_tool_version","get_executor","get_tool_name","node","npm","npx","parser","pnpm","tool_name_from_file_name","yarn","DefaultBinary","bin_path","borrow","borrow_mut","command","default_execution_context","from","from_config","from_name","into","local_execution_context","platform","shared_module_path","try_from","try_into","type_id","vzip","Bypass","DefaultBinary","Executor","InternalInstall","InternalInstallCommand","Multiple","Node","Npm","Npx","PackageInstall","PackageInstallCommand","PackageLink","PackageLinkCommand","PackageUpgrade","PackageUpgradeCommand","Pnpm","ProjectLocalBinary","Tool","ToolCommand","ToolKind","Uninstall","UninstallCommand","Yarn","borrow","borrow","borrow","borrow","borrow","borrow","borrow","borrow","borrow_mut","borrow_mut","borrow_mut","borrow_mut","borrow_mut","borrow_mut","borrow_mut","borrow_mut","check_linked_package","cli_platform","cli_platform","cli_platform","cli_platform","cli_platform","command","command","command","command","env","envs","envs","envs","envs","envs","execute","execute","execute","execute","execute","execute","execute","for_npm_link","from","from","from","from","from","from","from","from","from","from","from","from","from","from","from","installer","into","into","into","into","into","into","into","into","kind","new","new","new","new","new","new","platform","platform","platform","platform","tool","tool","tool","try_from","try_from","try_from","try_from","try_from","try_from","try_from","try_from","try_into","try_into","try_into","try_into","try_into","try_into","try_into","try_into","type_id","type_id","type_id","type_id","type_id","type_id","type_id","type_id","upgrader","vzip","vzip","vzip","vzip","vzip","vzip","vzip","vzip","command","execution_context","command","current_project_name","execution_context","REQUIRED_NPM_VERSION","command","execution_context","CommandArg","Global","GlobalCommand","Install","InstallArgs","Intercepted","InterceptedCommand","Link","LinkArgs","NPM_INSTALL_ALIASES","NPM_LINK_ALIASES","NPM_UNINSTALL_ALIASES","NPM_UPDATE_ALIASES","PNPM_LINK_ALIASES","PNPM_UNINSTALL_ALIASES","PNPM_UPDATE_ALIASES","Standard","UNSAFE_GLOBAL","Uninstall","UninstallArgs","Unlink","Upgrade","UpgradeArgs","borrow","borrow","borrow","borrow","borrow","borrow","borrow","borrow_mut","borrow_mut","borrow_mut","borrow_mut","borrow_mut","borrow_mut","borrow_mut","common_args","common_args","common_args","executor","executor","executor","executor","executor","executor_all_packages","for_npm","for_pnpm","for_yarn","from","from","from","from","from","from","from","has_global_without_prefix","into","into","into","into","into","into","into","is_flag","is_positional","manager","manager","tools","tools","tools","tools","try_from","try_from","try_from","try_from","try_from","try_from","try_from","try_into","try_into","try_into","try_into","try_into","try_into","try_into","type_id","type_id","type_id","type_id","type_id","type_id","type_id","vzip","vzip","vzip","vzip","vzip","vzip","vzip","command","execution_context","validate_platform_pnpm","command","execution_context","validate_platform_yarn","ActivityKind","Args","Binary","Completions","Current","Default","Fetch","Help","Install","List","Node","Npm","Npx","Pin","Pnpm","Run","Session","Setup","Shim","Tool","Uninstall","Version","Volta","Which","Yarn","add_event_end","add_event_error","add_event_start","add_event_tool_end","borrow","borrow","borrow_mut","borrow_mut","clone","clone_into","cmp","compare","default_platform","eq","equivalent","equivalent","equivalent","equivalent","event_log","exit","exit_tool","fmt","from","from","hooks","hooks","init","into","into","partial_cmp","project","project","project_mut","project_platform","publish_to_event_log","to_owned","to_string","toolchain","toolchain","toolchain_mut","try_from","try_from","try_into","try_into","type_id","type_id","vzip","vzip","AlreadyExists","Created","Deleted","DoesntExist","ShimResult","borrow","borrow_mut","create","delete","eq","equivalent","equivalent","equivalent","equivalent","from","get_shim_list_deduped","into","platform","regenerate_shims_for_dir","try_from","try_into","type_id","vzip","create","entry_to_shim_name","INTERRUPTED_EXIT_CODE","SHIM_HAS_CONTROL","pass_control_to_shim","setup_signal_handler","MAX_PROGRESS_WIDTH","MAX_WIDTH","action_str","format_error_cause","note_prefix","progress_bar","progress_spinner","success_prefix","text_width","tool_version","LOCK_FILE","LOCK_STATE","LockState","VoltaLock","_private","acquire","borrow","borrow","borrow_mut","borrow_mut","count","drop","file","from","from","into","into","try_from","try_from","try_into","try_into","type_id","type_id","vzip","vzip","AlreadyFetched","BinConfig","BundledNpm","FetchNeeded","FetchStatus","NODE_DISTRO_ARCH","NODE_DISTRO_EXTENSION","NODE_DISTRO_OS","Node","Node","Npm","Npm","PATH_VAR_NAME","Package","Package","PackageConfig","PackageDetails","PackageManifest","Pnpm","Pnpm","Spec","Tool","Yarn","Yarn","borrow","borrow","borrow_mut","borrow_mut","check_args","check_fetched","check_shim_reachable","debug_already_fetched","download_tool_error","fetch","find_expected_shim_dir","fmt","fmt","from","from","from_str_and_version","from_strings","info_fetched","info_installed","info_pinned","info_project_version","install","into","into","load_default_npm_version","name","node","npm","package","pin","pnpm","registry","registry_fetch_error","resolve","serial","sort_comparator","to_string","try_from","try_from","try_from_str","try_into","try_into","type_id","type_id","uninstall","version","vzip","vzip","yarn","NODE_DISTRO_ARCH","NODE_DISTRO_EXTENSION","NODE_DISTRO_IDENTIFIER","NODE_DISTRO_OS","Node","NodeVersion","archive_basename","archive_filename","borrow","borrow","borrow_mut","borrow_mut","clone","clone_into","ensure_fetched","fetch","fetch","fmt","fmt","fmt","from","from","install","into","into","load_default_npm_version","metadata","new","npm","pin","resolve","resolve","runtime","to_owned","to_string","to_string","try_from","try_from","try_into","try_into","type_id","type_id","version","vzip","vzip","Manifest","borrow","borrow_mut","deserialize","determine_remote_url","fetch","fetch_remote_distro","from","into","load_cached_distro","load_default_npm_version","npm_manifest_path","public_node_server_root","save_default_npm_version","try_from","try_into","type_id","unpack_archive","version","version","vzip","NodeEntry","NodeIndex","RawNodeEntry","RawNodeIndex","borrow","borrow","borrow","borrow","borrow_mut","borrow_mut","borrow_mut","borrow_mut","deserialize","deserialize","entries","files","fmt","from","from","from","from","from","into","into","into","into","lts","lts","lts_version_serde","npm","try_from","try_from","try_from","try_from","try_into","try_into","try_into","try_into","type_id","type_id","type_id","type_id","version","version","vzip","vzip","vzip","vzip","match_node_version","max_age","public_node_version_index","read_cached_opt","resolve","resolve_latest","resolve_lts","resolve_node_versions","resolve_semver","BundledNpm","Npm","archive_basename","archive_filename","borrow","borrow","borrow_mut","borrow_mut","ensure_fetched","fetch","fetch","fetch","fmt","fmt","from","from","install","install","into","into","new","pin","pin","resolve","resolve","to_string","to_string","try_from","try_from","try_into","try_into","type_id","type_id","version","vzip","vzip","determine_remote_url","fetch","fetch_remote_distro","load_cached_distro","overwrite_launcher","unpack_archive","fetch_npm_index","resolve","resolve_semver","resolve_tag","BinConfig","DirectInstall","InPlaceUpgrade","NeedsScope","No","Npm","Package","PackageConfig","PackageManager","PackageManifest","Pnpm","Yarn","Yes","bin","bins","borrow","borrow","borrow","borrow","borrow_mut","borrow_mut","borrow_mut","borrow_mut","check_upgraded_package","clone","clone_into","complete_install","complete_install","complete_upgrade","configure","directory","eq","fetch","fmt","from","from","from","from","from","install","install","into","into","into","into","link_package_to_shared_dir","manager","manager","manager","manager","manager","metadata","name","name","name","name","name","new","new","new","package","package","persist_install","pin","platform","platform","run_install","setup_command","setup_command","setup_staging_directory","staging","staging","to_owned","to_string","try_from","try_from","try_from","try_from","try_into","try_into","try_into","try_into","type_id","type_id","type_id","type_id","uninstall","uninstall","version","version","version","version","vzip","vzip","vzip","vzip","with_name","parse_manifest","validate_bins","write_config_and_shims","run_global_install","Npm","PackageManager","Pnpm","Yarn","binary_dir","borrow","borrow_mut","clone","clone_into","cmp","compare","deserialize","eq","equivalent","equivalent","equivalent","equivalent","fmt","from","get_installed_package","get_npm_package_name","get_pnpm_or_yarn_package_name","get_single_directory_name","into","partial_cmp","serialize","setup_global_command","source_dir","source_root","to_owned","try_from","try_into","type_id","vzip","BinConfig","GlobalYarnManifest","PackageConfig","PackageManifest","RawPlatformSpec","bin","bins","borrow","borrow","borrow","borrow","borrow","borrow_mut","borrow_mut","borrow_mut","borrow_mut","borrow_mut","cmp","compare","default_binary_name","dependencies","deserialize","deserialize","deserialize","deserialize","deserialize","eq","equivalent","equivalent","equivalent","equivalent","for_dir","from","from","from","from","from","from_file","from_file","from_file_if_exists","from_file_if_exists","into","into","into","into","into","manager","manager","name","name","name","node","npm","package","partial_cmp","platform","platform","pnpm","serde_bins","serialize","serialize","serialize","try_from","try_from","try_from","try_from","try_from","try_into","try_into","try_into","try_into","try_into","type_id","type_id","type_id","type_id","type_id","version","version","version","vzip","vzip","vzip","vzip","vzip","write","write","yarn","BinMapVisitor","borrow","borrow_mut","deserialize","expecting","fmt","from","into","try_from","try_into","type_id","visit_map","visit_str","vzip","binaries_from_package","remove_config_and_shim","remove_shared_link_dir","uninstall","Pnpm","archive_basename","archive_filename","borrow","borrow_mut","ensure_fetched","fetch","fetch","fmt","from","install","into","new","pin","resolve","resolve","to_string","try_from","try_into","type_id","version","vzip","determine_remote_url","fetch","fetch_remote_distro","load_cached_distro","unpack_archive","write_launcher","fetch_pnpm_index","resolve","resolve_semver","resolve_tag","NPM_ABBREVIATED_ACCEPT_HEADER","PackageDetails","PackageIndex","RawDistInfo","RawPackageMetadata","RawPackageVersionInfo","borrow","borrow","borrow","borrow","borrow","borrow_mut","borrow_mut","borrow_mut","borrow_mut","borrow_mut","clone","clone_into","deserialize","deserialize","deserialize","dist","dist_tags","entries","fetch_npm_registry","find_unpack_dir","fmt","fmt","fmt","fmt","from","from","from","from","from","from","into","into","into","into","into","name","public_registry_index","public_registry_package","scoped_public_registry_package","shasum","tags","tarball","to_owned","try_from","try_from","try_from","try_from","try_from","try_into","try_into","try_into","try_into","try_into","type_id","type_id","type_id","type_id","type_id","version","version","versions","vzip","vzip","vzip","vzip","vzip","HAS_VERSION","TOOL_SPEC_PATTERN","is_version_like","Yarn","archive_basename","archive_filename","borrow","borrow_mut","ensure_fetched","fetch","fetch","fmt","from","install","into","metadata","new","pin","resolve","resolve","to_string","try_from","try_into","type_id","version","vzip","determine_remote_url","ensure_bin_is_executable","fetch","fetch_remote_distro","load_cached_distro","unpack_archive","RawYarnAsset","RawYarnEntry","RawYarnIndex","YarnIndex","assets","borrow","borrow","borrow","borrow","borrow_mut","borrow_mut","borrow_mut","borrow_mut","deserialize","deserialize","deserialize","entries","from","from","from","from","from","into","into","into","into","is_full_release","name","tag_name","try_from","try_from","try_from","try_from","try_into","try_into","try_into","try_into","type_id","type_id","type_id","type_id","vzip","vzip","vzip","vzip","fetch_yarn_index","resolve","resolve_custom_tag","resolve_latest_legacy","resolve_semver","resolve_semver_from_registry","resolve_semver_legacy","resolve_semver_npm","resolve_tag","LazyToolchain","Toolchain","borrow","borrow","borrow_mut","borrow_mut","current","from","from","get","get_mut","init","into","into","platform","platform","save","serial","set_active_node","set_active_npm","set_active_pnpm","set_active_yarn","toolchain","try_from","try_from","try_into","try_into","type_id","type_id","vzip","vzip","NodeVersion","Platform","borrow","borrow","borrow_mut","borrow_mut","deserialize","deserialize","eq","eq","equivalent","equivalent","equivalent","equivalent","equivalent","equivalent","equivalent","equivalent","fmt","fmt","from","from","into","into","into_json","node","npm","of","pnpm","runtime","serialize","serialize","try_from","try_from","try_from","try_into","try_into","type_id","type_id","vzip","vzip","yarn","Custom","Exact","Latest","Lts","None","Semver","Tag","VersionSpec","VersionTag","borrow","borrow","borrow_mut","borrow_mut","default","fmt","fmt","fmt","fmt","from","from","from_str","from_str","hashmap_version_serde","into","into","option_version_serde","parse_requirements","parse_version","serial","to_string","to_string","trim_version","try_from","try_from","try_into","try_into","type_id","type_id","version_serde","vzip","vzip","Wrapper","borrow","borrow_mut","deserialize","deserialize","from","into","try_from","try_into","type_id","vzip","deserialize","serialize","parse_requirements","VersionVisitor","borrow","borrow_mut","deserialize","expecting","fmt","from","into","serialize","try_from","try_into","type_id","visit_str","vzip"],"q":[[0,"volta_core"],[21,"volta_core::command"],[22,"volta_core::error"],[200,"volta_core::error::ErrorKind"],[294,"volta_core::error::kind"],[423,"volta_core::error::kind::ErrorKind"],[517,"volta_core::error::reporter"],[521,"volta_core::event"],[600,"volta_core::event::EventKind"],[606,"volta_core::fs"],[619,"volta_core::hook"],[732,"volta_core::hook::serial"],[838,"volta_core::hook::tool"],[903,"volta_core::hook::tool::DistroHook"],[905,"volta_core::hook::tool::MetadataHook"],[907,"volta_core::inventory"],[917,"volta_core::layout"],[924,"volta_core::layout::unix"],[926,"volta_core::log"],[984,"volta_core::monitor"],[987,"volta_core::platform"],[1115,"volta_core::platform::image"],[1131,"volta_core::platform::system"],[1141,"volta_core::project"],[1201,"volta_core::project::serial"],[1265,"volta_core::run"],[1283,"volta_core::run::binary"],[1300,"volta_core::run::executor"],[1434,"volta_core::run::node"],[1436,"volta_core::run::npm"],[1439,"volta_core::run::npx"],[1442,"volta_core::run::parser"],[1542,"volta_core::run::pnpm"],[1545,"volta_core::run::yarn"],[1548,"volta_core::session"],[1621,"volta_core::shim"],[1644,"volta_core::shim::platform"],[1646,"volta_core::signal"],[1650,"volta_core::style"],[1660,"volta_core::sync"],[1685,"volta_core::tool"],[1758,"volta_core::tool::node"],[1803,"volta_core::tool::node::fetch"],[1824,"volta_core::tool::node::metadata"],[1872,"volta_core::tool::node::resolve"],[1881,"volta_core::tool::npm"],[1917,"volta_core::tool::npm::fetch"],[1923,"volta_core::tool::npm::resolve"],[1927,"volta_core::tool::package"],[2024,"volta_core::tool::package::configure"],[2027,"volta_core::tool::package::install"],[2028,"volta_core::tool::package::manager"],[2062,"volta_core::tool::package::metadata"],[2150,"volta_core::tool::package::metadata::serde_bins"],[2164,"volta_core::tool::package::uninstall"],[2168,"volta_core::tool::pnpm"],[2190,"volta_core::tool::pnpm::fetch"],[2196,"volta_core::tool::pnpm::resolve"],[2200,"volta_core::tool::registry"],[2272,"volta_core::tool::serial"],[2275,"volta_core::tool::yarn"],[2298,"volta_core::tool::yarn::fetch"],[2304,"volta_core::tool::yarn::metadata"],[2349,"volta_core::tool::yarn::resolve"],[2358,"volta_core::toolchain"],[2389,"volta_core::toolchain::serial"],[2431,"volta_core::version"],[2472,"volta_core::version::hashmap_version_serde"],[2483,"volta_core::version::option_version_serde"],[2485,"volta_core::version::serial"],[2486,"volta_core::version::version_serde"],[2500,"std::process"],[2501,"std::ffi::os_str"],[2502,"core::convert"],[2503,"core::fmt"],[2504,"core::fmt"],[2505,"alloc::boxed"],[2506,"core::convert"],[2507,"alloc::string"],[2508,"core::result"],[2509,"core::any"],[2510,"core::ops::function"],[2511,"std::path"],[2512,"serde::de"],[2513,"serde::ser"],[2514,"tempfile::dir"],[2515,"tempfile::file"],[2516,"std::path"],[2517,"std::io::error"],[2518,"core::ops::function"],[2519,"core::iter::traits::iterator"],[2520,"std::fs"],[2521,"core::iter::traits::collect"],[2522,"node_semver"],[2523,"alloc::collections::btree::set"],[2524,"volta_layout::v4"],[2525,"volta_layout::v1"],[2526,"log"],[2527,"log"],[2528,"core::cmp"],[2529,"std::ffi::os_str"],[2530,"std::env"],[2531,"core::fmt"],[2532,"std::fs"],[2533,"console::utils"],[2534,"indicatif::progress_bar"],[2535,"alloc::borrow"],[2536,"core::marker"],[2537,"core::ops::function"],[2538,"core::time"],[2539,"node_semver::range"],[2540,"serde::de"]],"d":["","","","Events for the sessions in executables and shims and …","Provides utilities for operating on the filesystem.","Provides types for working with Volta hooks.","Provides types for working with Volta’s inventory, the …","","This module provides a custom Logger implementation for …","","","Provides the Project
type, which represents a Node project …","","Provides the Session
type, which represents the user’s …","Provides utilities for modifying shims for 3rd-party …","","The view layer of Volta, with utilities for styling …","Inter-process locking on the Volta directory","","","","","Thrown when package tries to install a binary that is …","Thrown when executing an external binary fails","Thrown when a binary could not be found in the local …","Thrown when building the virtual environment path fails","Thrown when unable to launch a command with VOLTA_BYPASS …","Thrown when a user tries to volta fetch
something other …","Thrown when a user tries to volta pin
something other than …","Thrown when the Completions out-dir is not a directory","Package configuration is missing or incorrect.","Thrown when the containing directory could not be …","Trait providing the with_context method to easily convert …","","Thrown when unable to start the migration executable","","Thrown when unable to create the layout file","Thrown when unable to create a link to the shared global …","Thrown when creating a temporary directory fails","Thrown when creating a temporary file fails","","Thrown when deleting a directory fails","Thrown when deleting a file fails","","","A required environment variable was unset or invalid.","Contains the error value","","The requested executable is not available.","Thrown when unable to execute a hook command","The requested executable could not be run.","Exit codes supported by Volta Errors","Thrown when volta.extends
keys result in an infinite cycle","Thrown when determining the path to an extension manifest …","","A file could not be read or written.","Thrown when a hook command returns a non-zero exit code","Thrown when a hook contains multiple fields (prefix, …","Thrown when a hook doesn’t contain any of the known …","Thrown when determining the path to a hook fails","","Thrown when determining the name of a newly-installed …","An invalid combination of command-line arguments was …","","Thrown when output from a hook command could not be read","Thrown when a user does e.g. volta install node 12
instead …","Thrown when a user does e.g. volta install 12
instead of …","Thrown when a format other than “npm” or “github” …","Thrown when a tool name is invalid per npm’s rules.","Thrown when unable to acquire a lock on the Volta directory","A network error occurred.","Thrown when pinning or installing npm@bundled and couldn’…","Thrown when pnpm is not set at the command-line","Thrown when Yarn is not set at the command-line","Thrown when a user tries to install a Yarn or npm version …","Thrown when default pnpm is not set","Thrown when default Yarn is not set","","Thrown when the install dir could not be determined","","Thrown when a user tries to pin a npm, pnpm, or Yarn …","Thrown when the platform (Node version) could not be …","Thrown when parsing the project manifest and there is a …","Thrown when pnpm is not set in a project","Thrown when Yarn is not set in a project","Thrown when no shell profiles could be found","No match could be found for the requested version string.","Thrown when there is no Node version matching a requested …","Thrown when the user tries to pin Node or Yarn versions …","The command or feature is not yet implemented.","Thrown when npm link
is called with a package that isn’t …","Thrown when npm link
is called with a package that was not …","Thrown when there is no npm version matching the requested …","","Contains the success value","Thrown when the command to install a global package is not …","Thrown when parsing the package manifest fails","Thrown when reading the package manifest fails","Thrown when a specified package could not be found on the …","Thrown when parsing a package manifest fails","Thrown when reading a package manifest fails","Thrown when a package has been unpacked but is not formed …","Thrown when writing a package manifest fails","Thrown when unable to parse a bin config file","Thrown when unable to parse a hooks.json file","Thrown when unable to parse the node index cache","Thrown when unable to parse the node index","Thrown when unable to parse the node index cache expiration","Thrown when unable to parse the npm manifest file from a …","Thrown when unable to parse a package configuration","Thrown when unable to parse the platform.json file","Thrown when unable to parse a tool spec (<tool>[@<version>]
…","Thrown when persisting an archive to the inventory fails","Thrown when there is no pnpm version matching a requested …","Thrown when executing a project-local binary fails","Thrown when a project-local binary could not be found","Thrown when a publish hook contains both the url and bin …","Thrown when a publish hook contains neither url nor bin …","Thrown when there was an error reading the user bin …","Thrown when there was an error reading the config for a …","Thrown when unable to read the default npm version file","Thrown when unable to read the contents of a directory","Thrown when there was an error opening a hooks.json file","Thrown when there was an error reading the Node Index Cache","Thrown when there was an error reading the Node Index …","Thrown when there was an error reading the npm manifest …","Thrown when there was an error reading a package …","Thrown when there was an error opening the user platform …","Thrown when the public registry for Node or Yarn could not …","Thrown when the shim binary is called directly, not …","Thrown when there was an error setting a tool to executable","Thrown when there was an error copying an unpacked tool to …","Thrown when Volta is unable to create a shim","Thrown when Volta is unable to remove a shim","Thrown when serializing a bin config to JSON fails","Thrown when serializing a package config to JSON fails","Thrown when serializing the platform to JSON fails","No error occurred.","Thrown when a given feature has not yet been implemented","An unknown error occurred.","Thrown when unpacking an archive (tarball or zip) fails","Thrown when a package to upgrade was not found","Thrown when a package to upgrade was installed with a …","","Error type for Volta","Thrown when there was an error writing a bin config file","Thrown when there was an error writing the default npm to …","Thrown when there was an error writing the npm launcher","Thrown when there was an error writing the node index cache","Thrown when there was an error writing the node index …","Thrown when there was an error writing a package config","Thrown when writing the platform.json file fails","Thrown when a user attempts to install a version of Yarn2","Thrown when there is an error fetching the latest version …","Thrown when there is no Yarn version matching a requested …","","","","","","","","","","The exit code Volta should use when this error stops …","","","","","Returns the argument unchanged.","","Returns the argument unchanged.","Returns the argument unchanged.","Create a new VoltaError instance including a source error","","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","","Get a reference to the ErrorKind for this error","","Report an error, both to the console and to error logs","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","Thrown when package tries to install a binary that is …","Thrown when executing an external binary fails","Thrown when a binary could not be found in the local …","Thrown when building the virtual environment path fails","Thrown when unable to launch a command with VOLTA_BYPASS …","Thrown when a user tries to volta fetch
something other …","Thrown when a user tries to volta pin
something other than …","Thrown when the Completions out-dir is not a directory","Thrown when the containing directory could not be …","","Thrown when unable to start the migration executable","","Thrown when unable to create the layout file","Thrown when unable to create a link to the shared global …","Thrown when creating a temporary directory fails","Thrown when creating a temporary file fails","","Thrown when deleting a directory fails","Thrown when deleting a file fails","","","","Thrown when unable to execute a hook command","Thrown when volta.extends
keys result in an infinite cycle","Thrown when determining the path to an extension manifest …","Thrown when a hook command returns a non-zero exit code","Thrown when a hook contains multiple fields (prefix, …","Thrown when a hook doesn’t contain any of the known …","Thrown when determining the path to a hook fails","Thrown when determining the name of a newly-installed …","","Thrown when output from a hook command could not be read","Thrown when a user does e.g. volta install node 12
instead …","Thrown when a user does e.g. volta install 12
instead of …","Thrown when a format other than “npm” or “github” …","Thrown when a tool name is invalid per npm’s rules.","Thrown when unable to acquire a lock on the Volta directory","Thrown when pinning or installing npm@bundled and couldn’…","Thrown when pnpm is not set at the command-line","Thrown when Yarn is not set at the command-line","Thrown when a user tries to install a Yarn or npm version …","Thrown when default pnpm is not set","Thrown when default Yarn is not set","","Thrown when the install dir could not be determined","","Thrown when a user tries to pin a npm, pnpm, or Yarn …","Thrown when the platform (Node version) could not be …","Thrown when parsing the project manifest and there is a …","Thrown when pnpm is not set in a project","Thrown when Yarn is not set in a project","Thrown when no shell profiles could be found","Thrown when there is no Node version matching a requested …","Thrown when the user tries to pin Node or Yarn versions …","Thrown when npm link
is called with a package that isn’t …","Thrown when npm link
is called with a package that was not …","Thrown when there is no npm version matching the requested …","","","Thrown when the command to install a global package is not …","Thrown when parsing the package manifest fails","Thrown when reading the package manifest fails","Thrown when a specified package could not be found on the …","Thrown when parsing a package manifest fails","Thrown when reading a package manifest fails","Thrown when a package has been unpacked but is not formed …","Thrown when writing a package manifest fails","Thrown when unable to parse a bin config file","Thrown when unable to parse a hooks.json file","Thrown when unable to parse the node index cache","Thrown when unable to parse the node index","Thrown when unable to parse the node index cache expiration","Thrown when unable to parse the npm manifest file from a …","Thrown when unable to parse a package configuration","Thrown when unable to parse the platform.json file","Thrown when unable to parse a tool spec (<tool>[@<version>]
…","Thrown when persisting an archive to the inventory fails","Thrown when there is no pnpm version matching a requested …","Thrown when executing a project-local binary fails","Thrown when a project-local binary could not be found","Thrown when a publish hook contains both the url and bin …","Thrown when a publish hook contains neither url nor bin …","","Thrown when there was an error reading the user bin …","Thrown when there was an error reading the config for a …","Thrown when unable to read the default npm version file","Thrown when unable to read the contents of a directory","Thrown when there was an error opening a hooks.json file","Thrown when there was an error reading the Node Index Cache","Thrown when there was an error reading the Node Index …","Thrown when there was an error reading the npm manifest …","Thrown when there was an error reading a package …","Thrown when there was an error opening the user platform …","Thrown when the public registry for Node or Yarn could not …","Thrown when the shim binary is called directly, not …","Thrown when there was an error setting a tool to executable","Thrown when there was an error copying an unpacked tool to …","Thrown when Volta is unable to create a shim","Thrown when Volta is unable to remove a shim","Thrown when serializing a bin config to JSON fails","Thrown when serializing a package config to JSON fails","Thrown when serializing the platform to JSON fails","Thrown when a given feature has not yet been implemented","Thrown when unpacking an archive (tarball or zip) fails","Thrown when a package to upgrade was not found","Thrown when a package to upgrade was installed with a …","","Thrown when there was an error writing a bin config file","Thrown when there was an error writing the default npm to …","Thrown when there was an error writing the npm launcher","Thrown when there was an error writing the node index cache","Thrown when there was an error writing the node index …","Thrown when there was an error writing a package config","Thrown when writing the platform.json file fails","Thrown when a user attempts to install a version of Yarn2","Thrown when there is an error fetching the latest version …","Thrown when there is no Yarn version matching a requested …","","","","","","Returns the argument unchanged.","Calls U::from(self)
.","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","Combines all the arguments into a single String","","Report an error, both to the console and to error logs","Write an error log with all details about the error","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","Returns the argument unchanged.","Returns the argument unchanged.","Returns the argument unchanged.","Returns the argument unchanged.","","Constructs a new ‘EventLog’","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","Creates a staging directory in the Volta tmp directory","Creates a NamedTempFile in the Volta tmp directory","Reads the contents of a directory and returns a Vec of the …","Converts a failure because of file not found into a …","Reads the full contents of a directory, eagerly extracting …","Reads a file, if it exists.","Removes the target directory, if it exists. If the …","Removes the target file, if it exists. If the file doesn’…","Rename a file or directory to a new name, retrying if the …","Ensure that a given file has ‘executable’ permissions, …","Create a directory symlink. The dst
path will be a …","Create a file symlink. The dst
path will be a symbolic …","Opens a file, creating it if it doesn’t exist","Reports an event by forking a process and sending the …","Volta hooks related to events.","","Volta hook configuration","Lazily loaded Volta hook configuration","","A hook for publishing Volta events.","Format of the registry used for Yarn (Npm or Github)","Volta hooks for an individual tool","Reports an event by sending a POST request to a URL.","Volta hooks for Yarn","","","","","","","","","","","","","","","Returns the current hooks, which are a merge between the …","The hook for resolving the URL for a distro version","The hook for resolving the URL for a distro version","","","","","","","","","","","","","","","Returns the argument unchanged.","Returns the argument unchanged.","Returns the argument unchanged.","Returns the argument unchanged.","Returns the argument unchanged.","Returns the argument unchanged.","Returns the argument unchanged.","","Returns the merged hooks loaded from an iterator of …","","Forces the loading of the hook configuration from both …","The hook for resolving the Tool Index URL","The hook for resolving the Tool Index URL","Constructs a new LazyHookConfig
","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","The hook for resolving the URL for the latest version","The hook for resolving the URL for the latest version","Merges this HookConfig with another, giving precedence to …","Extends this ToolHooks with another, giving precendence to …","Extends this YarnHooks with another, giving precendence to …","Merges this EventHooks with another, giving precedence to …","","","","","","","","","The hook for publishing events, if any.","","","Types representing Volta Tool Hooks.","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","Returns the argument unchanged.","Returns the argument unchanged.","Returns the argument unchanged.","Returns the argument unchanged.","Returns the argument unchanged.","Returns the argument unchanged.","Returns the argument unchanged.","","","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","A hook for resolving the distro URL for a given tool …","","","A hook for resolving the URL for metadata about a tool","","","","","","","","","A hook for resolving the URL for the Yarn index","","","","","","","Use the expected filename to determine the extension for …","","","","","","","","","","","","","","","","Execute a shell command and return the trimmed stdout from …","","","","","Returns the argument unchanged.","Returns the argument unchanged.","Returns the argument unchanged.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","","Performs resolution of the distro URL based on the given …","Performs resolution of the metadata URL based on the given …","Performs resolution of the metadata URL based on the given …","","","","","","","","","","","","","","","","","Checks if a given Node version image is available on the …","Collects a set of all Node versions fetched on the local …","Checks if a given npm version image is available on the …","Collects a set of all npm versions fetched on the local …","Collects a set of all Package Configs on the local machine","Checks if a given pnpm version image is available on the …","Collects a set of all pnpm versions fetched on the local …","Reads the contents of a directory and returns the set of …","Checks if a given Yarn version image is available on the …","Collects a set of all Yarn versions fetched on the local …","","","Determine the binary install directory from the currently …","","","","","","","","","","Represents the context from which the logger was created","Represents the level of verbosity that was requested by …","","","","Log messages from the migration","","","","Log messages from one of the shims","","","","Log messages from the volta
executable","","","","","","","","","","","","","","","Returns the argument unchanged.","Returns the argument unchanged.","Returns the argument unchanged.","Initialize the global logger with a Logger instance Will …","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","","Determines the correct logging level based on the …","","","","","","","","","","","","","","","","","","Wraps the supplied content to the terminal width, if we …","Send event to the spawned command process","","","Represents a version from a pinned Binary platform","Represents a (maybe) platform with values from the command …","Represents a version from the command line (via volta run
)","Represents a version from the user default platform","A platform image.","","Represents 3 possible states: Having a value, not having a …","","Represents a real Platform, with Versions pulled from one …","Represents the specification of a single Platform, …","Represents a version from a project manifest","","The source with which a version is associated","","A lightweight namespace type representing the system …","Convert this PlatformSpec into a Platform with all sources …","Convert this PlatformSpec into a Platform with all sources …","Convert this PlatformSpec into a Platform with all sources …","","","","","","","","","","","","","","","Check out a Platform
into a fully-realized Image
","","","","","","","","","","","","","","","","Returns the user’s currently active platform, if any","","","","","","","","Returns the argument unchanged.","Returns the argument unchanged.","Returns the argument unchanged.","Returns the argument unchanged.","Returns the argument unchanged.","Returns the argument unchanged.","","Converts the InheritOption
into a regular Option
by …","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Applies a function to the contained value (if any)","Merges the CliPlatform
with a Platform
, inheriting from …","The pinned version of Node.","","","","The custom version of npm, if any. None
represents using …","","","","","The pinned version of pnpm, if any.","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","The pinned version of Yarn, if any.","","","","A platform image.","","","","Returns the argument unchanged.","Calls U::from(self)
.","The pinned version of Node.","The custom version of npm, if any. None
represents using …","Produces a modified version of the current PATH
…","The pinned version of pnpm, if any.","Determines the sourced version of npm that will be …","","","","","The pinned version of Yarn, if any.","A lightweight namespace type representing the system …","","","Returns the argument unchanged.","Calls U::from(self)
.","Produces a modified version of the current PATH
…","","","","","A lazily loaded Project","","A Node project workspace in the filesystem","","","","","","","","Searches the project roots to find the path to a …","Starts at base_dir
and walks up the directory tree until a …","Creates an optional Project instance from the current …","Creates an optional Project instance from the specified …","Returns the argument unchanged.","Returns the argument unchanged.","Returns the argument unchanged.","Creates a Project instance from the given package manifest …","","","Returns true if the input binary name is a direct …","Returns true if the project dependency map contains the …","","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","","","","","Returns a reference to the manifest file for the current …","","","Yarn projects that are using PnP or pnpm linker need to …","","","Pins the Node version in this project’s manifest file","Pins the npm version in this project’s manifest file","Pins the pnpm version in this project’s manifest file","Pins the Yarn version in this project’s manifest file","Returns a reference to the Project’s PlatformSpec
, if …","","","","","","","","","","","","","","","","","","Returns an iterator of paths to all of the workspace roots","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","Returns the argument unchanged.","Returns the argument unchanged.","Returns the argument unchanged.","Returns the argument unchanged.","","","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","","","Moves the tool versions into a PartialPlatform
and returns …","","","","","","","","","","","","","","","","","Updates the volta
hash in the specified manifest with the …","","","","","","","Environment variable set internally when a shim has been …","","","Write a debug message with the full image that will be …","Write a debug message that there is no platform available","Execute a shim command, based on the command-line …","Execute a tool with the provided arguments","","","Get the appropriate Tool command, based on the requested …","Determine the name of the command to run by inspecting the …","","","","","","","","Information about the location and execution context of …","","","","Determine the correct command to run for a 3rd-party binary","Determine the execution context (PATH and failure error …","Returns the argument unchanged.","","Load information about a default binary by name, if …","Calls U::from(self)
.","Determine the execution context (PATH and failure error …","","Determine the value for NODE_PATH, with the shared lib …","","","","","","","","","Executor for running an internal install (installing Node, …","","","","","","Process builder for launching a package install command …","","Process builder for launching a npm link <package>
command","","Process builder for launching a global package upgrade …","","","","Process builder for launching a Volta-managed tool","The kind of tool being executed, used to determine the …","","Executor for running a tool uninstall command.","","","","","","","","","","","","","","","","","","Check for possible failure cases with the linked package: …","","Updates the Platform for the command to include values …","Updates the Platform for the command to include values …","Updates the Platform for the command to include values …","Updates the Platform for the command to include values …","","The command that will ultimately be executed","The command that will ultimately be executed","The command that will ultimately be executed","Adds or updates a single environment variable that the …","","Adds or updates environment variables that the command …","Adds or updates environment variables that the command …","Adds or updates environment variables that the command …","Adds or updates environment variables that the command …","","Runs the command, returning the ExitStatus
if it …","Runs the install command, applying the necessary …","Runs the link command, applying the necessary …","Runs the upgrade command, applying the necessary …","Runs the install, using Volta’s internal install logic …","Runs the uninstall with Volta’s internal uninstall logic","","","","","","","","Returns the argument unchanged.","","Returns the argument unchanged.","Returns the argument unchanged.","Returns the argument unchanged.","Returns the argument unchanged.","Returns the argument unchanged.","Returns the argument unchanged.","Returns the argument unchanged.","The installer that modifies the command as necessary and …","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","","","","","","","","","The platform to use when running the command.","The platform to use when running the command","The platform to run the command under","The tool the user wants to link","","","","","","","","","","","","","","","","","","","","","","","","","","","Helper utility to modify the command and provide the …","","","","","","","","","Build a ToolCommand
for Node","Determine the execution context (PATH and failure error …","Build an Executor
for npm","Determine the name of the current project, if possible","Determine the execution context (PATH and failure error …","","Build a ToolCommand
for npx","Determine the execution context (PATH and failure error …","","","","","The arguments passed to a global install command","","An intercepted local command","","The arguments passed to an npm link
command","Aliases that npm supports for the ‘install’ command","Aliases that npm supports for the ‘link’ command","Aliases that npm supports for the ‘uninstall’ command","Aliases that npm supports for the update
command","Aliases that pnpm supports for the ‘link’ command see: …","Aliases that pnpm supports for the ‘remove’ command, …","Aliases that pnpm supports for the ‘update’ command, …","","","","The list of tools passed to an uninstall command","","","The list of tools passed to an upgrade command","","","","","","","","","","","","","","","Common arguments that apply to each tool (e.g. flags)","Common arguments that apply to each tool (e.g. flags)","The common arguments that apply to each tool","","Convert these global install arguments into an executor …","Convert the tools into an executor for the uninstall …","Convert these global upgrade arguments into an executor …","","Build an executor to upgrade all global packages that were …","Parse the given set of arguments to see if they correspond …","Parse the given set of arguments to see if they correspond …","Parse the given set of arguments to see if they correspond …","Returns the argument unchanged.","Returns the argument unchanged.","Returns the argument unchanged.","Returns the argument unchanged.","Returns the argument unchanged.","Returns the argument unchanged.","Returns the argument unchanged.","Check if the provided argument list includes a global flag …","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","","","The package manager being used","The package manager being used","The individual tool arguments","","The individual tool arguments","The list of tools to link (if any)","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","Determine the execution context (PATH and failure error …","","Build an Executor
for Yarn","Determine the execution context (PATH and failure error …","","","","","","","","","","","","","","","","","","Represents the user’s state during an execution of a …","","","","","","","","","","","","","","","","","","","","","Returns the user’s default platform, if any","","","","","","","","","","Returns the argument unchanged.","Returns the argument unchanged.","Produces a reference to the hook configuration","","Constructs a new Session
.","Calls U::from(self)
.","Calls U::from(self)
.","","Produces a reference to the current Node project, if any.","","Produces a mutable reference to the current Node project, …","Returns the current project’s pinned platform image, if …","","","","Produces a reference to the current toolchain (default …","","Produces a mutable reference to the current toolchain","","","","","","","","","","","","","","","","","","","","","","","Returns the argument unchanged.","","Calls U::from(self)
.","Unix-specific shim utilities","","","","","","","","","","","","","","Determines the string to display based on the Origin of …","Format the underlying cause of an error","Generate the styled prefix for a note","Constructs a command-line progress bar based on the …","Constructs a command-line progress spinner with the …","Generate the styled prefix for a success message","Get the width of the terminal, limited to a maximum of …","","","","The current state of locks for this process.","An RAII implementation of a process lock on the Volta …","","","","","","","","","","Returns the argument unchanged.","Returns the argument unchanged.","Calls U::from(self)
.","Calls U::from(self)
.","","","","","","","","","","","","","Represents the result of checking if a tool is available …","","","","","","","","","","","","Details about a package in the npm Registry","","","","Specification for a tool and its associated version.","Trait representing all of the actions that can be taken …","","","","","","","Check the args for the bad patterns of","Uses the supplied already_fetched
predicate to determine …","Check if a newly-installed shim is first on the PATH. If …","","","Fetch a Tool into the local inventory","Locate the base directory for the relevant shim in the …","","","Returns the argument unchanged.","Returns the argument unchanged.","","Get a valid, sorted Vec<Spec>
given a Vec<String>
.","","","","","Install a tool, making it the default so it is available …","Calls U::from(self)
.","Calls U::from(self)
.","","The name of the tool, without the version, used for …","","","","Pin a tool in the local project so that it is usable …","","","","Resolve a tool spec into a fully realized Tool that can be …","","Compare Spec
s for sorting when converting from strings","","","","Try to parse a tool and version from a string like `[@].","","","","","Uninstall a tool, removing it from the local inventory","","","","","The architecture component of a Node distro filename","The extension for Node distro files","The file identifier in the Node index files
array","The OS component of a Node distro filename","The Tool implementation for fetching and installing Node","A full Node version including not just the version of Node …","","","","","","","","","","Provides fetcher for Node distributions","","","","","Returns the argument unchanged.","Returns the argument unchanged.","","Calls U::from(self)
.","Calls U::from(self)
.","Load the local npm version file to determine the default …","","","The npm version globally installed with the Node distro.","","Provides resolution of Node requirements into specific …","","The version of Node itself.","","","","","","","","","","","","","The portion of npm’s package.json
file that we care about","","","","Determine the remote URL to download from, using the hooks …","","Fetch the distro archive from the internet","Returns the argument unchanged.","Calls U::from(self)
.","Return the archive if it is valid. It may have been …","Load the local npm version file to determine the default …","","","Save the default npm version to the filesystem for a given …","","","","Unpack the node archive into the image directory so that …","Parse the version out of a package.json file","","","","The index of the public Node server.","","","","","","","","","","","","","","","","Returns the argument unchanged.","","Returns the argument unchanged.","Returns the argument unchanged.","Returns the argument unchanged.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","","","","","","","","","","","","","","","","","","","","","","","","Get the cache max-age of an HTTP response.","Returns the URL of the index of available Node versions on …","Reads a public index from the Node cache, if it exists and …","","","","","","The Tool implementation for setting npm to the version …","The Tool implementation for fetching and installing npm","","","","","","","","Provides fetcher for npm distributions","","","","","Returns the argument unchanged.","Returns the argument unchanged.","","","Calls U::from(self)
.","Calls U::from(self)
.","","","","Provides resolution of npm Version requirements into …","","","","","","","","","","","","","Determine the remote URL to download from, using the hooks …","","Fetch the distro archive from the internet","Return the archive if it is valid. It may have been …","Overwrite the launcher script","Unpack the npm archive into the image directory so that it …","","","","","Configuration information about a single installed binary …","Helper struct for direct installs through npm i -g
or …","Helper struct for direct in-place upgrades using …","","","","The Tool implementation for installing 3rd-party global …","Configuration information about an installed package","The package manager used to install a given package","The relevant information we need out of a package’s …","","","","The bin
section, containing a map of binary names to …","The binaries installed by this package","","","","","","","","","Check for possible failure cases with the package to be …","","","","","","","","","","","Returns the argument unchanged.","Returns the argument unchanged.","Returns the argument unchanged.","","Returns the argument unchanged.","","","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","","","","","The package manager that was used to install this package","The package manager used to install this binary","","","","The package name","The binary name","The name of the package","","","","","The package that installed the binary","","","The platform used to install this package","The platform used to install this binary","","","","Create the temporary staging directory we will use to …","","","","","","","","","","","","","","","","","","Uninstalls the specified package.","","The package version","The package version","The version of the package","","","","","","Read the manifest for the package being installed","Validate that we aren’t attempting to install a bin that …","Generate configuration files and shims for the package and …","Use npm install --global
to install the package","","The package manager used to install a given package","","","Given the package_root
, returns the directory where …","","","","","","","","","","","","","","Returns the argument unchanged.","Determine the name of the package that was installed into …","Determine the package name for an npm global install","Determine the package name for a pnpm or Yarn global …","Return the name of the single subdirectory (if any) to the …","Calls U::from(self)
.","","","Modify a given Command
to be set up for global installs, …","Given the package_root
, returns the directory where the …","Given the package_root
, returns the root of the source …","","","","","","Configuration information about a single installed binary …","Struct to read the dependencies
out of Yarn’s global …","Configuration information about an installed package","The relevant information we need out of a package’s …","","The bin
section, containing a map of binary names to …","The binaries installed by this package","","","","","","","","","","","","","Determine the default binary name from the package name","","","","","","","","","","","","Parse the package.json
for a given package directory","Returns the argument unchanged.","Returns the argument unchanged.","Returns the argument unchanged.","Returns the argument unchanged.","Returns the argument unchanged.","Parse a PackageConfig
instance from a config file","Parse a BinConfig
instance from the given config file","","","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","The package manager that was used to install this package","The package manager used to install this binary","The package name","The binary name","The name of the package","","","The package that installed the binary","","The platform used to install this package","The platform used to install this binary","","","","","","","","","","","","","","","","","","","","","The package version","The package version","The version of the package","","","","","","Write this PackageConfig
into the appropriate config file","Write this BinConfig
to the appropriate config file","","","","","","","","Returns the argument unchanged.","Calls U::from(self)
.","","","","","","","Reads the contents of a directory and returns a Vec …","Remove a shim and its associated configuration file","Remove the link to the package in the shared lib directory","Uninstalls the specified package.","The Tool implementation for fetching and installing pnpm","","","","","","Provides fetcher for pnpm distributions","","","Returns the argument unchanged.","","Calls U::from(self)
.","","","","","","","","","","","Determine the remote URL to download from, using the hooks …","","Fetch the distro archive from the internet","Return the archive if it is valid. It may have been …","Unpack the pnpm archive into the image directory so that …","Create executable launchers for the pnpm and pnpx binaries","Fetch the index of available pnpm versions from the npm …","","","","","Details about a package in the npm Registry","Index of versions of a specific package from the npm …","","Package Metadata Response","","","","","","","","","","","","","","","","","","","","","Figure out the unpacked package directory name dynamically","","","","","Returns the argument unchanged.","","Returns the argument unchanged.","Returns the argument unchanged.","Returns the argument unchanged.","Returns the argument unchanged.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","Determine if a given string is “version-like”.","The Tool implementation for fetching and installing Yarn","","","","","","Provides fetcher for Yarn distributions","","","Returns the argument unchanged.","","Calls U::from(self)
.","","","","Provides resolution of Yarn requirements into specific …","","","","","","","","Determine the remote URL to download from, using the hooks …","","","Fetch the distro archive from the internet","Return the archive if it is valid. It may have been …","Unpack the yarn archive into the image directory so that …","","","","The public Yarn index.","The GitHub API provides a list of assets. Some Yarn …","","","","","","","","","","","","","","Returns the argument unchanged.","Returns the argument unchanged.","Returns the argument unchanged.","Returns the argument unchanged.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Is this entry a full release, i.e., does this entry’s …","The filename of an asset included in a Yarn GitHub release.","Yarn releases are given a tag name of the form “v$version…","","","","","","","","","","","","","","","","","","","","","","","","","","Lazily loaded toolchain","","","","","","","Returns the argument unchanged.","Returns the argument unchanged.","Forces loading of the toolchain and returns an immutable …","Forces loading of the toolchain and returns a mutable …","Creates a new LazyToolchain
","Calls U::from(self)
.","Calls U::from(self)
.","","","","","Set the active Node version in the default platform file.","Set the active Npm version in the default platform file.","Set the active pnpm version in the default platform file.","Set the active Yarn version in the default platform file.","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","Returns the argument unchanged.","Returns the argument unchanged.","Calls U::from(self)
.","Calls U::from(self)
.","Serialize the Platform to a JSON String","","","","","","","","","","","","","","","","","","An arbitrary tag version","Exact Version","The ‘latest’ tag, a special case that exists for all …","The ‘lts’ tag, a special case for Node","No version specified (default)","SemVer Range","Arbitrary Version Tag","","","","","","","","","","","","Returns the argument unchanged.","Returns the argument unchanged.","","","","Calls U::from(self)
.","Calls U::from(self)
.","","","","","","","","","","","","","","","","","","","","","","Returns the argument unchanged.","Calls U::from(self)
.","","","","","","","","","","","","","","Returns the argument unchanged.","Calls U::from(self)
.","","","","","",""],"i":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,11,11,11,11,11,11,11,11,4,11,0,11,11,11,11,11,11,11,11,11,11,11,11,4,20,0,4,11,4,0,11,11,0,4,11,11,11,11,0,11,4,11,11,11,11,11,11,11,4,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,4,11,11,4,11,11,11,11,20,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,4,11,4,11,11,11,11,0,11,11,11,11,11,11,11,11,11,11,7,10,4,7,10,4,4,4,4,7,7,7,10,4,7,7,10,4,7,7,7,10,4,0,7,10,0,0,7,10,4,7,7,10,4,7,10,4,7,10,4,7,10,4,172,173,174,175,176,177,178,175,179,180,181,182,183,184,185,186,187,188,189,190,191,192,176,193,177,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,173,193,229,230,177,231,232,233,234,235,236,237,238,221,222,239,240,241,192,215,242,243,244,217,245,190,246,247,248,173,174,249,190,246,250,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,0,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,0,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,0,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,173,174,175,176,177,178,175,179,180,181,182,183,184,185,186,187,188,189,190,191,192,176,193,177,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,173,193,229,230,177,231,232,233,234,235,236,237,238,221,222,239,240,241,192,215,242,243,244,217,245,190,246,247,248,173,174,249,190,246,250,0,0,0,0,24,24,24,0,0,0,0,24,24,23,23,23,23,23,23,29,23,27,29,24,23,27,29,24,27,29,24,29,24,29,29,29,29,24,24,24,24,27,23,29,29,24,23,27,29,24,0,23,23,27,29,24,24,27,29,29,29,23,27,29,24,27,23,27,29,24,23,27,29,24,23,27,29,24,0,23,27,29,24,251,252,252,253,252,254,0,0,0,0,0,0,0,0,0,0,0,0,0,31,0,46,0,0,46,0,0,0,31,0,49,45,50,52,47,31,46,49,45,50,52,47,31,46,45,50,52,31,46,31,31,31,31,46,46,46,46,45,45,31,46,49,45,50,52,47,31,46,45,45,46,49,50,52,49,49,45,50,52,47,31,46,50,52,45,50,52,47,0,45,45,45,45,50,45,45,47,0,49,0,49,45,50,52,47,47,31,31,46,49,45,50,52,47,31,46,49,45,50,52,47,31,46,49,45,50,52,47,31,46,45,45,0,0,0,0,0,0,0,58,59,57,58,59,57,60,56,61,62,58,59,57,60,56,61,62,58,59,57,60,56,61,62,61,62,60,59,58,59,57,60,56,61,62,61,62,58,59,57,60,56,61,62,58,58,60,59,58,61,62,61,62,60,60,61,60,58,59,56,58,59,57,60,56,61,62,58,59,58,59,57,60,56,61,62,58,59,57,60,56,61,62,58,59,57,60,56,61,62,57,58,59,57,60,56,61,62,60,0,63,65,0,0,0,0,0,63,65,0,0,63,65,0,0,63,65,64,63,65,64,0,63,65,64,63,63,63,63,65,65,65,65,64,64,64,64,0,63,65,64,64,63,65,64,63,65,64,64,63,65,64,63,65,64,63,65,64,63,65,64,63,65,64,255,255,256,256,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,71,0,0,0,0,0,0,74,71,0,0,74,0,71,71,74,0,0,74,72,71,74,72,71,71,71,72,72,72,71,74,72,71,72,74,72,71,72,0,72,72,72,72,71,74,72,71,74,72,71,74,72,71,74,72,71,0,0,0,0,87,0,87,87,0,88,0,88,0,0,87,88,0,0,0,81,81,81,83,83,87,88,81,89,82,83,87,88,81,89,82,0,82,83,87,88,81,89,82,83,87,88,81,89,82,83,81,81,82,88,81,81,81,81,81,87,83,87,88,81,89,82,0,88,83,87,88,81,89,82,88,89,85,81,89,82,85,81,89,82,81,85,81,89,82,83,0,83,87,88,81,89,82,87,83,87,88,81,81,89,82,83,87,88,81,89,82,83,87,88,81,89,82,83,83,87,88,81,89,82,83,83,83,83,85,81,89,82,0,85,85,85,85,85,85,85,85,85,85,85,85,85,85,85,0,257,257,257,257,257,257,257,257,257,0,0,0,93,44,91,93,44,91,44,44,0,44,44,93,44,91,44,93,93,44,44,93,93,44,91,0,0,0,0,44,44,91,44,91,91,44,44,44,44,44,44,91,93,0,93,44,91,93,44,91,93,44,91,93,44,91,44,44,91,0,0,0,96,96,96,0,0,96,258,258,97,96,95,94,97,96,95,94,94,95,97,95,94,95,97,94,96,97,96,95,94,97,95,97,96,95,94,94,94,94,97,94,94,96,97,96,95,94,97,96,95,94,97,96,95,94,0,95,97,96,95,94,94,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,103,103,103,0,0,103,103,103,103,0,103,0,103,103,103,103,110,110,0,100,0,100,110,110,110,100,0,100,0,100,0,110,110,100,0,0,100,0,110,100,105,110,106,104,107,108,109,100,105,110,106,104,107,108,109,104,100,105,106,104,107,105,106,104,107,105,100,105,106,104,107,100,105,106,104,107,108,109,106,100,100,100,100,100,100,100,100,105,110,106,104,107,108,109,106,100,105,110,106,104,107,108,109,105,105,106,104,107,108,109,105,106,104,107,104,108,109,100,105,110,106,104,107,108,109,100,105,110,106,104,107,108,109,100,105,110,106,104,107,108,109,107,100,105,110,106,104,107,108,109,0,0,0,0,0,0,0,0,0,118,0,113,0,118,0,259,0,0,0,0,0,0,0,0,118,0,113,0,259,113,0,118,113,114,115,116,259,117,118,113,114,115,116,259,117,114,116,117,113,114,115,116,117,116,118,118,118,118,113,114,115,116,259,117,0,118,113,114,115,116,259,117,0,0,114,116,114,115,116,117,118,113,114,115,116,259,117,118,113,114,115,116,259,117,118,113,114,115,116,259,117,118,113,114,115,116,259,117,0,0,0,0,0,0,0,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,0,25,25,25,25,25,25,25,25,84,84,84,84,84,25,84,25,25,25,25,25,84,25,25,25,25,25,84,84,84,25,84,25,84,84,84,84,25,25,84,84,84,84,84,25,25,84,84,84,84,25,84,25,84,25,84,25,121,121,121,121,0,121,121,0,0,121,121,121,121,121,121,0,121,0,0,121,121,121,121,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,131,131,260,131,260,131,260,131,260,260,131,260,131,260,131,260,131,260,131,260,131,132,0,0,132,0,0,0,0,0,112,0,112,0,0,112,0,0,0,0,112,0,0,0,112,132,112,132,112,112,0,0,0,0,51,0,112,112,132,112,112,112,0,0,0,0,51,132,112,0,112,0,0,0,51,0,0,0,112,0,112,112,132,112,112,132,112,132,112,112,159,132,112,0,0,0,0,0,0,0,53,53,53,135,53,135,135,135,53,0,53,53,135,135,53,135,53,53,135,0,0,53,135,53,0,0,135,135,53,135,53,135,53,135,53,135,53,53,135,0,136,136,136,0,0,0,136,136,0,0,0,0,0,136,136,136,0,136,136,136,0,0,0,0,141,140,138,139,141,140,138,139,138,139,141,139,140,141,141,140,138,139,141,140,138,139,140,139,0,139,141,140,138,139,141,140,138,139,141,140,138,139,140,139,141,140,138,139,0,0,0,0,0,0,0,0,0,0,0,54,54,54,145,54,145,54,0,54,145,54,145,54,145,54,145,54,145,54,54,145,0,0,54,145,54,145,54,145,54,145,54,54,145,0,0,0,0,0,0,0,0,0,0,0,0,0,0,148,111,0,0,0,0,111,111,148,150,68,149,151,147,148,149,151,147,148,147,148,148,149,151,147,0,147,148,149,149,149,151,147,148,148,0,149,149,151,147,148,0,0,151,147,68,102,0,149,151,68,102,150,149,151,147,147,102,0,149,68,102,149,151,147,0,149,151,148,149,149,151,147,148,149,151,147,148,149,151,147,148,0,0,149,68,102,150,149,151,147,148,151,0,0,0,0,111,0,111,111,111,111,111,111,111,111,111,111,111,111,111,111,111,111,111,111,0,0,0,111,111,111,111,111,111,111,111,111,111,111,0,0,0,0,0,150,68,68,102,261,150,152,68,102,261,150,152,68,68,0,152,68,102,261,150,152,68,68,68,68,68,150,68,102,261,150,152,68,102,68,102,68,102,261,150,152,68,102,68,102,150,261,261,102,68,68,102,261,0,68,102,261,68,102,261,150,152,68,102,261,150,152,68,102,261,150,152,68,102,150,68,102,261,150,152,68,102,261,0,153,153,0,153,153,153,153,153,153,153,153,153,153,0,0,0,0,0,55,55,55,55,55,0,55,55,55,55,55,55,55,0,0,55,55,55,55,55,55,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,146,159,157,158,156,146,159,157,158,156,156,156,157,158,156,158,157,146,0,0,159,157,158,156,146,146,159,157,158,156,146,159,157,158,156,157,0,0,0,156,146,156,156,146,159,157,158,156,146,159,157,158,156,146,159,157,158,156,159,158,157,146,159,157,158,156,0,0,0,0,160,160,160,160,160,0,160,160,160,160,160,0,160,160,0,0,160,160,160,160,160,160,0,0,0,0,0,0,0,0,0,0,162,164,161,162,163,164,161,162,163,161,162,163,164,164,164,161,162,163,164,161,162,163,162,163,162,164,161,162,163,164,161,162,163,164,161,162,163,164,161,162,163,0,0,0,0,0,0,0,0,0,0,0,166,120,166,120,120,166,120,166,166,166,166,120,120,120,120,0,120,120,120,120,166,166,120,166,120,166,120,166,120,0,0,167,168,167,168,167,168,167,168,167,167,167,167,168,168,168,168,167,168,167,168,167,168,168,168,167,168,168,167,167,168,167,168,168,167,168,167,168,167,168,168,165,134,165,165,134,134,134,0,0,134,165,134,165,134,134,134,165,165,134,165,134,165,0,134,165,0,0,0,0,134,165,0,134,165,134,165,134,165,0,134,165,0,169,169,0,169,169,169,169,169,169,169,0,0,0,0,171,171,0,171,171,171,171,0,171,171,171,171,171],"f":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,[-1,1,[[3,[2]]]],0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[4,4],[[-1,-2],5,[],[]],[4,6],[7,4],[[7,8],9],[[7,8],9],[[10,8],9],[[4,8],9],[-1,-1,[]],[11,7],[-1,-1,[]],[-1,-1,[]],[[-1,11],7,[[14,[[13,[12]]]]]],0,[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],0,[7,11],0,[[15,7],5],0,[7,[[16,[12]]]],0,[-1,-2,[],[]],[-1,17,[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,19,[]],[-1,19,[]],[-1,19,[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[[-1,-2],[[20,[-3]]],[],21,[]],0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,[-1,-2,[],[]],[-1,-2,[],[]],[11,4],[[11,8],9],[[11,8],9],[-1,-1,[]],[-1,-2,[],[]],[-1,17,[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,19,[]],[-1,-2,[],[]],0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,[[],17],[7,[[16,[17]]]],[[15,7],5],[[15,17,17],[[18,[22,[13,[12]]]]]],0,0,0,0,0,0,0,0,0,[[23,24,25],5],[23,5],[[23,25,4],5],[[23,25,7],5],[[23,25],5],[[23,25,26],5],0,[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,[[18,[27]]],28],[-1,[[18,[29]]],28],[-1,[[18,[24]]],28],[[29,29],30],[[24,24],30],[[-1,-2],30,[],[]],[[-1,-2],30,[],[]],[[-1,-2],30,[],[]],[[-1,-2],30,[],[]],[[-1,-2],30,[],[]],[[-1,-2],30,[],[]],[[-1,-2],30,[],[]],[[-1,-2],30,[],[]],0,0,0,[[29,8],9],[[24,8],9],[-1,-1,[]],[-1,-1,[]],[-1,-1,[]],[-1,-1,[]],[[],29],[[],23],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[[24,25],27],0,0,0,0,[[23,[16,[31]]],5],[[27,-1],18,32],[[29,-1],18,32],[[24,-1],18,32],0,[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,19,[]],[-1,19,[]],[-1,19,[]],[-1,19,[]],[[],33],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],0,0,0,0,0,0,[[],[[20,[34]]]],[[],[[20,[35]]]],[[36,-1],[[38,[[37,[-2]]]]],39,[]],[40,[[38,[-1]]],41],[36,[[38,[[0,[42]]]]]],[-1,[[38,[[16,[17]]]]],[[3,[36]]]],[-1,[[20,[5]]],[[3,[36]]]],[-1,[[20,[5]]],[[3,[36]]]],[[-1,-2],[[38,[5]]],[[3,[36]]],[[3,[36]]]],[36,[[38,[5]]]],[[-1,-2],[[38,[5]]],[[3,[36]]],[[3,[36]]]],[[-1,-2],[[38,[5]]],[[3,[36]]],[[3,[36]]]],[36,[[38,[43]]]],0,0,0,0,0,0,0,0,0,0,0,[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[[[16,[44]]],[[20,[45]]]],0,0,[[31,31],30],[[46,46],30],[[-1,-2],30,[],[]],[[-1,-2],30,[],[]],[[-1,-2],30,[],[]],[[-1,-2],30,[],[]],[[-1,-2],30,[],[]],[[-1,-2],30,[],[]],[[-1,-2],30,[],[]],[[-1,-2],30,[],[]],[45,[[16,[47]]]],0,[[31,8],9],[[46,8],9],[-1,-1,[]],[-1,-1,[]],[-1,-1,[]],[-1,-1,[]],[-1,-1,[]],[-1,-1,[]],[-1,-1,[]],[36,[[20,[[16,[45]]]]]],[-1,[[20,[45]]],48],[15,[[20,[46]]]],[[49,[16,[44]]],[[20,[45]]]],0,0,[[],49],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],0,0,[[45,45],45],[[[50,[-1]],[50,[-1]]],[[50,[-1]]],51],[[52,52],52],[[47,47],47],0,[45,[[16,[[50,[53]]]]]],0,[45,[[16,[[50,[54]]]]]],0,0,[45,[[16,[[50,[55]]]]]],0,0,0,0,0,[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[56,[[20,[47]]]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[57,[[20,[31]]]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,19,[]],[-1,19,[]],[-1,19,[]],[-1,19,[]],[-1,19,[]],[-1,19,[]],[-1,19,[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[45,[[16,[52]]]],0,0,0,0,0,0,0,0,0,0,0,[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,[[18,[58]]],28],[-1,[[18,[59]]],28],[-1,[[18,[57]]],28],[-1,[[18,[60]]],28],[-1,[[18,[56]]],28],[-1,[[18,[[61,[-2]]]]],28,51],[-1,[[18,[62]]],28],0,0,0,0,[-1,-1,[]],[-1,-1,[]],[-1,-1,[]],[-1,-1,[]],[-1,-1,[]],[-1,-1,[]],[-1,-1,[]],0,0,[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[[58,36],[[20,[63]]]],[[58,-1,-2,-3],[[20,[-4]]],21,21,21,[]],[[60,36],[[20,[45]]]],[[59,36],[[20,[64]]]],[[58,36],[[20,[65]]]],[[[61,[-1]],36],[[20,[[50,[-1]]]]],51],[[62,36],[[20,[52]]]],0,0,0,0,0,0,0,0,0,[[58,-1],18,32],[[59,-1],18,32],[[57,-1],18,32],[[60,-1],18,32],[[56,-1],18,32],[[[61,[-1]],-2],18,51,32],[[62,-1],18,32],0,0,[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,19,[]],[-1,19,[]],[-1,19,[]],[-1,19,[]],[-1,19,[]],[-1,19,[]],[-1,19,[]],0,[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[15,[[16,[15]]]],[[63,63],30],[[65,65],30],[[64,64],30],[[-1,-2],30,[],[]],[[-1,-2],30,[],[]],[[-1,-2],30,[],[]],[[-1,-2],30,[],[]],[[-1,-2],30,[],[]],[[-1,-2],30,[],[]],[[-1,-2],30,[],[]],[[-1,-2],30,[],[]],[[-1,-2],30,[],[]],[[-1,-2],30,[],[]],[[-1,-2],30,[],[]],[[-1,-2],30,[],[]],[[15,36,[16,[17]]],[[20,[17]]]],[[63,8],9],[[65,8],9],[[64,8],9],0,[-1,-1,[]],[-1,-1,[]],[-1,-1,[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],0,[[63,66,15],[[20,[17]]]],[[65,15],[[20,[17]]]],[[64,15],[[20,[17]]]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,19,[]],[-1,19,[]],[-1,19,[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],0,0,0,0,[66,[[20,[30]]]],[[],[[20,[[67,[66]]]]]],[66,[[20,[30]]]],[[],[[20,[[67,[66]]]]]],[[],[[20,[[67,[68]]]]]],[66,[[20,[30]]]],[[],[[20,[[67,[66]]]]]],[36,[[20,[[67,[66]]]]]],[66,[[20,[30]]]],[[],[[20,[[67,[66]]]]]],0,0,[[],[[20,[22]]]],[[],[[20,[[37,[22]]]]]],0,[[],[[20,[69]]]],[[],[[20,[70]]]],[[],[[20,[22]]]],[[],[[20,[[37,[22]]]]]],0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[71,71],[[-1,-2],5,[],[]],0,[[72,73],30],[72,5],[[71,8],9],[-1,-1,[]],[-1,-1,[]],[-1,-1,[]],[[74,71],[[18,[5,75]]]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],0,[[],76],[[72,77],5],[[72,-1],5,78],[[72,-1],5,78],[[74,71],72],[-1,-2,[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,19,[]],[-1,19,[]],[-1,19,[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[[15,-1],17,78],[[15,[79,[27]]],5],[[15,[16,[22]]],[[16,[80]]]],[17,[[16,[22]]]],0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,[81,82],[81,82],[81,82],[[[83,[-1]]],[[83,[-1]]],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[[],11],[[82,84],[[20,[85]]]],[[[83,[-1]]],[[83,[-1]]],86],[87,87],[[[88,[-1]]],[[88,[-1]]],86],[81,81],[89,89],[82,82],[[-1,-2],5,[],[]],[[-1,-2],5,[],[]],[[-1,-2],5,[],[]],[[-1,-2],5,[],[]],[[-1,-2],5,[],[]],[[-1,-2],5,[],[]],[[[83,[-1]]],[[83,[-1]]],86],[[81,81],90],[[-1,-2],90,[],[]],[84,[[20,[[16,[82]]]]]],[[],[[88,[-1]]],[]],[[81,81],30],[[-1,-2],30,[],[]],[[-1,-2],30,[],[]],[[-1,-2],30,[],[]],[[-1,-2],30,[],[]],[[87,8],9],[-1,-1,[]],[-1,-1,[]],[-1,-1,[]],[-1,-1,[]],[-1,-1,[]],[-1,-1,[]],0,[[[88,[-1]],[16,[-1]]],[[16,[-1]]],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[[[88,[-1]],-2],[[88,[-3]]],[],21,[]],[[89,82],82],0,0,0,0,0,0,0,0,[[81,81],[[16,[90]]]],0,0,0,0,0,0,[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,17,[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[91,[[20,[81]]]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,19,[]],[-1,19,[]],[-1,19,[]],[-1,19,[]],[-1,19,[]],[-1,19,[]],0,[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,[[83,[-1]]],[]],[-1,[[83,[-1]]],[]],[-1,[[83,[-1]]],[]],[-1,[[83,[-1]]],[]],0,0,0,0,0,[85,[[20,[[37,[22]]]]]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-1,[]],[-1,-2,[],[]],0,0,[85,[[20,[92]]]],0,[85,[[20,[[83,[66]]]]]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,19,[]],[-1,-2,[],[]],0,0,[-1,-2,[],[]],[-1,-2,[],[]],[-1,-1,[]],[-1,-2,[],[]],[[],[[20,[92]]]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,19,[]],[-1,-2,[],[]],0,0,0,[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],0,[[44,-1],[[16,[22]]],[[3,[36]]]],[22,[[16,[22]]]],[[],[[20,[[16,[44]]]]]],[22,[[20,[[16,[44]]]]]],[-1,-1,[]],[-1,-1,[]],[-1,-1,[]],[22,[[20,[44]]]],[93,[[20,[[16,[44]]]]]],[93,[[20,[[16,[44]]]]]],[[44,2],[[20,[30]]]],[[44,15],30],[[],93],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[36,30],[36,30],[36,30],[36,30],[44,36],0,[[91,91],91],[44,30],0,0,[[44,66],[[20,[5]]]],[[44,[16,[66]]],[[20,[5]]]],[[44,[16,[66]]],[[20,[5]]]],[[44,[16,[66]]],[[20,[5]]]],[44,[[16,[81]]]],0,0,0,0,[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,19,[]],[-1,19,[]],[-1,19,[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],0,[44,[[0,[42]]]],0,0,0,0,0,0,0,0,0,0,0,0,[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[[],94],0,0,[-1,[[18,[95]]],28],[-1,[[18,[94]]],28],0,0,0,[[96,8],9],[-1,-1,[]],[-1,-1,[]],[-1,-1,[]],[-1,-1,[]],[36,[[20,[97]]]],[36,[[20,[95]]]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],0,0,[94,[[20,[[5,[91,[16,[22]]]]]]]],0,0,[[94,-1],18,32],[-1,17,[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,19,[]],[-1,19,[]],[-1,19,[]],[-1,19,[]],[[36,96,[16,[66]]],[[20,[5]]]],0,[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],0,0,0,0,[85,5],[[],5],[84,[[20,[98]]]],[[2,[79,[92]],[99,[-1,-2,-3]],89,84],[[20,[98]]],[[3,[2]]],[[3,[2]]],[]],0,[[[83,[66]]],17],[[2,[79,[92]],84],[[20,[100]]]],[101,[[20,[92]]]],0,0,0,0,0,[2,92],0,0,0,[-1,-2,[],[]],[-1,-2,[],[]],[[2,[79,[92]],84],[[20,[100]]]],[[17,[16,[82]],84],[[20,[[5,[92,11]]]]]],[-1,-1,[]],[[102,84],[[20,[103]]]],[[2,84],[[20,[[16,[103]]]]]],[-1,-2,[],[]],[[17,[16,[82]],84],[[20,[[5,[92,11]]]]]],0,[[],[[20,[92]]]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,19,[]],[-1,-2,[],[]],0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[[104,84],[[20,[5]]]],[[100,89],5],[[105,89],5],[[106,89],5],[[104,89],5],[[107,89],5],0,0,0,0,[[105,-1,-2],5,[[3,[2]]],[[3,[2]]]],[[100,[99,[-1,-2,-3]]],5,[[3,[2]]],[[3,[2]]],[]],[[105,-1],5,48],[[106,-1],5,48],[[104,-1],5,48],[[107,-1],5,48],[[100,84],[[20,[98]]]],[[105,84],[[20,[98]]]],[[106,84],[[20,[98]]]],[[104,84],[[20,[98]]]],[[107,84],[[20,[98]]]],[[108,84],[[20,[98]]]],[109,[[20,[98]]]],[[-1,82,17],[[20,[106]]],48],[109,100],[104,100],[105,100],[[[37,[100]]],100],[108,100],[106,100],[-1,-1,[]],[107,100],[-1,-1,[]],[-1,-1,[]],[-1,-1,[]],[-1,-1,[]],[-1,-1,[]],[-1,-1,[]],[-1,-1,[]],0,[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],0,[[-1,-2,[16,[82]],110],105,[[3,[2]]],48],[[-1,82,111],[[20,[106]]],48],[[-1,82,17],104,48],[[-1,17,82,111],[[20,[107]]],48],[112,108],[112,109],0,0,0,0,0,0,0,[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,19,[]],[-1,19,[]],[-1,19,[]],[-1,19,[]],[-1,19,[]],[-1,19,[]],[-1,19,[]],[-1,19,[]],0,[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[[[79,[92]],84],[[20,[100]]]],[[[16,[82]],84],[[20,[[5,[92,11]]]]]],[[[79,[92]],84],[[20,[100]]]],[84,[[16,[17]]]],[[[16,[82]],84],[[20,[[5,[92,11]]]]]],0,[[[79,[92]],84],[[20,[100]]]],[[[16,[82]],84],[[20,[[5,[92,11]]]]]],0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],0,0,0,[[113,81],[[20,[100]]]],[[114,81],[[20,[100]]]],[115,[[20,[100]]]],[[116,81],[[20,[100]]]],[[117,82,[16,[17]]],[[20,[100]]]],[[116,81],[[20,[100]]]],[[[79,[-1]]],118,[[3,[2]]]],[[[79,[-1]]],118,[[3,[2]]]],[[[79,[-1]]],118,[[3,[2]]]],[-1,-1,[]],[-1,-1,[]],[-1,-1,[]],[-1,-1,[]],[-1,-1,[]],[-1,-1,[]],[-1,-1,[]],[[[79,[-1]]],30,[[3,[2]]]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,30,[[3,[2]]]],[-1,30,[[3,[2]]]],0,0,0,0,0,0,[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,19,[]],[-1,19,[]],[-1,19,[]],[-1,19,[]],[-1,19,[]],[-1,19,[]],[-1,19,[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[[[79,[92]],84],[[20,[100]]]],[[[16,[82]],84],[[20,[[5,[92,11]]]]]],[82,[[20,[5]]]],[[[79,[92]],84],[[20,[100]]]],[[[16,[82]],84],[[20,[[5,[92,11]]]]]],[82,[[20,[5]]]],0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,[[84,25,4],5],[[84,25,7],5],[[84,25],5],[[84,25,26],5],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[25,25],[[-1,-2],5,[],[]],[[25,25],90],[[-1,-2],90,[],[]],[84,[[20,[[16,[81]]]]]],[[25,25],30],[[-1,-2],30,[],[]],[[-1,-2],30,[],[]],[[-1,-2],30,[],[]],[[-1,-2],30,[],[]],0,[[84,4],6],[[84,26],6],[[25,8],[[18,[5,119]]]],[-1,-1,[]],[-1,-1,[]],[84,[[20,[45]]]],0,[[],84],[-1,-2,[],[]],[-1,-2,[],[]],[[25,25],[[16,[90]]]],[84,[[20,[[16,[44]]]]]],0,[84,[[20,[[16,[44]]]]]],[84,[[20,[[16,[81]]]]]],[84,5],[-1,-2,[],[]],[-1,17,[]],[84,[[20,[120]]]],0,[84,[[20,[120]]]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,19,[]],[-1,19,[]],[-1,-2,[],[]],[-1,-2,[],[]],0,0,0,0,0,[-1,-2,[],[]],[-1,-2,[],[]],[15,[[20,[121]]]],[15,[[20,[121]]]],[[121,121],30],[[-1,-2],30,[],[]],[[-1,-2],30,[],[]],[[-1,-2],30,[],[]],[[-1,-2],30,[],[]],[-1,-1,[]],[36,[[20,[[122,[17]]]]]],[-1,-2,[],[]],0,[36,[[20,[5]]]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,19,[]],[-1,-2,[],[]],[15,[[20,[121]]]],[[[5,[123,124]]],[[16,[17]]]],0,0,[[],5],[[],5],0,0,[125,15],[12,17],[[],[[126,[15]]]],[[125,15,33],127],[-1,127,[[14,[[128,[15]]]]]],[[],[[126,[15]]]],[[],[[16,[129]]]],[[-1,-2],17,[78,130],[78,130]],0,0,0,0,0,[[],[[20,[131]]]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],0,[131,5],0,[-1,-1,[]],[-1,-1,[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,19,[]],[-1,19,[]],[-1,-2,[],[]],[-1,-2,[],[]],0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[[[79,[-1]],15],[[20,[5]]],[[3,[15]]]],[-1,[[20,[132]]],133],[15,5],[-1,5,78],[[112,-1],[[0,[21]]],[[3,[15]]]],[[[13,[-1]],84],[[20,[5]]],[]],[15,[[16,[22]]]],[[112,8],9],[[112,8],9],[-1,-1,[]],[-1,-1,[]],[[15,134],112],[[[79,[-1]],15],[[20,[[37,[112]]]]],[[3,[15]]]],[-1,5,78],[-1,5,78],[-1,5,78],[[-1,-2],5,78,78],[[[13,[-1]],84],[[20,[5]]],[]],[-1,-2,[],[]],[-1,-2,[],[]],0,[112,15],0,0,0,[[[13,[-1]],84],[[20,[5]]],[]],0,0,[[-1,-2],[[0,[21]]],[[3,[15]]],[[3,[15]]]],[[112,84],[[20,[[13,[51]]]]]],0,[[112,112],90],[-1,17,[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[15,[[20,[112]]]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,19,[]],[-1,19,[]],[112,[[20,[5]]]],0,[-1,-2,[],[]],[-1,-2,[],[]],0,0,0,0,0,0,0,[66,17],[66,17],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[135,135],[[-1,-2],5,[],[]],[[53,84],[[20,[135]]]],0,[[[13,[53]],84],[[20,[5]]]],[[53,8],9],[[135,8],9],[[135,8],9],[-1,-1,[]],[-1,-1,[]],[[[13,[53]],84],[[20,[5]]]],[-1,-2,[],[]],[-1,-2,[],[]],[66,[[20,[66]]]],0,[66,53],0,[[[13,[53]],84],[[20,[5]]]],0,[[134,84],[[20,[66]]]],0,[-1,-2,[],[]],[-1,17,[]],[-1,17,[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,19,[]],[-1,19,[]],0,[-1,-2,[],[]],[-1,-2,[],[]],0,[-1,-2,[],[]],[-1,-2,[],[]],[-1,[[18,[136]]],28],[[66,[16,[[50,[53]]]]],[[20,[17]]]],[[66,[16,[[50,[53]]]]],[[20,[135]]]],[[66,15,36],[[20,[[13,[137]]]]]],[-1,-1,[]],[-1,-2,[],[]],[36,[[16,[[13,[137]]]]]],[66,[[20,[66]]]],[66,22],[[],17],[[66,66],[[20,[5]]]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,19,[]],[[[13,[137]],66],[[20,[135]]]],[36,[[20,[66]]]],0,[-1,-2,[],[]],0,0,0,0,[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,[[18,[138]]],28],[-1,[[18,[139]]],28],0,0,[[140,8],9],[-1,-1,[]],[138,141],[-1,-1,[]],[-1,-1,[]],[-1,-1,[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],0,0,[-1,[[18,[30]]],28],0,[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,19,[]],[-1,19,[]],[-1,19,[]],[-1,19,[]],0,0,[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[[15,-1],[[20,[[16,[66]]]]],133],[142,143],[[],17],[15,[[20,[[16,[138]]]]]],[[134,84],[[20,[66]]]],[[[16,[[50,[53]]]]],[[20,[66]]]],[[[16,[[50,[53]]]]],[[20,[66]]]],[15,[[20,[138]]]],[[144,[16,[[50,[53]]]]],[[20,[66]]]],0,0,[15,17],[15,17],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[[54,84],[[20,[5]]]],0,[[[13,[54]],84],[[20,[5]]]],[[[13,[145]],84],[[20,[5]]]],[[54,8],9],[[145,8],9],[-1,-1,[]],[-1,-1,[]],[[[13,[54]],84],[[20,[5]]]],[[[13,[145]],84],[[20,[5]]]],[-1,-2,[],[]],[-1,-2,[],[]],[66,54],[[[13,[54]],84],[[20,[5]]]],[[[13,[145]],84],[[20,[5]]]],0,[[134,84],[[20,[[16,[66]]]]]],[-1,17,[]],[-1,17,[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,19,[]],[-1,19,[]],0,[-1,-2,[],[]],[-1,-2,[],[]],[[66,[16,[[50,[54]]]]],[[20,[17]]]],[[66,[16,[[50,[54]]]]],[[20,[5]]]],[[66,15,36],[[20,[[13,[137]]]]]],[36,[[16,[[13,[137]]]]]],[[36,15],[[20,[5]]]],[[[13,[137]],66],[[20,[5]]]],[[[16,[[50,[54]]]]],[[20,[[5,[17,146]]]]]],[[134,84],[[20,[[16,[66]]]]]],[[144,[16,[[50,[54]]]]],[[20,[66]]]],[[15,[16,[[50,[54]]]]],[[20,[66]]]],0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[147,[[20,[5]]]],[148,148],[[-1,-2],5,[],[]],[[149,85],[[20,[150]]]],[[151,85],[[20,[5]]]],[[147,85],[[20,[5]]]],0,0,[[148,148],30],[[[13,[149]],84],[[20,[5]]]],[[149,8],9],[-1,-1,[]],[-1,-1,[]],[-1,-1,[]],[30,148],[-1,-1,[]],0,[[[13,[149]],84],[[20,[5]]]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[[15,111],[[20,[5]]]],0,0,0,0,0,0,0,0,0,0,0,[[17,134],[[20,[149]]]],[111,[[20,[151]]]],[[17,111],[[20,[147]]]],0,0,[[15,-1,36],[[20,[5]]],78],[[[13,[149]],84],[[20,[5]]]],0,0,[[149,85],[[20,[5]]]],[[151,1],5],[[147,1],5],[[111,148],[[20,[34]]]],0,0,[-1,-2,[],[]],[-1,17,[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,19,[]],[-1,19,[]],[-1,19,[]],[-1,19,[]],0,[15,[[20,[5]]]],0,0,0,0,[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[[111,17],[[20,[151]]]],[[15,22,111],[[20,[150]]]],[[15,150],[[20,[5]]]],[[15,150,85,111],[[20,[5]]]],[[17,22,85],[[20,[5]]]],0,0,0,0,[[111,22],22],[-1,-2,[],[]],[-1,-2,[],[]],[111,111],[[-1,-2],5,[],[]],[[111,111],90],[[-1,-2],90,[],[]],[-1,[[18,[111]]],28],[[111,111],30],[[-1,-2],30,[],[]],[[-1,-2],30,[],[]],[[-1,-2],30,[],[]],[[-1,-2],30,[],[]],[[111,8],9],[-1,-1,[]],[[111,22],[[16,[17]]]],[22,[[16,[17]]]],[22,[[16,[17]]]],[36,[[16,[17]]]],[-1,-2,[],[]],[[111,111],[[16,[90]]]],[[111,-1],18,32],[[111,1,22],5],[[111,22],22],[[111,22],22],[-1,-2,[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,19,[]],[-1,-2,[],[]],0,0,0,0,0,0,0,[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[[68,68],90],[[-1,-2],90,[],[]],[15,17],0,[-1,[[18,[68]]],28],[-1,[[18,[102]]],28],[-1,[[18,[81]]],28],[-1,[[18,[150]]],28],[-1,[[18,[152]]],28],[[68,68],30],[[-1,-2],30,[],[]],[[-1,-2],30,[],[]],[[-1,-2],30,[],[]],[[-1,-2],30,[],[]],[[15,36],[[20,[150]]]],[-1,-1,[]],[-1,-1,[]],[-1,-1,[]],[-1,-1,[]],[-1,-1,[]],[-1,[[20,[68]]],[[3,[36]]]],[-1,[[20,[102]]],[[3,[36]]]],[-1,[[20,[[16,[68]]]]],[[3,[36]]]],[-1,[[20,[[16,[102]]]]],[[3,[36]]]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],0,0,0,0,0,0,0,0,[[68,68],[[16,[90]]]],0,0,0,0,[[68,-1],18,32],[[102,-1],18,32],[[81,-1],18,32],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,19,[]],[-1,19,[]],[-1,19,[]],[-1,19,[]],[-1,19,[]],0,0,0,[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[68,[[20,[5]]]],[102,[[20,[5]]]],0,0,[-1,-2,[],[]],[-1,-2,[],[]],[-1,[[18,[[37,[17]]]]],28],[[153,8],9],[[-1,8],[[18,[5,119]]],[]],[-1,-1,[]],[-1,-2,[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,19,[]],[[153,-1],18,154],[[153,15],[[18,[-1]]],155],[-1,-2,[],[]],[15,[[20,[[37,[17]]]]]],[[15,15],[[20,[5]]]],[15,[[20,[5]]]],[15,[[20,[5]]]],0,[15,17],[15,17],[-1,-2,[],[]],[-1,-2,[],[]],[[55,84],[[20,[5]]]],0,[[[13,[55]],84],[[20,[5]]]],[[55,8],9],[-1,-1,[]],[[[13,[55]],84],[[20,[5]]]],[-1,-2,[],[]],[66,55],[[[13,[55]],84],[[20,[5]]]],0,[[134,84],[[20,[66]]]],[-1,17,[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,19,[]],0,[-1,-2,[],[]],[[66,[16,[[50,[55]]]]],[[20,[17]]]],[[66,[16,[[50,[55]]]]],[[20,[5]]]],[[66,15,36],[[20,[[13,[137]]]]]],[36,[[16,[[13,[137]]]]]],[[[13,[137]],66],[[20,[5]]]],[[36,15],[[20,[5]]]],[[[16,[[50,[55]]]]],[[20,[[5,[17,146]]]]]],[[134,84],[[20,[66]]]],[[144,[16,[[50,[55]]]]],[[20,[66]]]],[[15,[16,[[50,[55]]]]],[[20,[66]]]],0,0,0,0,0,0,[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[156,156],[[-1,-2],5,[],[]],[-1,[[18,[157]]],28],[-1,[[18,[158]]],28],[-1,[[18,[156]]],28],0,0,0,[[17,15],[[20,[[5,[17,146]]]]]],[36,[[20,[22]]]],[[159,8],9],[[157,8],9],[[158,8],9],[[156,8],9],[-1,-1,[]],[157,146],[-1,-1,[]],[-1,-1,[]],[-1,-1,[]],[-1,-1,[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],0,[15,17],[[15,15],17],[[15,15,15],17],0,0,0,[-1,-2,[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,19,[]],[-1,19,[]],[-1,19,[]],[-1,19,[]],[-1,19,[]],0,0,0,[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],0,0,[15,30],0,[15,17],[15,17],[-1,-2,[],[]],[-1,-2,[],[]],[[160,84],[[20,[5]]]],0,[[[13,[160]],84],[[20,[5]]]],[[160,8],9],[-1,-1,[]],[[[13,[160]],84],[[20,[5]]]],[-1,-2,[],[]],0,[66,160],[[[13,[160]],84],[[20,[5]]]],0,[[134,84],[[20,[66]]]],[-1,17,[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,19,[]],0,[-1,-2,[],[]],[[66,[16,[52]]],[[20,[17]]]],[[36,15],[[20,[5]]]],[[66,[16,[52]]],[[20,[5]]]],[[66,15,36],[[20,[[13,[137]]]]]],[36,[[16,[[13,[137]]]]]],[[[13,[137]],66],[[20,[5]]]],0,0,0,0,0,[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,[[18,[161]]],28],[-1,[[18,[162]]],28],[-1,[[18,[163]]],28],0,[161,164],[-1,-1,[]],[-1,-1,[]],[-1,-1,[]],[-1,-1,[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[162,30],0,0,[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,19,[]],[-1,19,[]],[-1,19,[]],[-1,19,[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[15,[[20,[[5,[17,146]]]]]],[[134,84],[[20,[66]]]],[17,[[20,[66]]]],[17,[[20,[66]]]],[[144,[16,[52]]],[[20,[66]]]],[144,[[20,[66]]]],[[144,17],[[20,[66]]]],[[144,17],[[20,[66]]]],[[165,[16,[52]]],[[20,[66]]]],0,0,[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[[],[[20,[120]]]],[-1,-1,[]],[-1,-1,[]],[166,[[20,[120]]]],[166,[[20,[120]]]],[[],166],[-1,-2,[],[]],[-1,-2,[],[]],[120,[[16,[81]]]],0,[120,[[20,[5]]]],0,[[120,66],[[20,[5]]]],[[120,[16,[66]]],[[20,[5]]]],[[120,[16,[66]]],[[20,[5]]]],[[120,[16,[66]]],[[20,[5]]]],0,[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,19,[]],[-1,19,[]],[-1,-2,[],[]],[-1,-2,[],[]],0,0,[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,[[18,[167]]],28],[-1,[[18,[168]]],28],[[167,167],30],[[168,168],30],[[-1,-2],30,[],[]],[[-1,-2],30,[],[]],[[-1,-2],30,[],[]],[[-1,-2],30,[],[]],[[-1,-2],30,[],[]],[[-1,-2],30,[],[]],[[-1,-2],30,[],[]],[[-1,-2],30,[],[]],[[167,8],9],[[168,8],9],[-1,-1,[]],[-1,-1,[]],[-1,-2,[],[]],[-1,-2,[],[]],[168,[[20,[17]]]],0,0,[81,168],0,0,[[167,-1],18,32],[[168,-1],18,32],[-1,[[18,[-2]]],[],[]],[17,[[20,[168]]]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,19,[]],[-1,19,[]],[-1,-2,[],[]],[-1,-2,[],[]],0,0,0,0,0,0,0,0,0,0,[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[[],134],[[134,8],9],[[134,8],9],[[165,8],9],[[165,8],9],[-1,-1,[]],[-1,-1,[]],[15,[[20,[134]]]],[15,[[20,[165]]]],0,[-1,-2,[],[]],[-1,-2,[],[]],0,[-1,[[20,[144]]],[[3,[15]]]],[-1,[[20,[66]]],[[3,[15]]]],0,[-1,17,[]],[-1,17,[]],[15,15],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,19,[]],[-1,19,[]],0,[-1,-2,[],[]],[-1,-2,[],[]],0,[-1,-2,[],[]],[-1,-2,[],[]],[-1,[[18,[[99,[17,66]]]]],28],[-1,[[18,[169]]],28],[-1,-1,[]],[-1,-2,[],[]],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,19,[]],[-1,-2,[],[]],[-1,[[18,[[16,[66]]]]],28],[[[16,[66]],-1],18,32],[15,[[18,[144,170]]]],0,[-1,-2,[],[]],[-1,-2,[],[]],[-1,[[18,[66]]],28],[[171,8],9],[[-1,8],[[18,[5,119]]],[]],[-1,-1,[]],[-1,-2,[],[]],[[66,-1],18,32],[-1,[[18,[-2]]],[],[]],[-1,[[18,[-2]]],[],[]],[-1,19,[]],[[171,15],[[18,[-1]]],155],[-1,-2,[],[]]],"c":[],"p":[[3,"Command",2500],[3,"OsStr",2501],[8,"AsRef",2502],[4,"ExitCode",22],[15,"tuple"],[15,"never"],[3,"VoltaError",22],[3,"Formatter",2503],[6,"Result",2503],[3,"Inner",22],[4,"ErrorKind",294],[8,"Error",2504],[3,"Box",2505],[8,"Into",2502],[15,"str"],[4,"Option",2506],[3,"String",2507],[4,"Result",2508],[3,"TypeId",2509],[6,"Fallible",22],[8,"FnOnce",2510],[3,"PathBuf",2511],[3,"EventLog",521],[4,"EventKind",521],[4,"ActivityKind",1548],[15,"i32"],[3,"Event",521],[8,"Deserializer",2512],[3,"ErrorEnv",521],[15,"bool"],[4,"Publish",619],[8,"Serializer",2513],[15,"u64"],[3,"TempDir",2514],[3,"NamedTempFile",2515],[3,"Path",2511],[3,"Vec",2516],[6,"Result",2517],[8,"FnMut",2510],[3,"Error",2517],[8,"Default",2518],[8,"Iterator",2519],[3,"File",2520],[3,"Project",1141],[3,"HookConfig",619],[4,"RegistryFormat",619],[3,"EventHooks",619],[8,"IntoIterator",2521],[3,"LazyHookConfig",619],[3,"ToolHooks",619],[8,"Tool",1685],[3,"YarnHooks",619],[3,"Node",1758],[3,"Npm",1881],[3,"Pnpm",2168],[3,"RawEventHooks",732],[3,"RawPublishHook",732],[3,"RawResolveHook",732],[3,"RawIndexHook",732],[3,"RawHookConfig",732],[3,"RawToolHooks",732],[3,"RawYarnHooks",732],[4,"DistroHook",838],[3,"YarnIndexHook",838],[4,"MetadataHook",838],[3,"Version",2522],[3,"BTreeSet",2523],[3,"PackageConfig",2062],[3,"VoltaHome",2524],[3,"VoltaInstall",2525],[4,"LogVerbosity",926],[3,"Logger",926],[3,"Metadata",2526],[4,"LogContext",926],[3,"SetLoggerError",2526],[4,"LevelFilter",2526],[3,"Record",2526],[8,"Display",2503],[15,"slice"],[3,"Child",2500],[3,"PlatformSpec",987],[3,"Platform",987],[3,"Sourced",987],[3,"Session",1548],[3,"Image",1115],[8,"Clone",2527],[4,"Source",987],[4,"InheritOption",987],[3,"CliPlatform",987],[4,"Ordering",2528],[3,"PartialPlatform",1141],[3,"OsString",2501],[3,"LazyProject",1141],[3,"ToolchainSpec",1201],[3,"RawManifest",1201],[4,"ManifestKey",1201],[3,"Manifest",1201],[3,"ExitStatus",2500],[3,"HashMap",2529],[4,"Executor",1300],[3,"ArgsOs",2530],[3,"BinConfig",2062],[3,"DefaultBinary",1283],[3,"PackageLinkCommand",1300],[3,"ToolCommand",1300],[3,"PackageInstallCommand",1300],[3,"PackageUpgradeCommand",1300],[3,"InternalInstallCommand",1300],[3,"UninstallCommand",1300],[4,"ToolKind",1300],[4,"PackageManager",2028],[4,"Spec",1685],[4,"GlobalCommand",1442],[3,"InstallArgs",1442],[3,"UninstallArgs",1442],[3,"UpgradeArgs",1442],[3,"LinkArgs",1442],[4,"CommandArg",1442],[3,"Error",2503],[3,"Toolchain",2358],[4,"ShimResult",1621],[3,"HashSet",2531],[3,"DirEntry",2520],[3,"Metadata",2520],[4,"Origin",2532],[3,"StyledObject",2533],[3,"ProgressBar",2534],[4,"Cow",2535],[15,"usize"],[8,"Sized",2536],[3,"VoltaLock",1660],[4,"FetchStatus",1685],[8,"Fn",2510],[4,"VersionSpec",2431],[3,"NodeVersion",1758],[3,"Manifest",1803],[8,"Archive",2532],[3,"RawNodeIndex",1824],[3,"RawNodeEntry",1824],[3,"NodeEntry",1824],[3,"NodeIndex",1824],[3,"HeaderMap",2537],[3,"Duration",2538],[3,"Range",2539],[3,"BundledNpm",1881],[3,"PackageIndex",2200],[3,"InPlaceUpgrade",1927],[4,"NeedsScope",1927],[3,"Package",1927],[3,"PackageManifest",2062],[3,"DirectInstall",1927],[3,"GlobalYarnManifest",2062],[3,"BinMapVisitor",2150],[8,"MapAccess",2512],[8,"Error",2512],[3,"RawDistInfo",2200],[3,"RawPackageMetadata",2200],[3,"RawPackageVersionInfo",2200],[3,"PackageDetails",2200],[3,"Yarn",2275],[3,"RawYarnIndex",2304],[3,"RawYarnEntry",2304],[3,"RawYarnAsset",2304],[3,"YarnIndex",2304],[4,"VersionTag",2431],[3,"LazyToolchain",2358],[3,"NodeVersion",2389],[3,"Platform",2389],[3,"Wrapper",2472],[3,"SemverError",2522],[3,"VersionVisitor",2486],[8,"Context",22],[13,"InvalidInvocation",423],[13,"InvalidInvocationOfBareVersion",423],[13,"DeprecatedCommandError",423],[13,"NoShellProfile",423],[13,"BinaryAlreadyInstalled",423],[13,"BypassError",423],[13,"ExecuteHookError",423],[13,"HookCommandFailed",423],[13,"HookPathError",423],[13,"InvalidHookCommand",423],[13,"InvalidHookOutput",423],[13,"NoBundledNpm",423],[13,"ProjectLocalBinaryExecError",423],[13,"ProjectLocalBinaryNotFound",423],[13,"CreateDirError",423],[13,"ReadBinConfigDirError",423],[13,"ReadDirError",423],[13,"SetupToolImageError",423],[13,"DeleteDirectoryError",423],[13,"ExtensionCycleError",423],[13,"InvalidToolName",423],[13,"Unimplemented",423],[13,"CreateLayoutFileError",423],[13,"DeleteFileError",423],[13,"PackageParseError",423],[13,"PackageReadError",423],[13,"PackageWriteError",423],[13,"ParseHooksError",423],[13,"ReadBinConfigError",423],[13,"ReadDefaultNpmError",423],[13,"ReadHooksError",423],[13,"ReadNodeIndexCacheError",423],[13,"ReadNodeIndexExpiryError",423],[13,"ReadPackageConfigError",423],[13,"ReadPlatformError",423],[13,"WriteBinConfigError",423],[13,"WriteDefaultNpmError",423],[13,"WriteNodeIndexCacheError",423],[13,"WriteNodeIndexExpiryError",423],[13,"WritePackageConfigError",423],[13,"WritePlatformError",423],[13,"InvalidRegistryFormat",423],[13,"DownloadToolNetworkError",423],[13,"ParseNodeIndexError",423],[13,"RegistryFetchError",423],[13,"YarnLatestFetchError",423],[13,"CreateTempDirError",423],[13,"CreateTempFileError",423],[13,"UpgradePackageNotFound",423],[13,"UpgradePackageWrongManager",423],[13,"NodeVersionNotFound",423],[13,"NpmVersionNotFound",423],[13,"PnpmVersionNotFound",423],[13,"YarnVersionNotFound",423],[13,"BinaryNotFound",423],[13,"CreateSharedLinkError",423],[13,"ShimCreateError",423],[13,"ShimRemoveError",423],[13,"CannotFetchPackage",423],[13,"CannotPinPackage",423],[13,"NpmLinkMissingPackage",423],[13,"NpmLinkWrongManager",423],[13,"PackageInstallFailed",423],[13,"PackageManifestParseError",423],[13,"PackageManifestReadError",423],[13,"PackageNotFound",423],[13,"CompletionsOutFileError",423],[13,"ContainingDirError",423],[13,"ExtensionPathError",423],[13,"NoDefaultNodeVersion",423],[13,"NoPinnedNodeVersion",423],[13,"PersistInventoryError",423],[13,"SetToolExecutable",423],[13,"UnpackArchiveError",423],[13,"WriteLauncherError",423],[13,"ParseToolSpecError",423],[13,"NpxNotAvailable",423],[13,"VersionParseError",423],[13,"Args",600],[13,"Error",600],[13,"End",600],[13,"ToolEnd",600],[13,"Bin",903],[13,"Bin",905],[3,"System",1131],[6,"DependencyMapIterator",1201],[4,"InterceptedCommand",1442],[3,"LockState",1660],[3,"RawPlatformSpec",2062]],"b":[[165,"impl-Debug-for-VoltaError"],[166,"impl-Display-for-VoltaError"],[414,"impl-Debug-for-ErrorKind"],[415,"impl-Display-for-ErrorKind"],[1363,"impl-From%3CUninstallCommand%3E-for-Executor"],[1364,"impl-From%3CPackageLinkCommand%3E-for-Executor"],[1365,"impl-From%3CToolCommand%3E-for-Executor"],[1366,"impl-From%3CVec%3CExecutor%3E%3E-for-Executor"],[1367,"impl-From%3CInternalInstallCommand%3E-for-Executor"],[1368,"impl-From%3CPackageInstallCommand%3E-for-Executor"],[1370,"impl-From%3CPackageUpgradeCommand%3E-for-Executor"],[1720,"impl-Display-for-Spec"],[1721,"impl-Debug-for-Spec"],[1776,"impl-Display-for-NodeVersion"],[1777,"impl-Debug-for-NodeVersion"],[2445,"impl-Display-for-VersionSpec"],[2446,"impl-Debug-for-VersionSpec"],[2447,"impl-Debug-for-VersionTag"],[2448,"impl-Display-for-VersionTag"]]},\
+"volta_layout":{"doc":"","t":"FAAAAAAODDLLLLLMLLLMLMLLMLMLLMLLLMLLLMLMLLLMLLLMLMLMLMLLLLLMLMLLMMLMLMLLMLMLLLLLLLLLMLMDDLLLLLMLLLMLMLLMLMLLMLLLMLLLMLMLMLMLMLLLMLLLMLMLMLMLLLLLMLMLLMMLMLMLLMLMLLLLLLLLLMLMDCLLLMLLMLMLLMLMLLMLLMLLMLMLMLLMLLLMLMLMLMLLLLMLMLLLLMLMLMLMLLMLMLLLLLLMLMDCLLLMLLMLMLLMLMLLMLLMLLMLMLMLLMLLLMLMLMLMLLLLMLMLLMLLLMLMLMLLMLMLLMLMLLLLLLMLMDCLLLMLLMLMLLMLMLLMLLMLLMLMLMLLMLLLMLMLMLMLLLLMLMLLMLLLMLMLMLLMLMLLMLMLLLLLLMLM","n":["executable","macros","v0","v1","v2","v3","v4","path_buf","VoltaHome","VoltaInstall","borrow","borrow","borrow_mut","borrow_mut","cache_dir","cache_dir","create","create","default_bin_dir","default_bin_dir","default_hooks_file","default_hooks_file","default_package_config_file","default_package_dir","default_package_dir","default_platform_file","default_platform_file","default_tool_bin_config","default_toolchain_dir","default_toolchain_dir","from","from","image_dir","image_dir","into","into","inventory_dir","inventory_dir","log_dir","log_dir","new","new","node_cache_dir","node_cache_dir","node_image_bin_dir","node_image_dir","node_image_root_dir","node_image_root_dir","node_index_expiry_file","node_index_expiry_file","node_index_file","node_index_file","node_inventory_dir","node_inventory_dir","node_npm_version_file","package_distro_file","package_distro_shasum","package_image_dir","package_image_root_dir","package_image_root_dir","package_inventory_dir","package_inventory_dir","root","root","root","root","shim_dir","shim_dir","shim_executable","shim_executable","shim_file","tmp_dir","tmp_dir","tools_dir","tools_dir","try_from","try_from","try_into","try_into","type_id","type_id","yarn_image_bin_dir","yarn_image_dir","yarn_image_root_dir","yarn_image_root_dir","yarn_inventory_dir","yarn_inventory_dir","VoltaHome","VoltaInstall","borrow","borrow","borrow_mut","borrow_mut","cache_dir","cache_dir","create","create","default_bin_dir","default_bin_dir","default_hooks_file","default_hooks_file","default_package_config_file","default_package_dir","default_package_dir","default_platform_file","default_platform_file","default_tool_bin_config","default_toolchain_dir","default_toolchain_dir","from","from","image_dir","image_dir","into","into","inventory_dir","inventory_dir","layout_file","layout_file","log_dir","log_dir","main_executable","main_executable","migrate_executable","migrate_executable","new","new","node_cache_dir","node_cache_dir","node_image_bin_dir","node_image_dir","node_image_root_dir","node_image_root_dir","node_index_expiry_file","node_index_expiry_file","node_index_file","node_index_file","node_inventory_dir","node_inventory_dir","node_npm_version_file","package_distro_file","package_distro_shasum","package_image_dir","package_image_root_dir","package_image_root_dir","package_inventory_dir","package_inventory_dir","root","root","root","root","shim_dir","shim_dir","shim_executable","shim_executable","shim_file","tmp_dir","tmp_dir","tools_dir","tools_dir","try_from","try_from","try_into","try_into","type_id","type_id","yarn_image_bin_dir","yarn_image_dir","yarn_image_root_dir","yarn_image_root_dir","yarn_inventory_dir","yarn_inventory_dir","VoltaHome","VoltaInstall","borrow","borrow_mut","cache_dir","cache_dir","create","default_bin_dir","default_bin_dir","default_hooks_file","default_hooks_file","default_package_config_file","default_package_dir","default_package_dir","default_platform_file","default_platform_file","default_tool_bin_config","default_toolchain_dir","default_toolchain_dir","from","image_dir","image_dir","into","inventory_dir","inventory_dir","layout_file","layout_file","log_dir","log_dir","new","node_cache_dir","node_cache_dir","node_image_bin_dir","node_image_dir","node_image_root_dir","node_image_root_dir","node_index_expiry_file","node_index_expiry_file","node_index_file","node_index_file","node_inventory_dir","node_inventory_dir","node_npm_version_file","npm_image_bin_dir","npm_image_dir","npm_image_root_dir","npm_image_root_dir","npm_inventory_dir","npm_inventory_dir","package_distro_file","package_distro_shasum","package_image_dir","package_image_root_dir","package_image_root_dir","package_inventory_dir","package_inventory_dir","root","root","shim_dir","shim_dir","shim_file","tmp_dir","tmp_dir","tools_dir","tools_dir","try_from","try_into","type_id","yarn_image_bin_dir","yarn_image_dir","yarn_image_root_dir","yarn_image_root_dir","yarn_inventory_dir","yarn_inventory_dir","VoltaHome","VoltaInstall","borrow","borrow_mut","cache_dir","cache_dir","create","default_bin_dir","default_bin_dir","default_hooks_file","default_hooks_file","default_package_config_file","default_package_dir","default_package_dir","default_platform_file","default_platform_file","default_tool_bin_config","default_toolchain_dir","default_toolchain_dir","from","image_dir","image_dir","into","inventory_dir","inventory_dir","layout_file","layout_file","log_dir","log_dir","new","node_cache_dir","node_cache_dir","node_image_bin_dir","node_image_dir","node_image_root_dir","node_image_root_dir","node_index_expiry_file","node_index_expiry_file","node_index_file","node_index_file","node_inventory_dir","node_inventory_dir","node_npm_version_file","npm_image_bin_dir","npm_image_dir","npm_image_root_dir","npm_image_root_dir","npm_inventory_dir","npm_inventory_dir","package_image_dir","package_image_root_dir","package_image_root_dir","pnpm_image_bin_dir","pnpm_image_dir","pnpm_image_root_dir","pnpm_image_root_dir","pnpm_inventory_dir","pnpm_inventory_dir","root","root","shared_lib_dir","shared_lib_root","shared_lib_root","shim_dir","shim_dir","shim_file","tmp_dir","tmp_dir","tools_dir","tools_dir","try_from","try_into","type_id","yarn_image_bin_dir","yarn_image_dir","yarn_image_root_dir","yarn_image_root_dir","yarn_inventory_dir","yarn_inventory_dir","VoltaHome","VoltaInstall","borrow","borrow_mut","cache_dir","cache_dir","create","default_bin_dir","default_bin_dir","default_hooks_file","default_hooks_file","default_package_config_file","default_package_dir","default_package_dir","default_platform_file","default_platform_file","default_tool_bin_config","default_toolchain_dir","default_toolchain_dir","from","image_dir","image_dir","into","inventory_dir","inventory_dir","layout_file","layout_file","log_dir","log_dir","new","node_cache_dir","node_cache_dir","node_image_bin_dir","node_image_dir","node_image_root_dir","node_image_root_dir","node_index_expiry_file","node_index_expiry_file","node_index_file","node_index_file","node_inventory_dir","node_inventory_dir","node_npm_version_file","npm_image_bin_dir","npm_image_dir","npm_image_root_dir","npm_image_root_dir","npm_inventory_dir","npm_inventory_dir","package_image_dir","package_image_root_dir","package_image_root_dir","pnpm_image_bin_dir","pnpm_image_dir","pnpm_image_root_dir","pnpm_image_root_dir","pnpm_inventory_dir","pnpm_inventory_dir","root","root","shared_lib_dir","shared_lib_root","shared_lib_root","shim_dir","shim_dir","shim_file","tmp_dir","tmp_dir","tools_dir","tools_dir","try_from","try_into","type_id","yarn_image_bin_dir","yarn_image_dir","yarn_image_root_dir","yarn_image_root_dir","yarn_inventory_dir","yarn_inventory_dir"],"q":[[0,"volta_layout"],[7,"volta_layout::macros"],[8,"volta_layout::v0"],[87,"volta_layout::v1"],[172,"volta_layout::v2"],[246,"volta_layout::v3"],[325,"volta_layout::v4"],[404,"alloc::string"],[405,"std::path"],[406,"std::io::error"],[407,"std::path"],[408,"core::any"]],"d":["","","","","","","","","","","","","","","Returns the cache_dir
path.","","Creates all subdirectories in this directory layout.","Creates all subdirectories in this directory layout.","Returns the default_bin_dir
path.","","Returns the default_hooks_file
path.","","","Returns the default_package_dir
path.","","Returns the default_platform_file
path.","","","Returns the default_toolchain_dir
path.","","Returns the argument unchanged.","Returns the argument unchanged.","Returns the image_dir
path.","","Calls U::from(self)
.","Calls U::from(self)
.","Returns the inventory_dir
path.","","Returns the log_dir
path.","","Constructs a new instance of the VoltaInstall
layout, …","Constructs a new instance of the VoltaHome
layout, rooted …","Returns the node_cache_dir
path.","","","","Returns the node_image_root_dir
path.","","Returns the node_index_expiry_file
path.","","Returns the node_index_file
path.","","Returns the node_inventory_dir
path.","","","","","","Returns the package_image_root_dir
path.","","Returns the package_inventory_dir
path.","","Returns the root path for this directory layout.","Returns the root path for this directory layout.","","","Returns the shim_dir
path.","","Returns the shim_executable
path.","","","Returns the tmp_dir
path.","","Returns the tools_dir
path.","","","","","","","","","","Returns the yarn_image_root_dir
path.","","Returns the yarn_inventory_dir
path.","","","","","","","","Returns the cache_dir
path.","","Creates all subdirectories in this directory layout.","Creates all subdirectories in this directory layout.","Returns the default_bin_dir
path.","","Returns the default_hooks_file
path.","","","Returns the default_package_dir
path.","","Returns the default_platform_file
path.","","","Returns the default_toolchain_dir
path.","","Returns the argument unchanged.","Returns the argument unchanged.","Returns the image_dir
path.","","Calls U::from(self)
.","Calls U::from(self)
.","Returns the inventory_dir
path.","","Returns the layout_file
path.","","Returns the log_dir
path.","","Returns the main_executable
path.","","Returns the migrate_executable
path.","","Constructs a new instance of the VoltaInstall
layout, …","Constructs a new instance of the VoltaHome
layout, rooted …","Returns the node_cache_dir
path.","","","","Returns the node_image_root_dir
path.","","Returns the node_index_expiry_file
path.","","Returns the node_index_file
path.","","Returns the node_inventory_dir
path.","","","","","","Returns the package_image_root_dir
path.","","Returns the package_inventory_dir
path.","","Returns the root path for this directory layout.","Returns the root path for this directory layout.","","","Returns the shim_dir
path.","","Returns the shim_executable
path.","","","Returns the tmp_dir
path.","","Returns the tools_dir
path.","","","","","","","","","","Returns the yarn_image_root_dir
path.","","Returns the yarn_inventory_dir
path.","","","","","","Returns the cache_dir
path.","","Creates all subdirectories in this directory layout.","Returns the default_bin_dir
path.","","Returns the default_hooks_file
path.","","","Returns the default_package_dir
path.","","Returns the default_platform_file
path.","","","Returns the default_toolchain_dir
path.","","Returns the argument unchanged.","Returns the image_dir
path.","","Calls U::from(self)
.","Returns the inventory_dir
path.","","Returns the layout_file
path.","","Returns the log_dir
path.","","Constructs a new instance of the VoltaHome
layout, rooted …","Returns the node_cache_dir
path.","","","","Returns the node_image_root_dir
path.","","Returns the node_index_expiry_file
path.","","Returns the node_index_file
path.","","Returns the node_inventory_dir
path.","","","","","Returns the npm_image_root_dir
path.","","Returns the npm_inventory_dir
path.","","","","","Returns the package_image_root_dir
path.","","Returns the package_inventory_dir
path.","","Returns the root path for this directory layout.","","Returns the shim_dir
path.","","","Returns the tmp_dir
path.","","Returns the tools_dir
path.","","","","","","","Returns the yarn_image_root_dir
path.","","Returns the yarn_inventory_dir
path.","","","","","","Returns the cache_dir
path.","","Creates all subdirectories in this directory layout.","Returns the default_bin_dir
path.","","Returns the default_hooks_file
path.","","","Returns the default_package_dir
path.","","Returns the default_platform_file
path.","","","Returns the default_toolchain_dir
path.","","Returns the argument unchanged.","Returns the image_dir
path.","","Calls U::from(self)
.","Returns the inventory_dir
path.","","Returns the layout_file
path.","","Returns the log_dir
path.","","Constructs a new instance of the VoltaHome
layout, rooted …","Returns the node_cache_dir
path.","","","","Returns the node_image_root_dir
path.","","Returns the node_index_expiry_file
path.","","Returns the node_index_file
path.","","Returns the node_inventory_dir
path.","","","","","Returns the npm_image_root_dir
path.","","Returns the npm_inventory_dir
path.","","","Returns the package_image_root_dir
path.","","","","Returns the pnpm_image_root_dir
path.","","Returns the pnpm_inventory_dir
path.","","Returns the root path for this directory layout.","","","Returns the shared_lib_root
path.","","Returns the shim_dir
path.","","","Returns the tmp_dir
path.","","Returns the tools_dir
path.","","","","","","","Returns the yarn_image_root_dir
path.","","Returns the yarn_inventory_dir
path.","","","","","","Returns the cache_dir
path.","","Creates all subdirectories in this directory layout.","Returns the default_bin_dir
path.","","Returns the default_hooks_file
path.","","","Returns the default_package_dir
path.","","Returns the default_platform_file
path.","","","Returns the default_toolchain_dir
path.","","Returns the argument unchanged.","Returns the image_dir
path.","","Calls U::from(self)
.","Returns the inventory_dir
path.","","Returns the layout_file
path.","","Returns the log_dir
path.","","Constructs a new instance of the VoltaHome
layout, rooted …","Returns the node_cache_dir
path.","","","","Returns the node_image_root_dir
path.","","Returns the node_index_expiry_file
path.","","Returns the node_index_file
path.","","Returns the node_inventory_dir
path.","","","","","Returns the npm_image_root_dir
path.","","Returns the npm_inventory_dir
path.","","","Returns the package_image_root_dir
path.","","","","Returns the pnpm_image_root_dir
path.","","Returns the pnpm_inventory_dir
path.","","Returns the root path for this directory layout.","","","Returns the shared_lib_root
path.","","Returns the shim_dir
path.","","","Returns the tmp_dir
path.","","Returns the tools_dir
path.","","","","","","","Returns the yarn_image_root_dir
path.","","Returns the yarn_inventory_dir
path.",""],"i":[0,0,0,0,0,0,0,0,0,0,5,3,5,3,3,3,5,3,3,3,3,3,3,3,3,3,3,3,3,3,5,3,3,3,5,3,3,3,3,3,5,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,5,3,5,3,3,3,5,5,3,3,3,3,3,5,3,5,3,5,3,3,3,3,3,3,3,0,0,12,11,12,11,11,11,12,11,11,11,11,11,11,11,11,11,11,11,11,11,12,11,11,11,12,11,11,11,11,11,11,11,12,12,12,12,12,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,12,11,12,11,11,11,12,12,11,11,11,11,11,12,11,12,11,12,11,11,11,11,11,11,11,0,0,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,0,0,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,0,0,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15],"f":[[1,2],0,0,0,0,0,0,0,0,0,[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[3,4],0,[5,[[7,[6]]]],[3,[[7,[6]]]],[3,4],0,[3,4],0,[[3,1],8],[3,4],0,[3,4],0,[[3,1],8],[3,4],0,[-1,-1,[]],[-1,-1,[]],[3,4],0,[-1,-2,[],[]],[-1,-2,[],[]],[3,4],0,[3,4],0,[8,5],[8,3],[3,4],0,[[3,1,1],8],[[3,1,1],8],[3,4],0,[3,4],0,[3,4],0,[3,4],0,[[3,1],8],[[3,1,1],8],[[3,1,1],8],[[3,1,1],8],[3,4],0,[3,4],0,[5,4],[3,4],0,0,[3,4],0,[5,4],0,[[3,1],8],[3,4],0,[3,4],0,[-1,[[9,[-2]]],[],[]],[-1,[[9,[-2]]],[],[]],[-1,[[9,[-2]]],[],[]],[-1,[[9,[-2]]],[],[]],[-1,10,[]],[-1,10,[]],[[3,1],8],[[3,1],8],[3,4],0,[3,4],0,0,0,[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[11,4],0,[12,[[7,[6]]]],[11,[[7,[6]]]],[11,4],0,[11,4],0,[[11,1],8],[11,4],0,[11,4],0,[[11,1],8],[11,4],0,[-1,-1,[]],[-1,-1,[]],[11,4],0,[-1,-2,[],[]],[-1,-2,[],[]],[11,4],0,[11,4],0,[11,4],0,[12,4],0,[12,4],0,[8,12],[8,11],[11,4],0,[[11,1,1],8],[[11,1,1],8],[11,4],0,[11,4],0,[11,4],0,[11,4],0,[[11,1],8],[[11,1,1],8],[[11,1,1],8],[[11,1,1],8],[11,4],0,[11,4],0,[12,4],[11,4],0,0,[11,4],0,[12,4],0,[[11,1],8],[11,4],0,[11,4],0,[-1,[[9,[-2]]],[],[]],[-1,[[9,[-2]]],[],[]],[-1,[[9,[-2]]],[],[]],[-1,[[9,[-2]]],[],[]],[-1,10,[]],[-1,10,[]],[[11,1],8],[[11,1],8],[11,4],0,[11,4],0,0,0,[-1,-2,[],[]],[-1,-2,[],[]],[13,4],0,[13,[[7,[6]]]],[13,4],0,[13,4],0,[[13,1],8],[13,4],0,[13,4],0,[[13,1],8],[13,4],0,[-1,-1,[]],[13,4],0,[-1,-2,[],[]],[13,4],0,[13,4],0,[13,4],0,[8,13],[13,4],0,[[13,1],8],[[13,1],8],[13,4],0,[13,4],0,[13,4],0,[13,4],0,[[13,1],8],[[13,1],8],[[13,1],8],[13,4],0,[13,4],0,[[13,1,1],8],[[13,1,1],8],[[13,1,1],8],[13,4],0,[13,4],0,[13,4],0,[13,4],0,[[13,1],8],[13,4],0,[13,4],0,[-1,[[9,[-2]]],[],[]],[-1,[[9,[-2]]],[],[]],[-1,10,[]],[[13,1],8],[[13,1],8],[13,4],0,[13,4],0,0,0,[-1,-2,[],[]],[-1,-2,[],[]],[14,4],0,[14,[[7,[6]]]],[14,4],0,[14,4],0,[[14,1],8],[14,4],0,[14,4],0,[[14,1],8],[14,4],0,[-1,-1,[]],[14,4],0,[-1,-2,[],[]],[14,4],0,[14,4],0,[14,4],0,[8,14],[14,4],0,[[14,1],8],[[14,1],8],[14,4],0,[14,4],0,[14,4],0,[14,4],0,[[14,1],8],[[14,1],8],[[14,1],8],[14,4],0,[14,4],0,[[14,1],8],[14,4],0,[[14,1],8],[[14,1],8],[14,4],0,[14,4],0,[14,4],0,[[14,1],8],[14,4],0,[14,4],0,[[14,1],8],[14,4],0,[14,4],0,[-1,[[9,[-2]]],[],[]],[-1,[[9,[-2]]],[],[]],[-1,10,[]],[[14,1],8],[[14,1],8],[14,4],0,[14,4],0,0,0,[-1,-2,[],[]],[-1,-2,[],[]],[15,4],0,[15,[[7,[6]]]],[15,4],0,[15,4],0,[[15,1],8],[15,4],0,[15,4],0,[[15,1],8],[15,4],0,[-1,-1,[]],[15,4],0,[-1,-2,[],[]],[15,4],0,[15,4],0,[15,4],0,[8,15],[15,4],0,[[15,1],8],[[15,1],8],[15,4],0,[15,4],0,[15,4],0,[15,4],0,[[15,1],8],[[15,1],8],[[15,1],8],[15,4],0,[15,4],0,[[15,1],8],[15,4],0,[[15,1],8],[[15,1],8],[15,4],0,[15,4],0,[15,4],0,[[15,1],8],[15,4],0,[15,4],0,[[15,1],8],[15,4],0,[15,4],0,[-1,[[9,[-2]]],[],[]],[-1,[[9,[-2]]],[],[]],[-1,10,[]],[[15,1],8],[[15,1],8],[15,4],0,[15,4],0],"c":[],"p":[[15,"str"],[3,"String",404],[3,"VoltaHome",8],[3,"Path",405],[3,"VoltaInstall",8],[15,"tuple"],[6,"Result",406],[3,"PathBuf",405],[4,"Result",407],[3,"TypeId",408],[3,"VoltaHome",87],[3,"VoltaInstall",87],[3,"VoltaHome",172],[3,"VoltaHome",246],[3,"VoltaHome",325]],"b":[]},\
+"volta_layout_macro":{"doc":"","t":"AAODNNDENNEDNNDNGMLLLLLLLLLLLLLMMMMLLLLLLLLLLLLLLMMLLLLLLLLLLLLLLLLLLLLLLLMDDMLLLLLMLMLMLLMMLLLLMMLLLLLLLLLLLLM","n":["ast","ir","layout","Ast","Dir","Dir","Directory","EntryKind","Err","Exe","FieldContents","FieldPrefix","File","File","LayoutStruct","Ok","Result","attrs","borrow","borrow","borrow","borrow","borrow","borrow","borrow_mut","borrow_mut","borrow_mut","borrow_mut","borrow_mut","borrow_mut","compile","decls","directory","entries","filename","flatten","flatten","from","from","from","from","from","from","into","into","into","into","into","into","name","name","parse","parse","parse","parse","parse","try_from","try_from","try_from","try_from","try_from","try_from","try_into","try_into","try_into","try_into","try_into","try_into","type_id","type_id","type_id","type_id","type_id","type_id","visibility","Entry","Ir","attrs","borrow","borrow","borrow_mut","borrow_mut","codegen","context","dir_names","dirs","exe_names","exes","field_names","file_names","filename","files","from","from","into","into","name","name","to_create_method","to_ctor","to_exe_init","to_item_methods","to_normal_init","to_struct_decl","try_from","try_from","try_into","try_into","type_id","type_id","visibility"],"q":[[0,"volta_layout_macro"],[3,"volta_layout_macro::ast"],[75,"volta_layout_macro::ir"],[111,"proc_macro2"],[112,"core::result"],[113,"syn::lit"],[114,"alloc::vec"],[115,"syn::parse"],[116,"syn::error"],[117,"core::any"],[118,"core::iter::traits::iterator"],[119,"proc_macro2"]],"d":["","","A macro for defining Volta directory layout hierarchies.","Abstract syntax tree (AST) for the surface syntax of the …","","A directory field suffix, which consists of a braced …","Represents a directory entry in the AST, which can …","","Contains the error value","","AST for the suffix of a field in a layout!
struct …","AST for the common prefix of a single field in a layout!
…","","A file field suffix, which consists of a single semicolon (…","Represents a single type LayoutStruct in the AST, which …","Contains the success value","","","","","","","","","","","","","","","Compiles (macro-expands) the AST.","","","","","Lowers the AST to a flattened intermediate representation.","Lowers the directory to a flattened intermediate …","Returns the argument unchanged.","Returns the argument unchanged.","Returns the argument unchanged.","Returns the argument unchanged.","Returns the argument unchanged.","Returns the argument unchanged.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","","","","","","","","","","","","","","","","","","","","","","","","","","","","The intermediate representation (IR) of a struct type …","","","","","","","","","","","","","","","","Returns the argument unchanged.","Returns the argument unchanged.","Calls U::from(self)
.","Calls U::from(self)
.","","","","","","","","","","","","","","",""],"i":[0,0,0,0,18,13,0,0,19,18,0,0,18,13,0,19,0,3,1,3,6,18,12,13,1,3,6,18,12,13,1,1,3,6,12,3,6,1,3,6,18,12,13,1,3,6,18,12,13,3,12,1,3,6,12,13,1,3,6,18,12,13,1,3,6,18,12,13,1,3,6,18,12,13,3,0,0,4,4,16,4,16,4,16,4,4,4,4,4,4,16,4,4,16,4,16,4,16,4,4,16,4,16,4,4,16,4,16,4,16,4],"f":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[1,2],0,0,0,0,[3,[[5,[4,2]]]],[[6,4,[8,[7]]],[[5,[9,2]]]],[-1,-1,[]],[-1,-1,[]],[-1,-1,[]],[-1,-1,[]],[-1,-1,[]],[-1,-1,[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],0,0,[10,[[11,[1]]]],[10,[[11,[3]]]],[10,[[11,[6]]]],[10,[[11,[12]]]],[10,[[11,[13]]]],[-1,[[5,[-2]]],[],[]],[-1,[[5,[-2]]],[],[]],[-1,[[5,[-2]]],[],[]],[-1,[[5,[-2]]],[],[]],[-1,[[5,[-2]]],[],[]],[-1,[[5,[-2]]],[],[]],[-1,[[5,[-2]]],[],[]],[-1,[[5,[-2]]],[],[]],[-1,[[5,[-2]]],[],[]],[-1,[[5,[-2]]],[],[]],[-1,[[5,[-2]]],[],[]],[-1,[[5,[-2]]],[],[]],[-1,14,[]],[-1,14,[]],[-1,14,[]],[-1,14,[]],[-1,14,[]],[-1,14,[]],0,0,0,0,[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[4,2],0,[4,[[0,[15]]]],0,[4,[[0,[15]]]],0,[4,[[0,[15]]]],[4,[[0,[15]]]],0,0,[-1,-1,[]],[-1,-1,[]],[-1,-2,[],[]],[-1,-2,[],[]],0,0,[4,2],[4,2],[[16,17],2],[4,2],[[16,17],2],[4,2],[-1,[[5,[-2]]],[],[]],[-1,[[5,[-2]]],[],[]],[-1,[[5,[-2]]],[],[]],[-1,[[5,[-2]]],[],[]],[-1,14,[]],[-1,14,[]],0],"c":[],"p":[[3,"Ast",3],[3,"TokenStream",111],[3,"LayoutStruct",3],[3,"Ir",75],[4,"Result",112],[3,"Directory",3],[3,"LitStr",113],[3,"Vec",114],[15,"tuple"],[6,"ParseStream",115],[6,"Result",116],[3,"FieldPrefix",3],[4,"FieldContents",3],[3,"TypeId",117],[8,"Iterator",118],[3,"Entry",75],[3,"Ident",111],[4,"EntryKind",3],[6,"Result",3]],"b":[]},\
+"volta_migrate":{"doc":"","t":"F","n":["main"],"q":[[0,"volta_migrate"]],"d":[""],"i":[0],"f":[[[],1]],"c":[],"p":[[15,"tuple"]],"b":[]},\
+"volta_shim":{"doc":"","t":"AFEINNLLFLLKLLLL","n":["common","main","Error","IntoResult","Tool","Volta","borrow","borrow_mut","ensure_layout","from","into","into_result","try_from","try_into","type_id","vzip"],"q":[[0,"volta_shim"],[2,"volta_shim::common"],[16,"core::result"],[17,"core::any"]],"d":["","","","","","","","","","Returns the argument unchanged.","Calls U::from(self)
.","","","","",""],"i":[0,0,0,0,2,2,2,2,0,2,2,5,2,2,2,2],"f":[0,[[],1],0,0,0,0,[-1,-2,[],[]],[-1,-2,[],[]],[[],[[3,[1,2]]]],[-1,-1,[]],[-1,-2,[],[]],[-1,[[3,[-2,2]]],[],[]],[-1,[[3,[-2]]],[],[]],[-1,[[3,[-2]]],[],[]],[-1,4,[]],[-1,-2,[],[]]],"c":[],"p":[[15,"tuple"],[4,"Error",2],[4,"Result",16],[3,"TypeId",17],[8,"IntoResult",2]],"b":[]}\
+}');
+if (typeof window !== 'undefined' && window.initSearch) {window.initSearch(searchIndex)};
+if (typeof exports !== 'undefined') {exports.searchIndex = searchIndex};
diff --git a/main/settings.html b/main/settings.html
new file mode 100644
index 000000000..fbc196a78
--- /dev/null
+++ b/main/settings.html
@@ -0,0 +1 @@
+1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +
//! This crate provides types for fetching and unpacking compressed
+//! archives in tarball or zip format.
+use std::fs::File;
+use std::path::Path;
+
+use attohttpc::header::HeaderMap;
+use headers::{ContentLength, Header, HeaderMapExt};
+use thiserror::Error;
+
+mod tarball;
+mod zip;
+
+pub use crate::tarball::Tarball;
+pub use crate::zip::Zip;
+
+/// Error type for this crate
+#[derive(Error, Debug)]
+pub enum ArchiveError {
+ #[error("HTTP failure ({0})")]
+ HttpError(attohttpc::StatusCode),
+
+ #[error("HTTP header '{0}' not found")]
+ MissingHeaderError(&'static attohttpc::header::HeaderName),
+
+ #[error("unexpected content length in HTTP response: {0}")]
+ UnexpectedContentLengthError(u64),
+
+ #[error("{0}")]
+ IoError(#[from] std::io::Error),
+
+ #[error("{0}")]
+ AttohttpcError(#[from] attohttpc::Error),
+
+ #[error("{0}")]
+ ZipError(#[from] zip_rs::result::ZipError),
+}
+
+/// Metadata describing whether an archive comes from a local or remote origin.
+#[derive(Copy, Clone)]
+pub enum Origin {
+ Local,
+ Remote,
+}
+
+pub trait Archive {
+ fn compressed_size(&self) -> u64;
+
+ /// Unpacks the zip archive to the specified destination folder.
+ fn unpack(
+ self: Box<Self>,
+ dest: &Path,
+ progress: &mut dyn FnMut(&(), usize),
+ ) -> Result<(), ArchiveError>;
+
+ fn origin(&self) -> Origin;
+}
+
+cfg_if::cfg_if! {
+ if #[cfg(unix)] {
+ /// Load an archive in the native OS-preferred format from the specified file.
+ ///
+ /// On Windows, the preferred format is zip. On Unixes, the preferred format
+ /// is tarball.
+ pub fn load_native(source: File) -> Result<Box<dyn Archive>, ArchiveError> {
+ Tarball::load(source)
+ }
+
+ /// Fetch a remote archive in the native OS-preferred format from the specified
+ /// URL and store its results at the specified file path.
+ ///
+ /// On Windows, the preferred format is zip. On Unixes, the preferred format
+ /// is tarball.
+ pub fn fetch_native(url: &str, cache_file: &Path) -> Result<Box<dyn Archive>, ArchiveError> {
+ Tarball::fetch(url, cache_file)
+ }
+ } else if #[cfg(windows)] {
+ /// Load an archive in the native OS-preferred format from the specified file.
+ ///
+ /// On Windows, the preferred format is zip. On Unixes, the preferred format
+ /// is tarball.
+ pub fn load_native(source: File) -> Result<Box<dyn Archive>, ArchiveError> {
+ Zip::load(source)
+ }
+
+ /// Fetch a remote archive in the native OS-preferred format from the specified
+ /// URL and store its results at the specified file path.
+ ///
+ /// On Windows, the preferred format is zip. On Unixes, the preferred format
+ /// is tarball.
+ pub fn fetch_native(url: &str, cache_file: &Path) -> Result<Box<dyn Archive>, ArchiveError> {
+ Zip::fetch(url, cache_file)
+ }
+ } else {
+ compile_error!("Unsupported OS (expected 'unix' or 'windows').");
+ }
+}
+
+/// Determines the length of an HTTP response's content in bytes, using
+/// the HTTP `"Content-Length"` header.
+fn content_length(headers: &HeaderMap) -> Result<u64, ArchiveError> {
+ headers
+ .typed_get()
+ .map(|ContentLength(v)| v)
+ .ok_or_else(|| ArchiveError::MissingHeaderError(ContentLength::name()))
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +
//! Provides types and functions for fetching and unpacking a Node installation
+//! tarball in Unix operating systems.
+
+use std::fs::File;
+use std::io::Read;
+use std::path::Path;
+
+use super::{content_length, Archive, ArchiveError, Origin};
+use flate2::read::GzDecoder;
+use fs_utils::ensure_containing_dir_exists;
+use progress_read::ProgressRead;
+use tee::TeeReader;
+
+/// A Node installation tarball.
+pub struct Tarball {
+ compressed_size: u64,
+ data: Box<dyn Read>,
+ origin: Origin,
+}
+
+impl Tarball {
+ /// Loads a tarball from the specified file.
+ pub fn load(source: File) -> Result<Box<dyn Archive>, ArchiveError> {
+ let compressed_size = source.metadata()?.len();
+ Ok(Box::new(Tarball {
+ compressed_size,
+ data: Box::new(source),
+ origin: Origin::Local,
+ }))
+ }
+
+ /// Initiate fetching of a tarball from the given URL, returning a
+ /// tarball that can be streamed (and that tees its data to a local
+ /// file as it streams).
+ pub fn fetch(url: &str, cache_file: &Path) -> Result<Box<dyn Archive>, ArchiveError> {
+ let (status, headers, response) = attohttpc::get(url).send()?.split();
+
+ if !status.is_success() {
+ return Err(ArchiveError::HttpError(status));
+ }
+
+ let compressed_size = content_length(&headers)?;
+
+ ensure_containing_dir_exists(&cache_file)?;
+ let file = File::create(cache_file)?;
+ let data = Box::new(TeeReader::new(response, file));
+
+ Ok(Box::new(Tarball {
+ compressed_size,
+ data,
+ origin: Origin::Remote,
+ }))
+ }
+}
+
+impl Archive for Tarball {
+ fn compressed_size(&self) -> u64 {
+ self.compressed_size
+ }
+ fn unpack(
+ self: Box<Self>,
+ dest: &Path,
+ progress: &mut dyn FnMut(&(), usize),
+ ) -> Result<(), ArchiveError> {
+ let decoded = GzDecoder::new(ProgressRead::new(self.data, (), progress));
+ let mut tarball = tar::Archive::new(decoded);
+ tarball.unpack(dest)?;
+ Ok(())
+ }
+ fn origin(&self) -> Origin {
+ self.origin
+ }
+}
+
+#[cfg(test)]
+pub mod tests {
+
+ use crate::tarball::Tarball;
+ use std::fs::File;
+ use std::path::PathBuf;
+
+ fn fixture_path(fixture_dir: &str) -> PathBuf {
+ let mut cargo_manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
+ cargo_manifest_dir.push("fixtures");
+ cargo_manifest_dir.push(fixture_dir);
+ cargo_manifest_dir
+ }
+
+ #[test]
+ fn test_load() {
+ let mut test_file_path = fixture_path("tarballs");
+ test_file_path.push("test-file.tar.gz");
+ let test_file = File::open(test_file_path).expect("Couldn't open test file");
+ let tarball = Tarball::load(test_file).expect("Failed to load tarball");
+
+ assert_eq!(tarball.compressed_size(), 402);
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +
//! Provides types and functions for fetching and unpacking a Node installation
+//! zip file in Windows operating systems.
+
+use std::fs::File;
+use std::io::Read;
+use std::path::Path;
+
+use super::{content_length, ArchiveError};
+use fs_utils::ensure_containing_dir_exists;
+use progress_read::ProgressRead;
+use tee::TeeReader;
+use verbatim::PathExt;
+use zip_rs::unstable::stream::ZipStreamReader;
+
+use super::Archive;
+use super::Origin;
+
+pub struct Zip {
+ compressed_size: u64,
+ data: Box<dyn Read>,
+ origin: Origin,
+}
+
+impl Zip {
+ /// Loads a cached Node zip archive from the specified file.
+ pub fn load(source: File) -> Result<Box<dyn Archive>, ArchiveError> {
+ let compressed_size = source.metadata()?.len();
+
+ Ok(Box::new(Zip {
+ compressed_size,
+ data: Box::new(source),
+ origin: Origin::Local,
+ }))
+ }
+
+ /// Initiate fetching of a Node zip archive from the given URL, returning
+ /// a `Remote` data source.
+ pub fn fetch(url: &str, cache_file: &Path) -> Result<Box<dyn Archive>, ArchiveError> {
+ let (status, headers, response) = attohttpc::get(url).send()?.split();
+
+ if !status.is_success() {
+ return Err(ArchiveError::HttpError(status));
+ }
+
+ let compressed_size = content_length(&headers)?;
+
+ ensure_containing_dir_exists(&cache_file)?;
+ let file = File::create(cache_file)?;
+ let data = Box::new(TeeReader::new(response, file));
+
+ Ok(Box::new(Zip {
+ compressed_size,
+ data,
+ origin: Origin::Remote,
+ }))
+ }
+}
+
+impl Archive for Zip {
+ fn compressed_size(&self) -> u64 {
+ self.compressed_size
+ }
+ fn unpack(
+ self: Box<Self>,
+ dest: &Path,
+ progress: &mut dyn FnMut(&(), usize),
+ ) -> Result<(), ArchiveError> {
+ // Use a verbatim path to avoid the legacy Windows 260 byte path limit.
+ let dest: &Path = &dest.to_verbatim();
+ let zip = ZipStreamReader::new(ProgressRead::new(self.data, (), progress));
+ zip.extract(dest)?;
+ Ok(())
+ }
+ fn origin(&self) -> Origin {
+ self.origin
+ }
+}
+
+#[cfg(test)]
+pub mod tests {
+
+ use crate::zip::Zip;
+ use std::fs::File;
+ use std::path::PathBuf;
+
+ fn fixture_path(fixture_dir: &str) -> PathBuf {
+ let mut cargo_manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
+ cargo_manifest_dir.push("fixtures");
+ cargo_manifest_dir.push(fixture_dir);
+ cargo_manifest_dir
+ }
+
+ #[test]
+ fn test_load() {
+ let mut test_file_path = fixture_path("zips");
+ test_file_path.push("test-file.zip");
+ let test_file = File::open(test_file_path).expect("Couldn't open test file");
+ let zip = Zip::load(test_file).expect("Failed to load zip file");
+
+ assert_eq!(zip.compressed_size(), 214);
+ }
+}
+
//! This crate provides utilities for operating on the filesystem.
+
+use std::fs;
+use std::io;
+use std::path::Path;
+
+/// This creates the parent directory of the input path, assuming the input path is a file.
+pub fn ensure_containing_dir_exists<P: AsRef<Path>>(path: &P) -> io::Result<()> {
+ path.as_ref()
+ .parent()
+ .ok_or_else(|| {
+ io::Error::new(
+ io::ErrorKind::NotFound,
+ format!(
+ "Could not determine directory information for {}",
+ path.as_ref().display()
+ ),
+ )
+ })
+ .and_then(fs::create_dir_all)
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +
//! This crate provides an adapter for the `std::io::Read` trait to
+//! allow reporting incremental progress to a callback function.
+
+use std::io::{self, Read, Seek, SeekFrom};
+
+/// A reader that reports incremental progress while reading.
+pub struct ProgressRead<R: Read, T, F: FnMut(&T, usize) -> T> {
+ source: R,
+ accumulator: T,
+ progress: F,
+}
+
+impl<R: Read, T, F: FnMut(&T, usize) -> T> Read for ProgressRead<R, T, F> {
+ /// Read some bytes from the underlying reader into the specified buffer,
+ /// and report progress to the progress callback. The progress callback is
+ /// passed the current value of the accumulator as its first argument and
+ /// the number of bytes read as its second argument. The result of the
+ /// progress callback is stored as the updated value of the accumulator,
+ /// to be passed to the next invocation of the callback.
+ fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
+ let len = self.source.read(buf)?;
+ let new_accumulator = {
+ let progress = &mut self.progress;
+ progress(&self.accumulator, len)
+ };
+ self.accumulator = new_accumulator;
+ Ok(len)
+ }
+}
+
+impl<R: Read, T, F: FnMut(&T, usize) -> T> ProgressRead<R, T, F> {
+ /// Construct a new progress reader with the specified underlying reader,
+ /// initial value for an accumulator, and progress callback.
+ pub fn new(source: R, init: T, progress: F) -> ProgressRead<R, T, F> {
+ ProgressRead {
+ source,
+ accumulator: init,
+ progress,
+ }
+ }
+}
+
+impl<R: Read + Seek, T, F: FnMut(&T, usize) -> T> Seek for ProgressRead<R, T, F> {
+ fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
+ self.source.seek(pos)
+ }
+}
+
//! Utilities to use with acceptance tests in Volta.
+
+#[macro_export]
+macro_rules! ok_or_panic {
+ { $e:expr } => {
+ match $e {
+ Ok(x) => x,
+ Err(err) => panic!("{} failed with {}", stringify!($e), err),
+ }
+ };
+}
+
+pub mod matchers;
+pub mod paths;
+pub mod process;
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402 +403 +404 +405 +406 +407 +408 +409 +410 +411 +412 +413 +414 +415 +416 +417 +418 +419 +420 +421 +422 +423 +424 +425 +426 +427 +428 +429 +430 +431 +432 +433 +434 +435 +436 +437 +438 +439 +440 +441 +442 +443 +444 +445 +446 +447 +448 +449 +450 +451 +452 +453 +454 +455 +456 +457 +458 +459 +460 +461 +462 +463 +464 +465 +466 +467 +468 +469 +470 +471 +472 +473 +474 +475 +476 +477 +478 +479 +480 +481 +482 +483 +484 +485 +486 +487 +488 +489 +490 +491 +492 +493 +494 +495 +496 +497 +498 +499 +500 +501 +502 +503 +504 +505 +506 +507 +508 +509 +510 +511 +512 +513 +514 +515 +516 +517 +518 +519 +520 +521 +522 +523 +524 +525 +526 +527 +528 +529 +530 +531 +532 +533 +534 +535 +536 +537 +538 +539 +540 +541 +542 +543 +544 +545 +546 +547 +548 +549 +550 +551 +552 +553 +554 +555 +556 +557 +558 +559 +560 +561 +562 +563 +564 +565 +566 +567 +568 +569 +570 +571 +572 +573 +574 +575 +576 +577 +578 +579 +580 +581 +582 +583 +584 +585 +586 +587 +588 +589 +590 +591 +592 +593 +594 +595 +596 +597 +598 +599 +600 +601 +602 +603 +604 +605 +606 +607 +608 +609 +610 +611 +612 +613 +614 +615 +616 +617 +618 +619 +620 +621 +622 +623 +624 +625 +626 +627 +628 +629 +630 +631 +632 +633 +634 +635 +636 +637 +638 +639 +640 +641 +642 +643 +644 +645 +646 +647 +648 +649 +650 +651 +652 +653 +654 +655 +656 +657 +658 +659 +660 +661 +662 +663 +664 +665 +666 +667 +668 +669 +670 +671 +672 +673 +674 +675 +676 +677 +678 +679 +680 +681 +682 +683 +684 +685 +686 +687 +688 +689 +690 +691 +692 +693 +694 +695 +696 +697 +698 +699 +700 +701 +702 +703 +704 +705 +706 +707 +708 +709 +710 +711 +712 +713 +714 +715 +716 +717 +718 +719 +
use std::fmt;
+use std::process::Output;
+use std::str;
+
+use crate::process::ProcessBuilder;
+
+use hamcrest2::core::{MatchResult, Matcher};
+use serde_json::{self, Value};
+
+#[derive(Clone)]
+pub struct Execs {
+ expect_stdout: Option<String>,
+ expect_stderr: Option<String>,
+ expect_exit_code: Option<i32>,
+ expect_stdout_contains: Vec<String>,
+ expect_stderr_contains: Vec<String>,
+ expect_either_contains: Vec<String>,
+ expect_stdout_contains_n: Vec<(String, usize)>,
+ expect_stdout_not_contains: Vec<String>,
+ expect_stderr_not_contains: Vec<String>,
+ expect_stderr_unordered: Vec<String>,
+ expect_neither_contains: Vec<String>,
+ expect_json: Option<Vec<Value>>,
+}
+
+impl Execs {
+ /// Verify that stdout is equal to the given lines.
+ /// See `lines_match` for supported patterns.
+ pub fn with_stdout<S: ToString>(mut self, expected: S) -> Execs {
+ self.expect_stdout = Some(expected.to_string());
+ self
+ }
+
+ /// Verify that stderr is equal to the given lines.
+ /// See `lines_match` for supported patterns.
+ pub fn with_stderr<S: ToString>(mut self, expected: S) -> Execs {
+ self._with_stderr(&expected);
+ self
+ }
+
+ fn _with_stderr(&mut self, expected: &dyn ToString) {
+ self.expect_stderr = Some(expected.to_string());
+ }
+
+ /// Verify the exit code from the process.
+ pub fn with_status(mut self, expected: i32) -> Execs {
+ self.expect_exit_code = Some(expected);
+ self
+ }
+
+ /// Verify that stdout contains the given contiguous lines somewhere in
+ /// its output.
+ /// See `lines_match` for supported patterns.
+ pub fn with_stdout_contains<S: ToString>(mut self, expected: S) -> Execs {
+ self.expect_stdout_contains.push(expected.to_string());
+ self
+ }
+
+ /// Verify that stderr contains the given contiguous lines somewhere in
+ /// its output.
+ /// See `lines_match` for supported patterns.
+ pub fn with_stderr_contains<S: ToString>(mut self, expected: S) -> Execs {
+ self.expect_stderr_contains.push(expected.to_string());
+ self
+ }
+
+ /// Verify that either stdout or stderr contains the given contiguous
+ /// lines somewhere in its output.
+ /// See `lines_match` for supported patterns.
+ pub fn with_either_contains<S: ToString>(mut self, expected: S) -> Execs {
+ self.expect_either_contains.push(expected.to_string());
+ self
+ }
+
+ /// Verify that stdout contains the given contiguous lines somewhere in
+ /// its output, and should be repeated `number` times.
+ /// See `lines_match` for supported patterns.
+ pub fn with_stdout_contains_n<S: ToString>(mut self, expected: S, number: usize) -> Execs {
+ self.expect_stdout_contains_n
+ .push((expected.to_string(), number));
+ self
+ }
+
+ /// Verify that stdout does not contain the given contiguous lines.
+ /// See `lines_match` for supported patterns.
+ /// See note on `with_stderr_does_not_contain`.
+ pub fn with_stdout_does_not_contain<S: ToString>(mut self, expected: S) -> Execs {
+ self.expect_stdout_not_contains.push(expected.to_string());
+ self
+ }
+
+ /// Verify that stderr does not contain the given contiguous lines.
+ /// See `lines_match` for supported patterns.
+ ///
+ /// Care should be taken when using this method because there is a
+ /// limitless number of possible things that *won't* appear. A typo means
+ /// your test will pass without verifying the correct behavior. If
+ /// possible, write the test first so that it fails, and then implement
+ /// your fix/feature to make it pass.
+ pub fn with_stderr_does_not_contain<S: ToString>(mut self, expected: S) -> Execs {
+ self.expect_stderr_not_contains.push(expected.to_string());
+ self
+ }
+
+ /// Verify that all of the stderr output is equal to the given lines,
+ /// ignoring the order of the lines.
+ /// See `lines_match` for supported patterns.
+ /// This is useful when checking the output of `cargo build -v` since
+ /// the order of the output is not always deterministic.
+ /// Recommend use `with_stderr_contains` instead unless you really want to
+ /// check *every* line of output.
+ ///
+ /// Be careful when using patterns such as `[..]`, because you may end up
+ /// with multiple lines that might match, and this is not smart enough to
+ /// do anything like longest-match. For example, avoid something like:
+ /// [RUNNING] `rustc [..]
+ /// [RUNNING] `rustc --crate-name foo [..]
+ /// This will randomly fail if the other crate name is `bar`, and the
+ /// order changes.
+ pub fn with_stderr_unordered<S: ToString>(mut self, expected: S) -> Execs {
+ self.expect_stderr_unordered.push(expected.to_string());
+ self
+ }
+
+ /// Verify the JSON output matches the given JSON.
+ /// Typically used when testing cargo commands that emit JSON.
+ /// Each separate JSON object should be separated by a blank line.
+ /// Example:
+ /// assert_that(
+ /// p.cargo("metadata"),
+ /// execs().with_json(r#"
+ /// {"example": "abc"}
+ /// {"example": "def"}
+ /// "#)
+ /// );
+ /// Objects should match in the order given.
+ /// The order of arrays is ignored.
+ /// Strings support patterns described in `lines_match`.
+ /// Use `{...}` to match any object.
+ pub fn with_json(mut self, expected: &str) -> Execs {
+ self.expect_json = Some(
+ expected
+ .split("\n\n")
+ .map(|obj| obj.parse().unwrap())
+ .collect(),
+ );
+ self
+ }
+
+ fn match_output(&self, actual: &Output) -> MatchResult {
+ self.match_status(actual)
+ .and(self.match_stdout(actual))
+ .and(self.match_stderr(actual))
+ }
+
+ fn match_status(&self, actual: &Output) -> MatchResult {
+ match self.expect_exit_code {
+ None => Ok(()),
+ Some(code) if actual.status.code() == Some(code) => Ok(()),
+ Some(_) => Err(format!(
+ "exited with {}\n--- stdout\n{}\n--- stderr\n{}",
+ actual.status,
+ String::from_utf8_lossy(&actual.stdout),
+ String::from_utf8_lossy(&actual.stderr)
+ )),
+ }
+ }
+
+ fn match_stdout(&self, actual: &Output) -> MatchResult {
+ self.match_std(
+ self.expect_stdout.as_ref(),
+ &actual.stdout,
+ "stdout",
+ &actual.stderr,
+ MatchKind::Exact,
+ )?;
+ for expect in self.expect_stdout_contains.iter() {
+ self.match_std(
+ Some(expect),
+ &actual.stdout,
+ "stdout",
+ &actual.stderr,
+ MatchKind::Partial,
+ )?;
+ }
+ for expect in self.expect_stderr_contains.iter() {
+ self.match_std(
+ Some(expect),
+ &actual.stderr,
+ "stderr",
+ &actual.stdout,
+ MatchKind::Partial,
+ )?;
+ }
+ for &(ref expect, number) in self.expect_stdout_contains_n.iter() {
+ self.match_std(
+ Some(expect),
+ &actual.stdout,
+ "stdout",
+ &actual.stderr,
+ MatchKind::PartialN(number),
+ )?;
+ }
+ for expect in self.expect_stdout_not_contains.iter() {
+ self.match_std(
+ Some(expect),
+ &actual.stdout,
+ "stdout",
+ &actual.stderr,
+ MatchKind::NotPresent,
+ )?;
+ }
+ for expect in self.expect_stderr_not_contains.iter() {
+ self.match_std(
+ Some(expect),
+ &actual.stderr,
+ "stderr",
+ &actual.stdout,
+ MatchKind::NotPresent,
+ )?;
+ }
+ for expect in self.expect_stderr_unordered.iter() {
+ self.match_std(
+ Some(expect),
+ &actual.stderr,
+ "stderr",
+ &actual.stdout,
+ MatchKind::Unordered,
+ )?;
+ }
+ for expect in self.expect_neither_contains.iter() {
+ self.match_std(
+ Some(expect),
+ &actual.stdout,
+ "stdout",
+ &actual.stdout,
+ MatchKind::NotPresent,
+ )?;
+
+ self.match_std(
+ Some(expect),
+ &actual.stderr,
+ "stderr",
+ &actual.stderr,
+ MatchKind::NotPresent,
+ )?;
+ }
+
+ for expect in self.expect_either_contains.iter() {
+ let match_std = self.match_std(
+ Some(expect),
+ &actual.stdout,
+ "stdout",
+ &actual.stdout,
+ MatchKind::Partial,
+ );
+ let match_err = self.match_std(
+ Some(expect),
+ &actual.stderr,
+ "stderr",
+ &actual.stderr,
+ MatchKind::Partial,
+ );
+
+ if let (Err(_), Err(_)) = (match_std, match_err) {
+ return Err(format!(
+ "expected to find:\n\
+ {}\n\n\
+ did not find in either output.",
+ expect
+ ));
+ }
+ }
+
+ if let Some(ref objects) = self.expect_json {
+ let stdout = str::from_utf8(&actual.stdout)
+ .map_err(|_| "stdout was not utf8 encoded".to_owned())?;
+ let lines = stdout
+ .lines()
+ .filter(|line| line.starts_with('{'))
+ .collect::<Vec<_>>();
+ if lines.len() != objects.len() {
+ return Err(format!(
+ "expected {} json lines, got {}, stdout:\n{}",
+ objects.len(),
+ lines.len(),
+ stdout
+ ));
+ }
+ for (obj, line) in objects.iter().zip(lines) {
+ self.match_json(obj, line)?;
+ }
+ }
+ Ok(())
+ }
+
+ fn match_stderr(&self, actual: &Output) -> MatchResult {
+ self.match_std(
+ self.expect_stderr.as_ref(),
+ &actual.stderr,
+ "stderr",
+ &actual.stdout,
+ MatchKind::Exact,
+ )
+ }
+
+ fn match_std(
+ &self,
+ expected: Option<&String>,
+ actual: &[u8],
+ description: &str,
+ extra: &[u8],
+ kind: MatchKind,
+ ) -> MatchResult {
+ let out = match expected {
+ Some(out) => out,
+ None => return Ok(()),
+ };
+ let actual = match str::from_utf8(actual) {
+ Err(..) => return Err(format!("{} was not utf8 encoded", description)),
+ Ok(actual) => actual,
+ };
+ // Let's not deal with \r\n vs \n on windows...
+ let actual = actual.replace('\r', "");
+ let actual = actual.replace('\t', "<tab>");
+
+ match kind {
+ MatchKind::Exact => {
+ let a = actual.lines();
+ let e = out.lines();
+
+ let diffs = self.diff_lines(a, e, false);
+ if diffs.is_empty() {
+ Ok(())
+ } else {
+ Err(format!(
+ "differences:\n\
+ {}\n\n\
+ other output:\n\
+ `{}`",
+ diffs.join("\n"),
+ String::from_utf8_lossy(extra)
+ ))
+ }
+ }
+ MatchKind::Partial => {
+ let mut a = actual.lines();
+ let e = out.lines();
+
+ let mut diffs = self.diff_lines(a.clone(), e.clone(), true);
+ #[allow(clippy::while_let_on_iterator)]
+ while let Some(..) = a.next() {
+ let a = self.diff_lines(a.clone(), e.clone(), true);
+ if a.len() < diffs.len() {
+ diffs = a;
+ }
+ }
+ if diffs.is_empty() {
+ Ok(())
+ } else {
+ Err(format!(
+ "expected to find:\n\
+ {}\n\n\
+ did not find in output:\n\
+ {}",
+ out, actual
+ ))
+ }
+ }
+ MatchKind::PartialN(number) => {
+ let mut a = actual.lines();
+ let e = out.lines();
+
+ let mut matches = 0;
+
+ loop {
+ if self.diff_lines(a.clone(), e.clone(), true).is_empty() {
+ matches += 1;
+ }
+
+ if a.next().is_none() {
+ break;
+ }
+ }
+
+ if matches == number {
+ Ok(())
+ } else {
+ Err(format!(
+ "expected to find {} occurrences:\n\
+ {}\n\n\
+ did not find in output:\n\
+ {}",
+ number, out, actual
+ ))
+ }
+ }
+ MatchKind::NotPresent => {
+ let mut a = actual.lines();
+ let e = out.lines();
+
+ let mut diffs = self.diff_lines(a.clone(), e.clone(), true);
+ #[allow(clippy::while_let_on_iterator)]
+ while let Some(..) = a.next() {
+ let a = self.diff_lines(a.clone(), e.clone(), true);
+ if a.len() < diffs.len() {
+ diffs = a;
+ }
+ }
+ if diffs.is_empty() {
+ Err(format!(
+ "expected not to find:\n\
+ {}\n\n\
+ but found in output:\n\
+ {}",
+ out, actual
+ ))
+ } else {
+ Ok(())
+ }
+ }
+ MatchKind::Unordered => {
+ let mut a = actual.lines().collect::<Vec<_>>();
+ let e = out.lines();
+
+ for e_line in e {
+ match a.iter().position(|a_line| lines_match(e_line, a_line)) {
+ Some(index) => a.remove(index),
+ None => {
+ return Err(format!(
+ "Did not find expected line:\n\
+ {}\n\
+ Remaining available output:\n\
+ {}\n",
+ e_line,
+ a.join("\n")
+ ));
+ }
+ };
+ }
+ if !a.is_empty() {
+ Err(format!(
+ "Output included extra lines:\n\
+ {}\n",
+ a.join("\n")
+ ))
+ } else {
+ Ok(())
+ }
+ }
+ }
+ }
+
+ fn match_json(&self, expected: &Value, line: &str) -> MatchResult {
+ let actual = match line.parse() {
+ Err(e) => return Err(format!("invalid json, {}:\n`{}`", e, line)),
+ Ok(actual) => actual,
+ };
+
+ match find_mismatch(expected, &actual) {
+ Some((expected_part, actual_part)) => Err(format!(
+ "JSON mismatch\nExpected:\n{}\nWas:\n{}\nExpected part:\n{}\nActual part:\n{}\n",
+ serde_json::to_string_pretty(expected).unwrap(),
+ serde_json::to_string_pretty(&actual).unwrap(),
+ serde_json::to_string_pretty(expected_part).unwrap(),
+ serde_json::to_string_pretty(actual_part).unwrap(),
+ )),
+ None => Ok(()),
+ }
+ }
+
+ fn diff_lines<'a>(
+ &self,
+ actual: str::Lines<'a>,
+ expected: str::Lines<'a>,
+ partial: bool,
+ ) -> Vec<String> {
+ let actual = actual.take(if partial {
+ expected.clone().count()
+ } else {
+ usize::MAX
+ });
+ zip_all(actual, expected)
+ .enumerate()
+ .filter_map(|(i, (a, e))| match (a, e) {
+ (Some(a), Some(e)) => {
+ if lines_match(e, a) {
+ None
+ } else {
+ Some(format!("{:3} - |{}|\n + |{}|\n", i, e, a))
+ }
+ }
+ (Some(a), None) => Some(format!("{:3} -\n + |{}|\n", i, a)),
+ (None, Some(e)) => Some(format!("{:3} - |{}|\n +\n", i, e)),
+ (None, None) => panic!("Cannot get here"),
+ })
+ .collect()
+ }
+}
+
+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
+enum MatchKind {
+ Exact,
+ Partial,
+ PartialN(usize),
+ NotPresent,
+ Unordered,
+}
+
+/// Compare a line with an expected pattern.
+/// - Use `[..]` as a wildcard to match 0 or more characters on the same line
+/// (similar to `.*` in a regex).
+/// - Use `[EXE]` to optionally add `.exe` on Windows (empty string on other
+/// platforms).
+/// - There is a wide range of macros (such as `[COMPILING]` or `[WARNING]`)
+/// to match cargo's "status" output and allows you to ignore the alignment.
+/// See `substitute_macros` for a complete list of macros.
+pub fn lines_match(expected: &str, actual: &str) -> bool {
+ // Let's not deal with / vs \ (windows...)
+ let expected = expected.replace('\\', "/");
+ let mut actual: &str = &actual.replace('\\', "/");
+ let expected = substitute_macros(&expected);
+ for (i, part) in expected.split("[..]").enumerate() {
+ match actual.find(part) {
+ Some(j) => {
+ if i == 0 && j != 0 {
+ return false;
+ }
+ actual = &actual[j + part.len()..];
+ }
+ None => return false,
+ }
+ }
+ actual.is_empty() || expected.ends_with("[..]")
+}
+
+#[test]
+fn lines_match_works() {
+ assert!(lines_match("a b", "a b"));
+ assert!(lines_match("a[..]b", "a b"));
+ assert!(lines_match("a[..]", "a b"));
+ assert!(lines_match("[..]", "a b"));
+ assert!(lines_match("[..]b", "a b"));
+
+ assert!(!lines_match("[..]b", "c"));
+ assert!(!lines_match("b", "c"));
+ assert!(!lines_match("b", "cb"));
+}
+
+// Compares JSON object for approximate equality.
+// You can use `[..]` wildcard in strings (useful for OS dependent things such
+// as paths). You can use a `"{...}"` string literal as a wildcard for
+// arbitrary nested JSON (useful for parts of object emitted by other programs
+// (e.g. rustc) rather than Cargo itself). Arrays are sorted before comparison.
+fn find_mismatch<'a>(expected: &'a Value, actual: &'a Value) -> Option<(&'a Value, &'a Value)> {
+ use serde_json::Value::*;
+ match (expected, actual) {
+ (Number(l), Number(r)) if l == r => None,
+ (Bool(l), Bool(r)) if l == r => None,
+ (String(l), String(r)) if lines_match(l, r) => None,
+ (Array(l), Array(r)) => {
+ if l.len() != r.len() {
+ return Some((expected, actual));
+ }
+
+ let mut l = l.iter().collect::<Vec<_>>();
+ let mut r = r.iter().collect::<Vec<_>>();
+
+ l.retain(
+ |l| match r.iter().position(|r| find_mismatch(l, r).is_none()) {
+ Some(i) => {
+ r.remove(i);
+ false
+ }
+ None => true,
+ },
+ );
+
+ if !l.is_empty() {
+ assert!(!r.is_empty());
+ Some((l[0], r[0]))
+ } else {
+ assert_eq!(r.len(), 0);
+ None
+ }
+ }
+ (Object(l), Object(r)) => {
+ let same_keys = l.len() == r.len() && l.keys().all(|k| r.contains_key(k));
+ if !same_keys {
+ return Some((expected, actual));
+ }
+
+ l.values()
+ .zip(r.values())
+ .find_map(|(l, r)| find_mismatch(l, r))
+ }
+ (Null, Null) => None,
+ // magic string literal "{...}" acts as wildcard for any sub-JSON
+ (String(l), _) if l == "{...}" => None,
+ _ => Some((expected, actual)),
+ }
+}
+
+struct ZipAll<I1: Iterator, I2: Iterator> {
+ first: I1,
+ second: I2,
+}
+
+impl<T, I1: Iterator<Item = T>, I2: Iterator<Item = T>> Iterator for ZipAll<I1, I2> {
+ type Item = (Option<T>, Option<T>);
+ fn next(&mut self) -> Option<(Option<T>, Option<T>)> {
+ let first = self.first.next();
+ let second = self.second.next();
+
+ match (first, second) {
+ (None, None) => None,
+ (a, b) => Some((a, b)),
+ }
+ }
+}
+
+fn zip_all<T, I1: Iterator<Item = T>, I2: Iterator<Item = T>>(a: I1, b: I2) -> ZipAll<I1, I2> {
+ ZipAll {
+ first: a,
+ second: b,
+ }
+}
+
+impl fmt::Display for Execs {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "execs")
+ }
+}
+
+impl fmt::Debug for Execs {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "execs")
+ }
+}
+
+impl Matcher<ProcessBuilder> for Execs {
+ fn matches(&self, mut process: ProcessBuilder) -> MatchResult {
+ self.matches(&mut process)
+ }
+}
+
+impl<'a> Matcher<&'a mut ProcessBuilder> for Execs {
+ fn matches(&self, process: &'a mut ProcessBuilder) -> MatchResult {
+ println!("running {}", process);
+ let res = process.exec_with_output();
+
+ match res {
+ Ok(out) => self.match_output(&out),
+ Err(err) => {
+ if let Some(out) = &err.output {
+ return self.match_output(out);
+ }
+ Err(format!("could not exec process {}: {}", process, err))
+ }
+ }
+ }
+}
+
+impl Matcher<Output> for Execs {
+ fn matches(&self, output: Output) -> MatchResult {
+ self.match_output(&output)
+ }
+}
+
+pub fn execs() -> Execs {
+ Execs {
+ expect_stdout: None,
+ expect_stderr: None,
+ expect_exit_code: Some(0),
+ expect_stdout_contains: Vec::new(),
+ expect_stderr_contains: Vec::new(),
+ expect_either_contains: Vec::new(),
+ expect_stdout_contains_n: Vec::new(),
+ expect_stdout_not_contains: Vec::new(),
+ expect_stderr_not_contains: Vec::new(),
+ expect_stderr_unordered: Vec::new(),
+ expect_neither_contains: Vec::new(),
+ expect_json: None,
+ }
+}
+
+fn substitute_macros(input: &str) -> String {
+ let macros = [
+ ("[RUNNING]", " Running"),
+ ("[COMPILING]", " Compiling"),
+ ("[CHECKING]", " Checking"),
+ ("[CREATED]", " Created"),
+ ("[FINISHED]", " Finished"),
+ ("[ERROR]", "error:"),
+ ("[WARNING]", "warning:"),
+ ("[DOCUMENTING]", " Documenting"),
+ ("[FRESH]", " Fresh"),
+ ("[UPDATING]", " Updating"),
+ ("[ADDING]", " Adding"),
+ ("[REMOVING]", " Removing"),
+ ("[DOCTEST]", " Doc-tests"),
+ ("[PACKAGING]", " Packaging"),
+ ("[DOWNLOADING]", " Downloading"),
+ ("[UPLOADING]", " Uploading"),
+ ("[VERIFYING]", " Verifying"),
+ ("[ARCHIVING]", " Archiving"),
+ ("[INSTALLING]", " Installing"),
+ ("[REPLACING]", " Replacing"),
+ ("[UNPACKING]", " Unpacking"),
+ ("[SUMMARY]", " Summary"),
+ ("[FIXING]", " Fixing"),
+ ("[EXE]", if cfg!(windows) { ".exe" } else { "" }),
+ ];
+ let mut result = input.to_owned();
+ for &(pat, subst) in ¯os {
+ result = result.replace(pat, subst)
+ }
+ result
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +
use std::cell::Cell;
+use std::env;
+use std::fs;
+use std::path::{Path, PathBuf};
+use std::sync::atomic::{AtomicUsize, Ordering};
+use std::sync::Once;
+
+static SMOKE_TEST_DIR: &str = "smoke_test";
+static NEXT_ID: AtomicUsize = AtomicUsize::new(0);
+
+thread_local!(static TASK_ID: usize = NEXT_ID.fetch_add(1, Ordering::SeqCst));
+
+// creates the root directory for the tests (once), and
+// initializes the root and home directories for the current task
+fn init() {
+ static GLOBAL_INIT: Once = Once::new();
+ thread_local!(static LOCAL_INIT: Cell<bool> = Cell::new(false));
+ GLOBAL_INIT.call_once(|| {
+ global_root().mkdir_p();
+ });
+ LOCAL_INIT.with(|i| {
+ if i.get() {
+ return;
+ }
+ i.set(true);
+ root().rm_rf();
+ home().mkdir_p();
+ })
+}
+
+// the root directory for the smoke tests, in `target/smoke_test`
+fn global_root() -> PathBuf {
+ let mut path = ok_or_panic! { env::current_exe() };
+ path.pop(); // chop off exe name
+ path.pop(); // chop off 'debug'
+
+ // If `cargo test` is run manually then our path looks like
+ // `target/debug/foo`, in which case our `path` is already pointing at
+ // `target`. If, however, `cargo test --target $target` is used then the
+ // output is `target/$target/debug/foo`, so our path is pointing at
+ // `target/$target`. Here we conditionally pop the `$target` name.
+ if path.file_name().and_then(|s| s.to_str()) != Some("target") {
+ path.pop();
+ }
+
+ path.join(SMOKE_TEST_DIR)
+}
+
+pub fn root() -> PathBuf {
+ init();
+ global_root().join(TASK_ID.with(|my_id| format!("t{}", my_id)))
+}
+
+pub fn home() -> PathBuf {
+ root().join("home")
+}
+
+enum Remove {
+ File,
+ Dir,
+}
+impl Remove {
+ fn to_str(&self) -> &'static str {
+ match *self {
+ Remove::File => "remove file",
+ Remove::Dir => "remove dir",
+ }
+ }
+
+ fn at(&self, path: &Path) {
+ if cfg!(windows) {
+ let mut p = ok_or_panic!(path.metadata()).permissions();
+ // This lint rule is not applicable: this is in a `cfg!(windows)` block.
+ #[allow(clippy::permissions_set_readonly_false)]
+ p.set_readonly(false);
+ ok_or_panic! { fs::set_permissions(path, p) };
+ }
+ match *self {
+ Remove::File => fs::remove_file(path),
+ Remove::Dir => fs::remove_dir_all(path), // ensure all dir contents are removed
+ }
+ .unwrap_or_else(|e| {
+ panic!("failed to {} {}: {}", self.to_str(), path.display(), e);
+ })
+ }
+}
+
+pub trait PathExt {
+ fn rm(&self);
+ fn rm_rf(&self);
+ fn rm_contents(&self);
+ fn ensure_empty(&self);
+ fn mkdir_p(&self);
+}
+
+impl PathExt for Path {
+ // delete a file if it exists
+ fn rm(&self) {
+ if !self.exists() {
+ return;
+ }
+ // On windows we can't remove a readonly file, and git will
+ // often clone files as readonly. As a result, we have some
+ // special logic to remove readonly files on windows.
+ Remove::File.at(self);
+ }
+
+ /* Technically there is a potential race condition, but we don't
+ * care all that much for our tests
+ */
+ fn rm_rf(&self) {
+ if !self.exists() {
+ return;
+ }
+ self.rm_contents();
+ Remove::Dir.at(self);
+ }
+
+ // remove directory contents but not the directory itself
+ fn rm_contents(&self) {
+ for file in ok_or_panic! { fs::read_dir(self) } {
+ let file = ok_or_panic! { file };
+ if file.file_type().map(|m| m.is_dir()).unwrap_or(false) {
+ file.path().rm_rf();
+ } else {
+ file.path().rm();
+ }
+ }
+ }
+
+ // ensure the directory is created and empty
+ fn ensure_empty(&self) {
+ self.mkdir_p();
+ self.rm_contents();
+ }
+
+ // create all paths up to the input path
+ fn mkdir_p(&self) {
+ fs::create_dir_all(self)
+ .unwrap_or_else(|e| panic!("failed to mkdir_p {}: {}", self.display(), e))
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +
use std::collections::HashMap;
+use std::env;
+use std::ffi::{OsStr, OsString};
+use std::fmt;
+use std::path::Path;
+use std::process::{Command, ExitStatus, Output};
+use std::str;
+
+use thiserror::Error;
+
+/// A builder object for an external process, similar to `std::process::Command`.
+#[derive(Clone, Debug)]
+pub struct ProcessBuilder {
+ /// The program to execute.
+ program: OsString,
+ /// A list of arguments to pass to the program.
+ args: Vec<OsString>,
+ /// Any environment variables that should be set for the program.
+ env: HashMap<String, Option<OsString>>,
+ /// Which directory to run the program from.
+ cwd: Option<OsString>,
+}
+
+impl fmt::Display for ProcessBuilder {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "`{}", self.program.to_string_lossy())?;
+
+ for arg in &self.args {
+ write!(f, " {}", arg.to_string_lossy())?;
+ }
+
+ write!(f, "`")
+ }
+}
+
+impl ProcessBuilder {
+ /// (chainable) Set the executable for the process.
+ pub fn program<T: AsRef<OsStr>>(&mut self, program: T) -> &mut ProcessBuilder {
+ self.program = program.as_ref().to_os_string();
+ self
+ }
+
+ /// (chainable) Add an arg to the args list.
+ pub fn arg<T: AsRef<OsStr>>(&mut self, arg: T) -> &mut ProcessBuilder {
+ self.args.push(arg.as_ref().to_os_string());
+ self
+ }
+
+ /// (chainable) Add many args to the args list.
+ pub fn args<T: AsRef<OsStr>>(&mut self, arguments: &[T]) -> &mut ProcessBuilder {
+ self.args
+ .extend(arguments.iter().map(|t| t.as_ref().to_os_string()));
+ self
+ }
+
+ /// (chainable) Replace args with new args list
+ pub fn args_replace<T: AsRef<OsStr>>(&mut self, arguments: &[T]) -> &mut ProcessBuilder {
+ self.args = arguments
+ .iter()
+ .map(|t| t.as_ref().to_os_string())
+ .collect();
+ self
+ }
+
+ /// (chainable) Set the current working directory of the process
+ pub fn cwd<T: AsRef<OsStr>>(&mut self, path: T) -> &mut ProcessBuilder {
+ self.cwd = Some(path.as_ref().to_os_string());
+ self
+ }
+
+ /// (chainable) Set an environment variable for the process.
+ pub fn env<T: AsRef<OsStr>>(&mut self, key: &str, val: T) -> &mut ProcessBuilder {
+ self.env
+ .insert(key.to_string(), Some(val.as_ref().to_os_string()));
+ self
+ }
+
+ /// (chainable) Unset an environment variable for the process.
+ pub fn env_remove(&mut self, key: &str) -> &mut ProcessBuilder {
+ self.env.insert(key.to_string(), None);
+ self
+ }
+
+ /// Get the executable name.
+ pub fn get_program(&self) -> &OsString {
+ &self.program
+ }
+
+ /// Get the program arguments
+ pub fn get_args(&self) -> &[OsString] {
+ &self.args
+ }
+
+ /// Get the current working directory for the process
+ pub fn get_cwd(&self) -> Option<&Path> {
+ self.cwd.as_ref().map(Path::new)
+ }
+
+ /// Get an environment variable as the process will see it (will inherit from environment
+ /// unless explicitally unset).
+ pub fn get_env(&self, var: &str) -> Option<OsString> {
+ self.env
+ .get(var)
+ .cloned()
+ .or_else(|| Some(env::var_os(var)))
+ .and_then(|s| s)
+ }
+
+ /// Get all environment variables explicitly set or unset for the process (not inherited
+ /// vars).
+ pub fn get_envs(&self) -> &HashMap<String, Option<OsString>> {
+ &self.env
+ }
+
+ /// Run the process, waiting for completion, and mapping non-success exit codes to an error.
+ pub fn exec(&self) -> Result<(), ProcessError> {
+ let mut command = self.build_command();
+
+ let exit = match command.status() {
+ Ok(e) => e,
+ Err(_) => {
+ return Err(process_error(
+ &format!("could not execute process {}", self),
+ None,
+ None,
+ ));
+ }
+ };
+
+ if exit.success() {
+ Ok(())
+ } else {
+ Err(process_error(
+ &format!("process didn't exit successfully: {}", self),
+ Some(exit),
+ None,
+ ))
+ }
+ }
+
+ /// Execute the process, returning the stdio output, or an error if non-zero exit status.
+ pub fn exec_with_output(&self) -> Result<Output, ProcessError> {
+ let mut command = self.build_command();
+
+ let output = match command.output() {
+ Ok(o) => o,
+ Err(_) => {
+ return Err(process_error(
+ &format!("could not execute process {}", self),
+ None,
+ None,
+ ));
+ }
+ };
+
+ if output.status.success() {
+ Ok(output)
+ } else {
+ Err(process_error(
+ &format!("process didn't exit successfully: {}", self),
+ Some(output.status),
+ Some(&output),
+ ))
+ }
+ }
+
+ /// Converts ProcessBuilder into a `std::process::Command`
+ pub fn build_command(&self) -> Command {
+ let mut command = Command::new(&self.program);
+ if let Some(cwd) = self.get_cwd() {
+ command.current_dir(cwd);
+ }
+ for arg in &self.args {
+ command.arg(arg);
+ }
+ for (k, v) in &self.env {
+ match *v {
+ Some(ref v) => {
+ command.env(k, v);
+ }
+ None => {
+ command.env_remove(k);
+ }
+ }
+ }
+ command
+ }
+}
+
+/// A helper function to create a `ProcessBuilder`.
+pub fn process<T: AsRef<OsStr>>(cmd: T) -> ProcessBuilder {
+ ProcessBuilder {
+ program: cmd.as_ref().to_os_string(),
+ args: Vec::new(),
+ cwd: None,
+ env: HashMap::new(),
+ }
+}
+
+#[derive(Debug, Error)]
+#[error("{desc}")]
+pub struct ProcessError {
+ pub desc: String,
+ pub exit: Option<ExitStatus>,
+ pub output: Option<Output>,
+}
+
+pub fn process_error(
+ msg: &str,
+ status: Option<ExitStatus>,
+ output: Option<&Output>,
+) -> ProcessError {
+ let exit = match status {
+ Some(s) => status_to_string(s),
+ None => "never executed".to_string(),
+ };
+ let mut desc = format!("{} ({})", &msg, exit);
+
+ if let Some(out) = output {
+ match str::from_utf8(&out.stdout) {
+ Ok(s) if !s.trim().is_empty() => {
+ desc.push_str("\n--- stdout\n");
+ desc.push_str(s);
+ }
+ Ok(..) | Err(..) => {}
+ }
+ match str::from_utf8(&out.stderr) {
+ Ok(s) if !s.trim().is_empty() => {
+ desc.push_str("\n--- stderr\n");
+ desc.push_str(s);
+ }
+ Ok(..) | Err(..) => {}
+ }
+ }
+
+ return ProcessError {
+ desc,
+ exit: status,
+ output: output.cloned(),
+ };
+
+ fn status_to_string(status: ExitStatus) -> String {
+ status.to_string()
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +
//! A Rust implementation of the validation rules from the core JS package
+//! [`validate-npm-package-name`](https://github.com/npm/validate-npm-package-name/).
+
+use once_cell::sync::Lazy;
+use percent_encoding::{utf8_percent_encode, AsciiSet, NON_ALPHANUMERIC};
+use regex::Regex;
+
+/// The set of characters to encode, matching the characters encoded by
+/// [`encodeURIComponent`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent#description)
+static ENCODE_URI_SET: &AsciiSet = &NON_ALPHANUMERIC
+ .remove(b'-')
+ .remove(b'_')
+ .remove(b'.')
+ .remove(b'!')
+ .remove(b'~')
+ .remove(b'*')
+ .remove(b'\'')
+ .remove(b'(')
+ .remove(b')');
+
+static SCOPED_PACKAGE: Lazy<Regex> =
+ Lazy::new(|| Regex::new(r"^(?:@([^/]+?)[/])?([^/]+?)$").expect("regex is valid"));
+static SPECIAL_CHARS: Lazy<Regex> = Lazy::new(|| Regex::new(r"[~'!()*]").expect("regex is valid"));
+const BLACKLIST: [&str; 2] = ["node_modules", "favicon.ico"];
+
+// Borrowed from https://github.com/juliangruber/builtins
+const BUILTINS: [&str; 39] = [
+ "assert",
+ "buffer",
+ "child_process",
+ "cluster",
+ "console",
+ "constants",
+ "crypto",
+ "dgram",
+ "dns",
+ "domain",
+ "events",
+ "fs",
+ "http",
+ "https",
+ "module",
+ "net",
+ "os",
+ "path",
+ "punycode",
+ "querystring",
+ "readline",
+ "repl",
+ "stream",
+ "string_decoder",
+ "sys",
+ "timers",
+ "tls",
+ "tty",
+ "url",
+ "util",
+ "vm",
+ "zlib",
+ "freelist",
+ // excluded only in some versions
+ "freelist",
+ "v8",
+ "process",
+ "async_hooks",
+ "http2",
+ "perf_hooks",
+];
+
+#[derive(Debug, PartialEq, Eq)]
+pub enum Validity {
+ /// Valid for new and old packages
+ Valid,
+
+ /// Valid only for old packages
+ ValidForOldPackages { warnings: Vec<String> },
+
+ /// Not valid for new or old packages
+ Invalid {
+ warnings: Vec<String>,
+ errors: Vec<String>,
+ },
+}
+
+impl Validity {
+ pub fn valid_for_old_packages(&self) -> bool {
+ matches!(self, Validity::Valid | Validity::ValidForOldPackages { .. })
+ }
+
+ pub fn valid_for_new_packages(&self) -> bool {
+ matches!(self, Validity::Valid)
+ }
+}
+
+pub fn validate(name: &str) -> Validity {
+ let mut warnings = Vec::new();
+ let mut errors = Vec::new();
+
+ if name.is_empty() {
+ errors.push("name length must be greater than zero".into());
+ }
+
+ if name.starts_with('.') {
+ errors.push("name cannot start with a period".into());
+ }
+
+ if name.starts_with('_') {
+ errors.push("name cannot start with an underscore".into());
+ }
+
+ if name.trim() != name {
+ errors.push("name cannot contain leading or trailing spaces".into());
+ }
+
+ // No funny business
+ for blacklisted_name in BLACKLIST.iter() {
+ if &name.to_lowercase() == blacklisted_name {
+ errors.push(format!("{} is a blacklisted name", blacklisted_name));
+ }
+ }
+
+ // Generate warnings for stuff that used to be allowed
+
+ for builtin in BUILTINS.iter() {
+ if name.to_lowercase() == *builtin {
+ warnings.push(format!("{} is a core module name", builtin));
+ }
+ }
+
+ // really-long-package-names-------------------------------such--length-----many---wow
+ // the thisisareallyreallylongpackagenameitshouldpublishdowenowhavealimittothelengthofpackagenames-poch.
+ if name.len() > 214 {
+ warnings.push("name can no longer contain more than 214 characters".into());
+ }
+
+ // mIxeD CaSe nAMEs
+ if name.to_lowercase() != name {
+ warnings.push("name can no longer contain capital letters".into());
+ }
+
+ if name
+ .split('/')
+ .last()
+ .map(|final_part| SPECIAL_CHARS.is_match(final_part))
+ .unwrap_or(false)
+ {
+ warnings.push(r#"name can no longer contain special characters ("~\'!()*")"#.into());
+ }
+
+ if utf8_percent_encode(name, ENCODE_URI_SET).to_string() != name {
+ // Maybe it's a scoped package name, like @user/package
+ if let Some(captures) = SCOPED_PACKAGE.captures(name) {
+ let valid_scope_name = captures
+ .get(1)
+ .map(|scope| scope.as_str())
+ .map(|scope| utf8_percent_encode(scope, ENCODE_URI_SET).to_string() == scope)
+ .unwrap_or(true);
+
+ let valid_package_name = captures
+ .get(2)
+ .map(|package| package.as_str())
+ .map(|package| utf8_percent_encode(package, ENCODE_URI_SET).to_string() == package)
+ .unwrap_or(true);
+
+ if valid_scope_name && valid_package_name {
+ return done(warnings, errors);
+ }
+ }
+
+ errors.push("name can only contain URL-friendly characters".into());
+ }
+
+ done(warnings, errors)
+}
+
+fn done(warnings: Vec<String>, errors: Vec<String>) -> Validity {
+ match (warnings.len(), errors.len()) {
+ (0, 0) => Validity::Valid,
+ (_, 0) => Validity::ValidForOldPackages { warnings },
+ (_, _) => Validity::Invalid { warnings, errors },
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn traditional() {
+ assert_eq!(validate("some-package"), Validity::Valid);
+ assert_eq!(validate("example.com"), Validity::Valid);
+ assert_eq!(validate("under_score"), Validity::Valid);
+ assert_eq!(validate("period.js"), Validity::Valid);
+ assert_eq!(validate("123numeric"), Validity::Valid);
+ assert_eq!(
+ validate("crazy!"),
+ Validity::ValidForOldPackages {
+ warnings: vec![
+ r#"name can no longer contain special characters ("~\'!()*")"#.into()
+ ]
+ }
+ );
+ }
+
+ #[test]
+ fn scoped() {
+ assert_eq!(validate("@npm/thingy"), Validity::Valid);
+ assert_eq!(
+ validate("@npm-zors/money!time.js"),
+ Validity::ValidForOldPackages {
+ warnings: vec![
+ r#"name can no longer contain special characters ("~\'!()*")"#.into()
+ ]
+ }
+ );
+ }
+
+ #[test]
+ fn invalid() {
+ assert_eq!(
+ validate(""),
+ Validity::Invalid {
+ errors: vec!["name length must be greater than zero".into()],
+ warnings: vec![]
+ }
+ );
+
+ assert_eq!(
+ validate(".start-with-period"),
+ Validity::Invalid {
+ errors: vec!["name cannot start with a period".into()],
+ warnings: vec![]
+ }
+ );
+
+ assert_eq!(
+ validate("_start-with-underscore"),
+ Validity::Invalid {
+ errors: vec!["name cannot start with an underscore".into()],
+ warnings: vec![]
+ }
+ );
+
+ assert_eq!(
+ validate("contain:colons"),
+ Validity::Invalid {
+ errors: vec!["name can only contain URL-friendly characters".into()],
+ warnings: vec![]
+ }
+ );
+
+ assert_eq!(
+ validate(" leading-space"),
+ Validity::Invalid {
+ errors: vec![
+ "name cannot contain leading or trailing spaces".into(),
+ "name can only contain URL-friendly characters".into()
+ ],
+ warnings: vec![]
+ }
+ );
+
+ assert_eq!(
+ validate("trailing-space "),
+ Validity::Invalid {
+ errors: vec![
+ "name cannot contain leading or trailing spaces".into(),
+ "name can only contain URL-friendly characters".into()
+ ],
+ warnings: vec![]
+ }
+ );
+
+ assert_eq!(
+ validate("s/l/a/s/h/e/s"),
+ Validity::Invalid {
+ errors: vec!["name can only contain URL-friendly characters".into()],
+ warnings: vec![]
+ }
+ );
+
+ assert_eq!(
+ validate("node_modules"),
+ Validity::Invalid {
+ errors: vec!["node_modules is a blacklisted name".into()],
+ warnings: vec![]
+ }
+ );
+
+ assert_eq!(
+ validate("favicon.ico"),
+ Validity::Invalid {
+ errors: vec!["favicon.ico is a blacklisted name".into()],
+ warnings: vec![]
+ }
+ );
+ }
+
+ #[test]
+ fn node_io_core() {
+ assert_eq!(
+ validate("http"),
+ Validity::ValidForOldPackages {
+ warnings: vec!["http is a core module name".into()]
+ }
+ );
+ }
+
+ #[test]
+ fn long_package_names() {
+ let one_too_long = "ifyouwanttogetthesumoftwonumberswherethosetwonumbersarechosenbyfindingthelargestoftwooutofthreenumbersandsquaringthemwhichismultiplyingthembyitselfthenyoushouldinputthreenumbersintothisfunctionanditwilldothatforyou-";
+ let short_enough = "ifyouwanttogetthesumoftwonumberswherethosetwonumbersarechosenbyfindingthelargestoftwooutofthreenumbersandsquaringthemwhichismultiplyingthembyitselfthenyoushouldinputthreenumbersintothisfunctionanditwilldothatforyou";
+
+ assert_eq!(
+ validate(one_too_long),
+ Validity::ValidForOldPackages {
+ warnings: vec!["name can no longer contain more than 214 characters".into()]
+ }
+ );
+
+ assert_eq!(validate(short_enough), Validity::Valid);
+ }
+
+ #[test]
+ fn legacy_mixed_case() {
+ assert_eq!(
+ validate("CAPITAL-LETTERS"),
+ Validity::ValidForOldPackages {
+ warnings: vec!["name can no longer contain capital letters".into()]
+ }
+ );
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +
use clap::{builder::styling, ColorChoice, Parser};
+
+use crate::command::{self, Command};
+use volta_core::error::{ExitCode, Fallible};
+use volta_core::session::Session;
+use volta_core::style::{text_width, MAX_WIDTH};
+
+#[derive(Parser)]
+#[command(
+ about = "The JavaScript Launcher ⚡",
+ long_about = "The JavaScript Launcher ⚡
+
+ To install a tool in your toolchain, use `volta install`.
+ To pin your project's runtime or package manager, use `volta pin`.",
+ color = ColorChoice::Auto,
+ disable_version_flag = true,
+ styles = styles(),
+ term_width = text_width().unwrap_or(MAX_WIDTH),
+)]
+pub(crate) struct Volta {
+ #[command(subcommand)]
+ pub(crate) command: Option<Subcommand>,
+
+ /// Enables verbose diagnostics
+ #[arg(long, global = true)]
+ pub(crate) verbose: bool,
+
+ /// Enables trace-level diagnostics.
+ #[arg(long, global = true, requires = "verbose")]
+ pub(crate) very_verbose: bool,
+
+ /// Prevents unnecessary output
+ #[arg(
+ long,
+ global = true,
+ conflicts_with = "verbose",
+ aliases = &["silent"]
+ )]
+ pub(crate) quiet: bool,
+
+ /// Prints the current version of Volta
+ #[arg(short, long)]
+ pub(crate) version: bool,
+}
+
+impl Volta {
+ pub(crate) fn run(self, session: &mut Session) -> Fallible<ExitCode> {
+ if self.version {
+ // suffix indicator for dev build
+ if cfg!(debug_assertions) {
+ println!("{}-dev", env!("CARGO_PKG_VERSION"));
+ } else {
+ println!("{}", env!("CARGO_PKG_VERSION"));
+ }
+ Ok(ExitCode::Success)
+ } else if let Some(command) = self.command {
+ command.run(session)
+ } else {
+ Volta::parse_from(["volta", "help"].iter()).run(session)
+ }
+ }
+}
+
+#[derive(clap::Subcommand)]
+pub(crate) enum Subcommand {
+ /// Fetches a tool to the local machine
+ Fetch(command::Fetch),
+
+ /// Installs a tool in your toolchain
+ Install(command::Install),
+
+ /// Uninstalls a tool from your toolchain
+ Uninstall(command::Uninstall),
+
+ /// Pins your project's runtime or package manager
+ Pin(command::Pin),
+
+ /// Displays the current toolchain
+ #[command(alias = "ls")]
+ List(command::List),
+
+ /// Generates Volta completions
+ ///
+ /// By default, completions will be generated for the value of your current shell,
+ /// shell, i.e. the value of `SHELL`. If you set the `<shell>` option, completions
+ /// will be generated for that shell instead.
+ ///
+ /// If you specify a directory, the completions will be written to a file there;
+ /// otherwise, they will be written to `stdout`.
+ #[command(arg_required_else_help = true)]
+ Completions(command::Completions),
+
+ /// Locates the actual binary that will be called by Volta
+ Which(command::Which),
+
+ #[command(long_about = crate::command::r#use::USAGE, hide = true)]
+ Use(command::Use),
+
+ /// Enables Volta for the current user / shell
+ Setup(command::Setup),
+
+ /// Run a command with custom Node, npm, pnpm, and/or Yarn versions
+ Run(command::Run),
+}
+
+impl Subcommand {
+ pub(crate) fn run(self, session: &mut Session) -> Fallible<ExitCode> {
+ match self {
+ Subcommand::Fetch(fetch) => fetch.run(session),
+ Subcommand::Install(install) => install.run(session),
+ Subcommand::Uninstall(uninstall) => uninstall.run(session),
+ Subcommand::Pin(pin) => pin.run(session),
+ Subcommand::List(list) => list.run(session),
+ Subcommand::Completions(completions) => completions.run(session),
+ Subcommand::Which(which) => which.run(session),
+ Subcommand::Use(r#use) => r#use.run(session),
+ Subcommand::Setup(setup) => setup.run(session),
+ Subcommand::Run(run) => run.run(session),
+ }
+ }
+}
+
+fn styles() -> styling::Styles {
+ styling::Styles::plain()
+ .header(
+ styling::AnsiColor::Yellow.on_default()
+ | styling::Effects::BOLD
+ | styling::Effects::ITALIC,
+ )
+ .usage(
+ styling::AnsiColor::Yellow.on_default()
+ | styling::Effects::BOLD
+ | styling::Effects::ITALIC,
+ )
+ .literal(styling::AnsiColor::Green.on_default() | styling::Effects::BOLD)
+ .placeholder(styling::AnsiColor::BrightBlue.on_default())
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +
use std::path::PathBuf;
+
+use clap::CommandFactory;
+use clap_complete::Shell;
+use log::info;
+
+use volta_core::{
+ error::{Context, ErrorKind, ExitCode, Fallible},
+ session::{ActivityKind, Session},
+ style::{note_prefix, success_prefix},
+};
+
+use crate::command::Command;
+
+#[derive(Debug, clap::Args)]
+pub(crate) struct Completions {
+ /// Shell to generate completions for
+ #[arg(index = 1, ignore_case = true, required = true)]
+ shell: Shell,
+
+ /// File to write generated completions to
+ #[arg(short, long = "output")]
+ out_file: Option<PathBuf>,
+
+ /// Write over an existing file, if any.
+ #[arg(short, long)]
+ force: bool,
+}
+
+impl Command for Completions {
+ fn run(self, session: &mut Session) -> Fallible<ExitCode> {
+ session.add_event_start(ActivityKind::Completions);
+
+ let mut app = crate::cli::Volta::command();
+ let app_name = app.get_name().to_owned();
+ match self.out_file {
+ Some(path) => {
+ if path.is_file() && !self.force {
+ return Err(ErrorKind::CompletionsOutFileError { path }.into());
+ }
+
+ // The user may have passed a path that does not yet exist. If
+ // so, we create it, informing the user we have done so.
+ if let Some(parent) = path.parent() {
+ if !parent.is_dir() {
+ info!(
+ "{} {} does not exist, creating it",
+ note_prefix(),
+ parent.display()
+ );
+ std::fs::create_dir_all(parent).with_context(|| {
+ ErrorKind::CreateDirError {
+ dir: parent.to_path_buf(),
+ }
+ })?;
+ }
+ }
+
+ let mut file = &std::fs::File::create(&path).with_context(|| {
+ ErrorKind::CompletionsOutFileError {
+ path: path.to_path_buf(),
+ }
+ })?;
+
+ clap_complete::generate(self.shell, &mut app, app_name, &mut file);
+
+ info!(
+ "{} installed completions to {}",
+ success_prefix(),
+ path.display()
+ );
+ }
+ None => clap_complete::generate(self.shell, &mut app, app_name, &mut std::io::stdout()),
+ };
+
+ session.add_event_end(ActivityKind::Completions, ExitCode::Success);
+ Ok(ExitCode::Success)
+ }
+}
+
use volta_core::error::{ExitCode, Fallible};
+use volta_core::session::{ActivityKind, Session};
+use volta_core::tool;
+
+use crate::command::Command;
+
+#[derive(clap::Args)]
+pub(crate) struct Fetch {
+ /// Tools to fetch, like `node`, `yarn@latest` or `your-package@^14.4.3`.
+ #[arg(value_name = "tool[@version]", required = true)]
+ tools: Vec<String>,
+}
+
+impl Command for Fetch {
+ fn run(self, session: &mut Session) -> Fallible<ExitCode> {
+ session.add_event_start(ActivityKind::Fetch);
+
+ for tool in tool::Spec::from_strings(&self.tools, "fetch")? {
+ tool.resolve(session)?.fetch(session)?;
+ }
+
+ session.add_event_end(ActivityKind::Fetch, ExitCode::Success);
+ Ok(ExitCode::Success)
+ }
+}
+
use volta_core::error::{ExitCode, Fallible};
+use volta_core::session::{ActivityKind, Session};
+use volta_core::tool::Spec;
+
+use crate::command::Command;
+
+#[derive(clap::Args)]
+pub(crate) struct Install {
+ /// Tools to install, like `node`, `yarn@latest` or `your-package@^14.4.3`.
+ #[arg(value_name = "tool[@version]", required = true)]
+ tools: Vec<String>,
+}
+
+impl Command for Install {
+ fn run(self, session: &mut Session) -> Fallible<ExitCode> {
+ session.add_event_start(ActivityKind::Install);
+
+ for tool in Spec::from_strings(&self.tools, "install")? {
+ tool.resolve(session)?.install(session)?;
+ }
+
+ session.add_event_end(ActivityKind::Install, ExitCode::Success);
+ Ok(ExitCode::Success)
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402 +403 +404 +405 +406 +407 +408 +409 +410 +411 +412 +413 +414 +415 +416 +417 +418 +419 +420 +421 +422 +423 +424 +425 +426 +427 +428 +429 +430 +431 +432 +433 +434 +435 +436 +437 +438 +439 +440 +441 +442 +443 +444 +445 +446 +447 +448 +449 +450 +451 +452 +453 +454 +455 +456 +457 +458 +459 +460 +461 +462 +463 +464 +465 +466 +467 +468 +469 +470 +471 +472 +473 +474 +475 +476 +477 +478 +479 +480 +481 +482 +483 +484 +485 +486 +487 +488 +489 +490 +491 +492 +493 +494 +495 +496 +497 +498 +499 +500 +501 +502 +503 +504 +505 +506 +507 +508 +509 +510 +511 +512 +513 +514 +515 +516 +517 +518 +519 +520 +521 +522 +523 +524 +525 +526 +527 +528 +529 +530 +531 +532 +533 +534 +535 +536 +537 +538 +539 +540 +541 +542 +543 +544 +545 +546 +547 +548 +549 +550 +551 +552 +553 +554 +555 +556 +557 +558 +559 +560 +561 +562 +563 +564 +565 +566 +567 +568 +569 +570 +571 +572 +573 +574 +575 +576 +577 +578 +579 +580 +581 +582 +583 +584 +585 +586 +587 +588 +589 +590 +591 +592 +593 +594 +595 +596 +597 +598 +599 +600 +601 +602 +603 +604 +605 +606 +607 +608 +609 +610 +611 +612 +613 +614 +615 +616 +617 +618 +619 +620 +621 +622 +623 +624 +625 +626 +627 +628 +629 +630 +631 +632 +633 +634 +635 +636 +637 +638 +639 +640 +641 +642 +643 +644 +645 +646 +647 +648 +649 +650 +651 +652 +653 +654 +655 +656 +657 +658 +659 +660 +661 +662 +663 +664 +665 +666 +667 +668 +669 +670 +671 +672 +673 +674 +675 +676 +677 +678 +679 +680 +681 +682 +683 +684 +685 +686 +687 +688 +689 +690 +691 +692 +693 +694 +695 +696 +697 +698 +699 +700 +701 +702 +703 +704 +705 +706 +707 +708 +709 +710 +711 +712 +713 +714 +715 +716 +717 +718 +719 +720 +721 +722 +723 +724 +725 +726 +727 +728 +729 +730 +731 +732 +733 +734 +735 +736 +737 +738 +739 +740 +741 +742 +743 +744 +745 +746 +747 +748 +749 +750 +751 +752 +753 +754 +755 +756 +757 +758 +759 +760 +761 +762 +763 +764 +765 +766 +767 +768 +769 +770 +771 +772 +773 +774 +775 +776 +777 +778 +779 +780 +781 +782 +783 +784 +785 +786 +787 +788 +789 +790 +791 +792 +793 +794 +795 +796 +797 +798 +799 +800 +801 +802 +803 +804 +805 +806 +807 +808 +809 +810 +811 +812 +813 +814 +815 +816 +817 +818 +819 +820 +821 +822 +823 +824 +825 +826 +827 +828 +829 +830 +831 +832 +833 +834 +835 +836 +837 +838 +839 +840 +841 +842 +843 +844 +845 +846 +847 +848 +849 +850 +851 +852 +853 +854 +855 +856 +857 +858 +859 +860 +861 +862 +863 +864 +865 +866 +867 +868 +869 +870 +871 +872 +873 +874 +875 +876 +877 +878 +879 +880 +881 +882 +883 +884 +885 +886 +887 +888 +889 +890 +891 +892 +893 +894 +895 +896 +897 +898 +899 +900 +901 +902 +903 +904 +905 +906 +907 +908 +909 +910 +911 +912 +913 +914 +915 +916 +917 +918 +919 +920 +921 +922 +923 +924 +925 +926 +927 +928 +929 +930 +931 +932 +933 +934 +935 +936 +937 +938 +939 +940 +941 +942 +943 +944 +945 +946 +947 +948 +949 +950 +951 +952 +953 +954 +955 +956 +957 +958 +959 +960 +961 +962 +963 +964 +965 +966 +967 +968 +969 +970 +971 +972 +973 +974 +975 +976 +977 +978 +979 +980 +981 +982 +983 +984 +985 +986 +987 +988 +989 +990 +991 +992 +993 +994 +995 +996 +997 +998 +999 +1000 +1001 +1002 +1003 +1004 +1005 +1006 +1007 +1008 +1009 +1010 +1011 +1012 +1013 +1014 +1015 +1016 +1017 +1018 +1019 +1020 +1021 +1022 +1023 +1024 +1025 +1026 +1027 +1028 +1029 +1030 +1031 +1032 +1033 +1034 +1035 +1036 +1037 +1038 +1039 +1040 +1041 +1042 +1043 +1044 +1045 +1046 +1047 +1048 +1049 +1050 +1051 +1052 +1053 +1054 +1055 +1056 +1057 +1058 +1059 +1060 +1061 +1062 +1063 +1064 +1065 +1066 +1067 +1068 +1069 +1070 +1071 +1072 +1073 +1074 +1075 +1076 +1077 +1078 +1079 +1080 +1081 +1082 +1083 +1084 +1085 +1086 +1087 +1088 +1089 +1090 +1091 +1092 +1093 +1094 +1095 +1096 +1097 +1098 +1099 +1100 +1101 +1102 +1103 +1104 +1105 +1106 +1107 +1108 +1109 +1110 +1111 +1112 +1113 +1114 +1115 +1116 +1117 +1118 +1119 +1120 +1121 +1122 +1123 +1124 +1125 +1126 +1127 +1128 +1129 +1130 +1131 +1132 +1133 +1134 +1135 +1136 +1137 +1138 +1139 +1140 +1141 +1142 +1143 +1144 +1145 +1146 +1147 +1148 +1149 +1150 +1151 +1152 +1153 +1154 +1155 +1156 +1157 +1158 +1159 +1160 +1161 +1162 +1163 +1164 +1165 +1166 +1167 +1168 +1169 +1170 +1171 +1172 +1173 +1174 +1175 +1176 +1177 +1178 +1179 +1180 +1181 +1182 +1183 +1184 +1185 +1186 +1187 +1188 +1189 +1190 +1191 +1192 +1193 +1194 +1195 +1196 +1197 +1198 +1199 +1200 +1201 +1202 +1203 +1204 +1205 +1206 +1207 +1208 +1209 +1210 +1211 +1212 +1213 +1214 +1215 +1216 +1217 +1218 +1219 +1220 +1221 +1222 +1223 +1224 +1225 +1226 +1227 +1228 +1229 +1230 +1231 +1232 +1233 +1234 +1235 +1236 +1237 +1238 +1239 +1240 +1241 +1242 +1243 +1244 +1245 +1246 +1247 +1248 +1249 +1250 +1251 +1252 +1253 +1254 +1255 +1256 +1257 +1258 +1259 +1260 +1261 +1262 +1263 +1264 +1265 +1266 +1267 +1268 +1269 +1270 +1271 +1272 +1273 +1274 +1275 +1276 +1277 +1278 +1279 +1280 +1281 +1282 +1283 +1284 +1285 +1286 +1287 +1288 +1289 +1290 +1291 +1292 +1293 +1294 +1295 +1296 +1297 +1298 +1299 +1300 +1301 +1302 +1303 +1304 +1305 +1306 +1307 +1308 +1309 +1310 +1311 +1312 +1313 +1314 +1315 +1316 +1317 +1318 +1319 +1320 +1321 +1322 +1323 +1324 +1325 +1326 +1327 +1328 +1329 +1330 +1331 +1332 +1333 +1334 +1335 +1336 +1337 +1338 +1339 +1340 +1341 +1342 +1343 +1344 +1345 +1346 +1347 +1348 +1349 +1350 +1351 +1352 +1353 +1354 +1355 +1356 +1357 +1358 +1359 +1360 +1361 +1362 +1363 +1364 +1365 +1366 +1367 +1368 +1369 +1370 +1371 +1372 +1373 +1374 +1375 +1376 +1377 +1378 +1379 +1380 +1381 +1382 +1383 +1384 +1385 +1386 +1387 +1388 +1389 +1390 +1391 +1392 +1393 +1394 +1395 +1396 +1397 +1398 +1399 +1400 +1401 +1402 +1403 +1404 +1405 +1406 +1407 +1408 +1409 +1410 +1411 +1412 +1413 +1414 +1415 +1416 +1417 +1418 +1419 +1420 +1421 +1422 +1423 +1424 +1425 +1426 +1427 +1428 +1429 +1430 +1431 +1432 +1433 +1434 +1435 +1436 +1437 +1438 +1439 +1440 +1441 +1442 +1443 +1444 +1445 +1446 +1447 +1448 +1449 +1450 +1451 +1452 +1453 +1454 +1455 +1456 +1457 +1458 +1459 +1460 +1461 +1462 +1463 +1464 +1465 +1466 +1467 +1468 +1469 +1470 +1471 +1472 +1473 +1474 +1475 +1476 +1477 +1478 +1479 +1480 +1481 +1482 +1483 +1484 +1485 +1486 +1487 +1488 +1489 +1490 +1491 +1492 +1493 +1494 +1495 +1496 +1497 +1498 +1499 +1500 +1501 +1502 +1503 +1504 +1505 +1506 +1507 +1508 +1509 +1510 +1511 +1512 +1513 +1514 +1515 +1516 +1517 +1518 +1519 +1520 +1521 +1522 +1523 +1524 +1525 +1526 +1527 +1528 +1529 +1530 +1531 +1532 +1533 +1534 +1535 +1536 +1537 +1538 +1539 +1540 +1541 +1542 +1543 +1544 +1545 +1546 +1547 +1548 +1549 +1550 +1551 +1552 +1553 +1554 +1555 +1556 +1557 +1558 +1559 +1560 +1561 +1562 +1563 +1564 +1565 +1566 +1567 +1568 +1569 +1570 +1571 +1572 +1573 +1574 +1575 +1576 +1577 +1578 +1579 +1580 +1581 +1582 +1583 +1584 +1585 +1586 +1587 +1588 +1589 +1590 +1591 +1592 +1593 +1594 +1595 +1596 +1597 +1598 +1599 +1600 +1601 +1602 +1603 +1604 +1605 +1606 +1607 +1608 +1609 +1610 +1611 +1612 +1613 +1614 +1615 +1616 +1617 +1618 +1619 +1620 +1621 +1622 +1623 +1624 +1625 +1626 +1627 +1628 +1629 +1630 +1631 +1632 +1633 +
//! Define the "human" format style for list commands.
+
+use std::collections::BTreeMap;
+
+use super::{Node, Package, PackageManager, PackageManagerKind, Toolchain};
+use once_cell::sync::Lazy;
+use textwrap::{fill, Options};
+use volta_core::style::{text_width, tool_version, MAX_WIDTH};
+
+static INDENTATION: &str = " ";
+static NO_RUNTIME: &str = "⚡️ No Node runtimes installed!
+
+ You can install a runtime by running `volta install node`. See `volta help install` for
+ details and more options.";
+
+static TEXT_WIDTH: Lazy<usize> = Lazy::new(|| text_width().unwrap_or(MAX_WIDTH));
+
+#[allow(clippy::unnecessary_wraps)] // Needs to match the API of `plain::format`
+pub(super) fn format(toolchain: &Toolchain) -> Option<String> {
+ // Formatting here depends on the toolchain: we do different degrees of
+ // indentation
+ Some(match toolchain {
+ Toolchain::Node(runtimes) => display_node(runtimes),
+ Toolchain::Active {
+ runtime,
+ package_managers,
+ packages,
+ } => display_active(runtime, package_managers, packages),
+ Toolchain::All {
+ runtimes,
+ package_managers,
+ packages,
+ } => display_all(runtimes, package_managers, packages),
+ Toolchain::PackageManagers { kind, managers } => display_package_managers(*kind, managers),
+ Toolchain::Packages(packages) => display_packages(packages),
+ Toolchain::Tool {
+ name,
+ host_packages,
+ } => display_tool(name, host_packages),
+ })
+}
+
+/// Format the output for `Toolchain::Active`.
+///
+/// Accepts the components *from* the toolchain rather than the item itself so
+/// that
+fn display_active(
+ runtime: &Option<Box<Node>>,
+ package_managers: &[PackageManager],
+ packages: &[Package],
+) -> String {
+ match runtime {
+ None => NO_RUNTIME.to_string(),
+ Some(node) => {
+ let runtime_version = wrap(format!("Node: {}", format_runtime(node)));
+
+ let package_manager_versions = if package_managers.is_empty() {
+ String::new()
+ } else {
+ format!(
+ "\n{}",
+ format_package_manager_list_condensed(package_managers)
+ )
+ };
+
+ let package_versions = if packages.is_empty() {
+ wrap("Tool binaries available: NONE")
+ } else {
+ wrap(format!(
+ "Tool binaries available:\n{}",
+ format_tool_list(packages)
+ ))
+ };
+
+ format!(
+ "⚡️ Currently active tools:\n\n{}{}\n{}\n\n{}",
+ runtime_version,
+ package_manager_versions,
+ package_versions,
+ "See options for more detailed reports by running `volta list --help`."
+ )
+ }
+ }
+}
+
+/// Format the output for `Toolchain::All`.
+fn display_all(
+ runtimes: &[Node],
+ package_managers: &[PackageManager],
+ packages: &[Package],
+) -> String {
+ if runtimes.is_empty() {
+ NO_RUNTIME.to_string()
+ } else {
+ let runtime_versions: String =
+ wrap(format!("Node runtimes:\n{}", format_runtime_list(runtimes)));
+ let package_manager_versions: String = wrap(format!(
+ "Package managers:\n{}",
+ format_package_manager_list_verbose(package_managers)
+ ));
+ let package_versions = wrap(format!("Packages:\n{}", format_package_list(packages)));
+ format!(
+ "⚡️ User toolchain:\n\n{}\n\n{}\n\n{}",
+ runtime_versions, package_manager_versions, package_versions
+ )
+ }
+}
+
+/// Format a set of `Toolchain::Node`s.
+fn display_node(runtimes: &[Node]) -> String {
+ if runtimes.is_empty() {
+ NO_RUNTIME.to_string()
+ } else {
+ format!(
+ "⚡️ Node runtimes in your toolchain:\n\n{}",
+ format_runtime_list(runtimes)
+ )
+ }
+}
+
+/// Format a set of `Toolchain::PackageManager`s for `volta list npm`
+fn display_npms(managers: &[PackageManager]) -> String {
+ if managers.is_empty() {
+ "⚡️ No custom npm versions installed (npm is still available bundled with Node).
+
+You can install an npm version by running `volta install npm`.
+See `volta help install` for details and more options."
+ .into()
+ } else {
+ let versions = wrap(
+ managers
+ .iter()
+ .map(format_package_manager)
+ .collect::<Vec<_>>()
+ .join("\n"),
+ );
+ format!("⚡️ Custom npm versions in your toolchain:\n\n{}", versions)
+ }
+}
+
+/// Format a set of `Toolchain::PackageManager`s.
+fn display_package_managers(kind: PackageManagerKind, managers: &[PackageManager]) -> String {
+ match kind {
+ PackageManagerKind::Npm => display_npms(managers),
+ _ => {
+ if managers.is_empty() {
+ // Note: Using `format_package_manager_kind` to get the properly capitalized version of the tool
+ // Then using the `Display` impl on the kind to get the version to show in the command
+ format!(
+ "⚡️ No {} versions installed.
+
+You can install a {0} version by running `volta install {}`.
+See `volta help install` for details and more options.",
+ format_package_manager_kind(kind),
+ kind
+ )
+ } else {
+ let versions = wrap(
+ managers
+ .iter()
+ .map(format_package_manager)
+ .collect::<Vec<String>>()
+ .join("\n"),
+ );
+ format!(
+ "⚡️ {} versions in your toolchain:\n\n{}",
+ format_package_manager_kind(kind),
+ versions
+ )
+ }
+ }
+ }
+}
+
+/// Format a set of `Toolchain::Package`s and their associated tools.
+fn display_packages(packages: &[Package]) -> String {
+ if packages.is_empty() {
+ String::from(
+ "⚡️ No tools or packages installed.
+
+You can safely install packages by running `volta install <package name>`.
+See `volta help install` for details and more options.",
+ )
+ } else {
+ format!(
+ "⚡️ Package versions in your toolchain:\n\n{}",
+ format_package_list(packages)
+ )
+ }
+}
+
+/// Format a single `Toolchain::Tool` with associated `Toolchain::Package`
+
+fn display_tool(tool: &str, host_packages: &[Package]) -> String {
+ if host_packages.is_empty() {
+ format!(
+ "⚡️ No tools or packages named `{}` installed.
+
+You can safely install packages by running `volta install <package name>`.
+See `volta help install` for details and more options.",
+ tool
+ )
+ } else {
+ let versions = wrap(
+ host_packages
+ .iter()
+ .map(format_package)
+ .collect::<Vec<String>>()
+ .join("\n"),
+ );
+ format!("⚡️ Tool `{}` available from:\n\n{}", tool, versions)
+ }
+}
+
+/// Format a list of `Toolchain::Package`s without detail information
+fn format_tool_list(packages: &[Package]) -> String {
+ packages
+ .iter()
+ .map(format_tool)
+ .collect::<Vec<String>>()
+ .join("\n")
+}
+/// Format a single `Toolchain::Package` without detail information
+fn format_tool(package: &Package) -> String {
+ match package {
+ Package::Default { tools, .. } | Package::Project { tools, .. } => {
+ let tools = match tools.len() {
+ 0 => String::from(""),
+ _ => tools.join(", "),
+ };
+ wrap(format!("{}{}", tools, list_package_source(package)))
+ }
+ Package::Fetched(..) => String::new(),
+ }
+}
+
+/// format a list of `Toolchain::Node`s.
+fn format_runtime_list(runtimes: &[Node]) -> String {
+ wrap(
+ runtimes
+ .iter()
+ .map(format_runtime)
+ .collect::<Vec<String>>()
+ .join("\n"),
+ )
+}
+
+/// format a single version of `Toolchain::Node`.
+fn format_runtime(runtime: &Node) -> String {
+ format!("v{}{}", runtime.version, runtime.source)
+}
+
+/// format a list of `Toolchain::PackageManager`s in condensed form
+fn format_package_manager_list_condensed(package_managers: &[PackageManager]) -> String {
+ wrap(
+ package_managers
+ .iter()
+ .map(|manager| {
+ format!(
+ "{}: {}",
+ format_package_manager_kind(manager.kind),
+ format_package_manager(manager)
+ )
+ })
+ .collect::<Vec<_>>()
+ .join("\n"),
+ )
+}
+
+/// format a list of `Toolchain::PackageManager`s in verbose form
+fn format_package_manager_list_verbose(package_managers: &[PackageManager]) -> String {
+ let mut manager_lists = BTreeMap::new();
+
+ for manager in package_managers {
+ manager_lists
+ .entry(manager.kind)
+ .or_insert_with(Vec::new)
+ .push(format_package_manager(manager));
+ }
+
+ wrap(
+ manager_lists
+ .iter()
+ .map(|(kind, list)| {
+ format!(
+ "{}:\n{}",
+ format_package_manager_kind(*kind),
+ wrap(list.join("\n"))
+ )
+ })
+ .collect::<Vec<_>>()
+ .join("\n"),
+ )
+}
+
+/// format a single `Toolchain::PackageManager`.
+fn format_package_manager(package_manager: &PackageManager) -> String {
+ format!("v{}{}", package_manager.version, package_manager.source)
+}
+
+/// format the title for a kind of package manager
+///
+/// This is distinct from the `Display` impl, because we need 'Yarn' to be capitalized for human output
+fn format_package_manager_kind(kind: PackageManagerKind) -> String {
+ match kind {
+ PackageManagerKind::Npm => "npm".into(),
+ PackageManagerKind::Pnpm => "pnpm".into(),
+ PackageManagerKind::Yarn => "Yarn".into(),
+ }
+}
+
+/// format a list of `Toolchain::Package`s and their associated tools.
+fn format_package_list(packages: &[Package]) -> String {
+ wrap(
+ packages
+ .iter()
+ .map(format_package)
+ .collect::<Vec<String>>()
+ .join("\n"),
+ )
+}
+
+/// Format a single `Toolchain::Package` and its associated tools.
+fn format_package(package: &Package) -> String {
+ match package {
+ Package::Default {
+ details,
+ node,
+ tools,
+ ..
+ } => {
+ let tools = match tools.len() {
+ 0 => String::from(""),
+ _ => tools.join(", "),
+ };
+
+ let version = format!("{}{}", details.version, list_package_source(package));
+ let binaries = wrap(format!("binary tools: {}", tools));
+ let platform_detail = wrap(format!(
+ "runtime: {}\npackage manager: {}",
+ tool_version("node", node),
+ // TODO: Should be updated when we support installing with custom package_managers,
+ // whether Yarn or non-built-in versions of npm
+ "npm@built-in"
+ ));
+ let platform = wrap(format!("platform:\n{}", platform_detail));
+ format!("{}@{}\n{}\n{}", details.name, version, binaries, platform)
+ }
+ Package::Project { name, tools, .. } => {
+ let tools = match tools.len() {
+ 0 => String::from(""),
+ _ => tools.join(", "),
+ };
+
+ let binaries = wrap(format!("binary tools: {}", tools));
+ format!("{}{}\n{}", name, list_package_source(package), binaries)
+ }
+ Package::Fetched(details) => {
+ let package_info = format!("{}@{}", details.name, details.version);
+ let footer_message = format!(
+ "To make it available to execute, run `volta install {}`.",
+ package_info
+ );
+ format!("{}\n\n{}", package_info, footer_message)
+ }
+ }
+}
+
+/// List a the source from a `Toolchain::Package`.
+fn list_package_source(package: &Package) -> String {
+ match package {
+ Package::Default { .. } => String::from(" (default)"),
+ Package::Project { path, .. } => format!(" (current @ {})", path.display()),
+ Package::Fetched(..) => String::new(),
+ }
+}
+
+/// Wrap and indent the output
+fn wrap<S>(text: S) -> String
+where
+ S: AsRef<str>,
+{
+ let options = Options::new(*TEXT_WIDTH)
+ .initial_indent(INDENTATION)
+ .subsequent_indent(INDENTATION);
+
+ let mut wrapped = fill(text.as_ref(), options);
+ // The `fill` method in the latest `textwrap` version does not trim the indentation whitespace
+ // from the final line.
+ wrapped.truncate(wrapped.trim_end_matches(INDENTATION).len());
+ wrapped
+}
+
+// These tests are organized by way of the *commands* supplied to `list`, unlike
+// in the `plain` module, because the formatting varies by command here, as it
+// does not there.
+#[cfg(test)]
+mod tests {
+ use std::path::PathBuf;
+
+ use node_semver::Version;
+ use once_cell::sync::Lazy;
+
+ use super::*;
+
+ static NODE_12: Lazy<Version> = Lazy::new(|| Version::from((12, 2, 0)));
+ static NODE_11: Lazy<Version> = Lazy::new(|| Version::from((11, 9, 0)));
+ static NODE_10: Lazy<Version> = Lazy::new(|| Version::from((10, 15, 3)));
+ static YARN_VERSION: Lazy<Version> = Lazy::new(|| Version::from((1, 16, 0)));
+ static NPM_VERSION: Lazy<Version> = Lazy::new(|| Version::from((6, 13, 1)));
+ static PROJECT_PATH: Lazy<PathBuf> = Lazy::new(|| PathBuf::from("~/path/to/project.json"));
+
+ mod active {
+ use super::*;
+ use crate::command::list::{
+ human::display_active, Node, PackageDetails, PackageManager, PackageManagerKind, Source,
+ };
+
+ #[test]
+ fn no_runtimes() {
+ let runtime = None;
+ let package_managers = vec![];
+ let packages = vec![];
+ assert_eq!(
+ display_active(&runtime, &package_managers, &packages),
+ NO_RUNTIME
+ );
+ }
+
+ #[test]
+ fn runtime_only_default() {
+ let expected = "⚡️ Currently active tools:
+
+ Node: v12.2.0 (default)
+ Tool binaries available: NONE
+
+See options for more detailed reports by running `volta list --help`.";
+
+ let runtime = Some(Box::new(Node {
+ source: Source::Default,
+ version: NODE_12.clone(),
+ }));
+ let package_managers = vec![];
+ let packages = vec![];
+
+ assert_eq!(
+ display_active(&runtime, &package_managers, &packages),
+ expected
+ );
+ }
+
+ #[test]
+ fn runtime_only_project() {
+ let expected = "⚡️ Currently active tools:
+
+ Node: v12.2.0 (current @ ~/path/to/project.json)
+ Tool binaries available: NONE
+
+See options for more detailed reports by running `volta list --help`.";
+
+ let runtime = Some(Box::new(Node {
+ source: Source::Project(PROJECT_PATH.clone()),
+ version: NODE_12.clone(),
+ }));
+ let package_managers = vec![];
+ let packages = vec![];
+
+ assert_eq!(
+ display_active(&runtime, &package_managers, &packages),
+ expected
+ );
+ }
+
+ #[test]
+ fn runtime_and_npm_default() {
+ let expected = "⚡️ Currently active tools:
+
+ Node: v12.2.0 (default)
+ npm: v6.13.1 (default)
+ Tool binaries available: NONE
+
+See options for more detailed reports by running `volta list --help`.";
+
+ let runtime = Some(Box::new(Node {
+ source: Source::Default,
+ version: NODE_12.clone(),
+ }));
+ let package_managers = vec![PackageManager {
+ kind: PackageManagerKind::Npm,
+ source: Source::Default,
+ version: NPM_VERSION.clone(),
+ }];
+ let packages = vec![];
+
+ assert_eq!(
+ display_active(&runtime, &package_managers, &packages),
+ expected
+ );
+ }
+
+ #[test]
+ fn runtime_and_yarn_default() {
+ let expected = "⚡️ Currently active tools:
+
+ Node: v12.2.0 (default)
+ Yarn: v1.16.0 (default)
+ Tool binaries available: NONE
+
+See options for more detailed reports by running `volta list --help`.";
+
+ let runtime = Some(Box::new(Node {
+ source: Source::Default,
+ version: NODE_12.clone(),
+ }));
+ let package_managers = vec![PackageManager {
+ kind: PackageManagerKind::Yarn,
+ source: Source::Default,
+ version: YARN_VERSION.clone(),
+ }];
+ let packages = vec![];
+
+ assert_eq!(
+ display_active(&runtime, &package_managers, &packages),
+ expected
+ );
+ }
+
+ #[test]
+ fn runtime_and_npm_mixed() {
+ let expected = "⚡️ Currently active tools:
+
+ Node: v12.2.0 (default)
+ npm: v6.13.1 (current @ ~/path/to/project.json)
+ Tool binaries available: NONE
+
+See options for more detailed reports by running `volta list --help`.";
+
+ let runtime = Some(Box::new(Node {
+ source: Source::Default,
+ version: NODE_12.clone(),
+ }));
+ let package_managers = vec![PackageManager {
+ kind: PackageManagerKind::Npm,
+ source: Source::Project(PROJECT_PATH.clone()),
+ version: NPM_VERSION.clone(),
+ }];
+ let packages = vec![];
+
+ assert_eq!(
+ display_active(&runtime, &package_managers, &packages),
+ expected
+ );
+ }
+
+ #[test]
+ fn runtime_and_yarn_mixed() {
+ let expected = "⚡️ Currently active tools:
+
+ Node: v12.2.0 (default)
+ Yarn: v1.16.0 (current @ ~/path/to/project.json)
+ Tool binaries available: NONE
+
+See options for more detailed reports by running `volta list --help`.";
+
+ let runtime = Some(Box::new(Node {
+ source: Source::Default,
+ version: NODE_12.clone(),
+ }));
+ let package_managers = vec![PackageManager {
+ kind: PackageManagerKind::Yarn,
+ source: Source::Project(PROJECT_PATH.clone()),
+ version: YARN_VERSION.clone(),
+ }];
+ let packages = vec![];
+
+ assert_eq!(
+ display_active(&runtime, &package_managers, &packages),
+ expected
+ );
+ }
+
+ #[test]
+ fn runtime_and_npm_project() {
+ let expected = "⚡️ Currently active tools:
+
+ Node: v12.2.0 (current @ ~/path/to/project.json)
+ npm: v6.13.1 (current @ ~/path/to/project.json)
+ Tool binaries available: NONE
+
+See options for more detailed reports by running `volta list --help`.";
+
+ let runtime = Some(Box::new(Node {
+ source: Source::Project(PROJECT_PATH.clone()),
+ version: NODE_12.clone(),
+ }));
+ let package_managers = vec![PackageManager {
+ kind: PackageManagerKind::Npm,
+ source: Source::Project(PROJECT_PATH.clone()),
+ version: NPM_VERSION.clone(),
+ }];
+ let packages = vec![];
+
+ assert_eq!(
+ display_active(&runtime, &package_managers, &packages),
+ expected
+ );
+ }
+
+ #[test]
+ fn runtime_and_yarn_project() {
+ let expected = "⚡️ Currently active tools:
+
+ Node: v12.2.0 (current @ ~/path/to/project.json)
+ Yarn: v1.16.0 (current @ ~/path/to/project.json)
+ Tool binaries available: NONE
+
+See options for more detailed reports by running `volta list --help`.";
+
+ let runtime = Some(Box::new(Node {
+ source: Source::Project(PROJECT_PATH.clone()),
+ version: NODE_12.clone(),
+ }));
+ let package_managers = vec![PackageManager {
+ kind: PackageManagerKind::Yarn,
+ source: Source::Project(PROJECT_PATH.clone()),
+ version: YARN_VERSION.clone(),
+ }];
+ let packages = vec![];
+
+ assert_eq!(
+ display_active(&runtime, &package_managers, &packages),
+ expected
+ );
+ }
+
+ #[test]
+ fn runtime_npm_and_yarn_default() {
+ let expected = "⚡️ Currently active tools:
+
+ Node: v12.2.0 (default)
+ npm: v6.13.1 (default)
+ Yarn: v1.16.0 (default)
+ Tool binaries available: NONE
+
+See options for more detailed reports by running `volta list --help`.";
+
+ let runtime = Some(Box::new(Node {
+ source: Source::Default,
+ version: NODE_12.clone(),
+ }));
+ let package_managers = vec![
+ PackageManager {
+ kind: PackageManagerKind::Npm,
+ source: Source::Default,
+ version: NPM_VERSION.clone(),
+ },
+ PackageManager {
+ kind: PackageManagerKind::Yarn,
+ source: Source::Default,
+ version: YARN_VERSION.clone(),
+ },
+ ];
+ let packages = vec![];
+
+ assert_eq!(
+ display_active(&runtime, &package_managers, &packages),
+ expected
+ );
+ }
+
+ #[test]
+ fn runtime_npm_and_yarn_project() {
+ let expected = "⚡️ Currently active tools:
+
+ Node: v12.2.0 (current @ ~/path/to/project.json)
+ npm: v6.13.1 (current @ ~/path/to/project.json)
+ Yarn: v1.16.0 (current @ ~/path/to/project.json)
+ Tool binaries available: NONE
+
+See options for more detailed reports by running `volta list --help`.";
+
+ let runtime = Some(Box::new(Node {
+ source: Source::Project(PROJECT_PATH.clone()),
+ version: NODE_12.clone(),
+ }));
+ let package_managers = vec![
+ PackageManager {
+ kind: PackageManagerKind::Npm,
+ source: Source::Project(PROJECT_PATH.clone()),
+ version: NPM_VERSION.clone(),
+ },
+ PackageManager {
+ kind: PackageManagerKind::Yarn,
+ source: Source::Project(PROJECT_PATH.clone()),
+ version: YARN_VERSION.clone(),
+ },
+ ];
+ let packages = vec![];
+
+ assert_eq!(
+ display_active(&runtime, &package_managers, &packages),
+ expected
+ );
+ }
+
+ #[test]
+ fn runtime_npm_and_yarn_mixed() {
+ let expected = "⚡️ Currently active tools:
+
+ Node: v12.2.0 (default)
+ npm: v6.13.1 (current @ ~/path/to/project.json)
+ Yarn: v1.16.0 (default)
+ Tool binaries available: NONE
+
+See options for more detailed reports by running `volta list --help`.";
+
+ let runtime = Some(Box::new(Node {
+ source: Source::Default,
+ version: NODE_12.clone(),
+ }));
+ let package_managers = vec![
+ PackageManager {
+ kind: PackageManagerKind::Npm,
+ source: Source::Project(PROJECT_PATH.clone()),
+ version: NPM_VERSION.clone(),
+ },
+ PackageManager {
+ kind: PackageManagerKind::Yarn,
+ source: Source::Default,
+ version: YARN_VERSION.clone(),
+ },
+ ];
+ let packages = vec![];
+
+ assert_eq!(
+ display_active(&runtime, &package_managers, &packages),
+ expected
+ );
+ }
+
+ #[test]
+ fn with_default_tools() {
+ let expected = "⚡️ Currently active tools:
+
+ Node: v12.2.0 (current @ ~/path/to/project.json)
+ npm: v6.13.1 (current @ ~/path/to/project.json)
+ Yarn: v1.16.0 (current @ ~/path/to/project.json)
+ Tool binaries available:
+ create-react-app (default)
+ tsc, tsserver (default)
+
+See options for more detailed reports by running `volta list --help`.";
+
+ let runtime = Some(Box::new(Node {
+ source: Source::Project(PROJECT_PATH.clone()),
+ version: NODE_12.clone(),
+ }));
+ let package_managers = vec![
+ PackageManager {
+ kind: PackageManagerKind::Npm,
+ source: Source::Project(PROJECT_PATH.clone()),
+ version: NPM_VERSION.clone(),
+ },
+ PackageManager {
+ kind: PackageManagerKind::Yarn,
+ source: Source::Project(PROJECT_PATH.clone()),
+ version: YARN_VERSION.clone(),
+ },
+ ];
+ let packages = vec![
+ Package::Default {
+ details: PackageDetails {
+ name: "create-react-app".to_string(),
+ version: Version::from((3, 0, 1)),
+ },
+ node: NODE_12.clone(),
+ tools: vec!["create-react-app".to_string()],
+ },
+ Package::Default {
+ details: PackageDetails {
+ name: "typescript".to_string(),
+ version: Version::from((3, 4, 3)),
+ },
+ node: NODE_12.clone(),
+ tools: vec!["tsc".to_string(), "tsserver".to_string()],
+ },
+ ];
+
+ assert_eq!(
+ display_active(&runtime, &package_managers, &packages),
+ expected
+ );
+ }
+
+ #[test]
+ fn with_project_tools() {
+ let expected = "⚡️ Currently active tools:
+
+ Node: v12.2.0 (current @ ~/path/to/project.json)
+ npm: v6.13.1 (current @ ~/path/to/project.json)
+ Yarn: v1.16.0 (current @ ~/path/to/project.json)
+ Tool binaries available:
+ create-react-app (current @ ~/path/to/project.json)
+ tsc, tsserver (default)
+
+See options for more detailed reports by running `volta list --help`.";
+
+ let runtime = Some(Box::new(Node {
+ source: Source::Project(PROJECT_PATH.clone()),
+ version: NODE_12.clone(),
+ }));
+ let package_managers = vec![
+ PackageManager {
+ kind: PackageManagerKind::Npm,
+ source: Source::Project(PROJECT_PATH.clone()),
+ version: NPM_VERSION.clone(),
+ },
+ PackageManager {
+ kind: PackageManagerKind::Yarn,
+ source: Source::Project(PROJECT_PATH.clone()),
+ version: YARN_VERSION.clone(),
+ },
+ ];
+ let packages = vec![
+ Package::Project {
+ name: "create-react-app".to_string(),
+ path: PROJECT_PATH.clone(),
+ tools: vec!["create-react-app".to_string()],
+ },
+ Package::Default {
+ details: PackageDetails {
+ name: "typescript".to_string(),
+ version: Version::from((3, 4, 3)),
+ },
+ node: NODE_12.clone(),
+ tools: vec!["tsc".to_string(), "tsserver".to_string()],
+ },
+ ];
+
+ assert_eq!(
+ display_active(&runtime, &package_managers, &packages),
+ expected
+ );
+ }
+ }
+
+ mod node {
+ use super::super::*;
+ use super::*;
+ use crate::command::list::Source;
+
+ #[test]
+ fn no_runtimes() {
+ let expected = NO_RUNTIME;
+
+ let runtimes = [];
+ assert_eq!(display_node(&runtimes).as_str(), expected);
+ }
+
+ #[test]
+ fn single_default() {
+ let expected = "⚡️ Node runtimes in your toolchain:
+
+ v10.15.3 (default)";
+ let runtimes = [Node {
+ source: Source::Default,
+ version: NODE_10.clone(),
+ }];
+
+ assert_eq!(display_node(&runtimes).as_str(), expected);
+ }
+
+ #[test]
+ fn single_project() {
+ let expected = "⚡️ Node runtimes in your toolchain:
+
+ v12.2.0 (current @ ~/path/to/project.json)";
+
+ let runtimes = [Node {
+ source: Source::Project(PROJECT_PATH.clone()),
+ version: NODE_12.clone(),
+ }];
+
+ assert_eq!(display_node(&runtimes).as_str(), expected);
+ }
+
+ #[test]
+ fn single_installed() {
+ let expected = "⚡️ Node runtimes in your toolchain:
+
+ v11.9.0";
+
+ let runtimes = [Node {
+ source: Source::None,
+ version: NODE_11.clone(),
+ }];
+
+ assert_eq!(display_node(&runtimes).as_str(), expected);
+ }
+
+ #[test]
+ fn multi() {
+ let expected = "⚡️ Node runtimes in your toolchain:
+
+ v12.2.0 (current @ ~/path/to/project.json)
+ v11.9.0
+ v10.15.3 (default)";
+
+ let runtimes = [
+ Node {
+ source: Source::Project(PROJECT_PATH.clone()),
+ version: NODE_12.clone(),
+ },
+ Node {
+ source: Source::None,
+ version: NODE_11.clone(),
+ },
+ Node {
+ source: Source::Default,
+ version: NODE_10.clone(),
+ },
+ ];
+
+ assert_eq!(display_node(&runtimes), expected);
+ }
+ }
+
+ mod package_managers {
+ use super::*;
+ use crate::command::list::{PackageManager, PackageManagerKind, Source};
+
+ #[test]
+ fn none_installed_npm() {
+ let expected =
+ "⚡️ No custom npm versions installed (npm is still available bundled with Node).
+
+You can install an npm version by running `volta install npm`.
+See `volta help install` for details and more options.";
+
+ assert_eq!(
+ display_package_managers(PackageManagerKind::Npm, &[]),
+ expected
+ );
+ }
+
+ #[test]
+ fn none_installed_yarn() {
+ let expected = "⚡️ No Yarn versions installed.
+
+You can install a Yarn version by running `volta install yarn`.
+See `volta help install` for details and more options.";
+
+ assert_eq!(
+ display_package_managers(PackageManagerKind::Yarn, &[]),
+ expected
+ );
+ }
+
+ #[test]
+ fn single_default_npm() {
+ let expected = "⚡️ Custom npm versions in your toolchain:
+
+ v6.13.1 (default)";
+
+ let package_managers = [PackageManager {
+ kind: PackageManagerKind::Npm,
+ source: Source::Default,
+ version: NPM_VERSION.clone(),
+ }];
+
+ assert_eq!(
+ display_package_managers(PackageManagerKind::Npm, &package_managers),
+ expected
+ );
+ }
+
+ #[test]
+ fn single_default_yarn() {
+ let expected = "⚡️ Yarn versions in your toolchain:
+
+ v1.16.0 (default)";
+
+ let package_managers = [PackageManager {
+ kind: PackageManagerKind::Yarn,
+ source: Source::Default,
+ version: YARN_VERSION.clone(),
+ }];
+
+ assert_eq!(
+ display_package_managers(PackageManagerKind::Yarn, &package_managers),
+ expected
+ );
+ }
+
+ #[test]
+ fn single_project_npm() {
+ let expected = "⚡️ Custom npm versions in your toolchain:
+
+ v6.13.1 (current @ ~/path/to/project.json)";
+
+ let package_managers = [PackageManager {
+ kind: PackageManagerKind::Npm,
+ source: Source::Project(PROJECT_PATH.clone()),
+ version: NPM_VERSION.clone(),
+ }];
+
+ assert_eq!(
+ display_package_managers(PackageManagerKind::Npm, &package_managers),
+ expected
+ );
+ }
+
+ #[test]
+ fn single_project_yarn() {
+ let expected = "⚡️ Yarn versions in your toolchain:
+
+ v1.16.0 (current @ ~/path/to/project.json)";
+
+ let package_managers = [PackageManager {
+ kind: PackageManagerKind::Yarn,
+ source: Source::Project(PROJECT_PATH.clone()),
+ version: YARN_VERSION.clone(),
+ }];
+
+ assert_eq!(
+ display_package_managers(PackageManagerKind::Yarn, &package_managers),
+ expected
+ );
+ }
+
+ #[test]
+ fn single_installed_npm() {
+ let expected = "⚡️ Custom npm versions in your toolchain:
+
+ v6.13.1";
+
+ let package_managers = [PackageManager {
+ kind: PackageManagerKind::Npm,
+ source: Source::None,
+ version: NPM_VERSION.clone(),
+ }];
+
+ assert_eq!(
+ display_package_managers(PackageManagerKind::Npm, &package_managers),
+ expected
+ );
+ }
+
+ #[test]
+ fn single_installed_yarn() {
+ let expected = "⚡️ Yarn versions in your toolchain:
+
+ v1.16.0";
+
+ let package_managers = [PackageManager {
+ kind: PackageManagerKind::Yarn,
+ source: Source::None,
+ version: YARN_VERSION.clone(),
+ }];
+
+ assert_eq!(
+ display_package_managers(PackageManagerKind::Yarn, &package_managers),
+ expected
+ );
+ }
+
+ #[test]
+ fn multi_npm() {
+ let expected = "⚡️ Custom npm versions in your toolchain:
+
+ v5.6.0
+ v6.13.1 (default)
+ v6.14.2 (current @ ~/path/to/project.json)";
+
+ let package_managers = [
+ PackageManager {
+ kind: PackageManagerKind::Npm,
+ source: Source::None,
+ version: Version::from((5, 6, 0)),
+ },
+ PackageManager {
+ kind: PackageManagerKind::Npm,
+ source: Source::Default,
+ version: NPM_VERSION.clone(),
+ },
+ PackageManager {
+ kind: PackageManagerKind::Npm,
+ source: Source::Project(PROJECT_PATH.clone()),
+ version: Version::from((6, 14, 2)),
+ },
+ ];
+
+ assert_eq!(
+ display_package_managers(PackageManagerKind::Npm, &package_managers),
+ expected
+ );
+ }
+
+ #[test]
+ fn multi_yarn() {
+ let expected = "⚡️ Yarn versions in your toolchain:
+
+ v1.3.0
+ v1.16.0 (default)
+ v1.17.0 (current @ ~/path/to/project.json)";
+
+ let package_managers = [
+ PackageManager {
+ kind: PackageManagerKind::Yarn,
+ source: Source::None,
+ version: Version::from((1, 3, 0)),
+ },
+ PackageManager {
+ kind: PackageManagerKind::Yarn,
+ source: Source::Default,
+ version: YARN_VERSION.clone(),
+ },
+ PackageManager {
+ kind: PackageManagerKind::Yarn,
+ source: Source::Project(PROJECT_PATH.clone()),
+ version: Version::from((1, 17, 0)),
+ },
+ ];
+
+ assert_eq!(
+ display_package_managers(PackageManagerKind::Yarn, &package_managers),
+ expected
+ );
+ }
+ }
+
+ mod packages {
+ use super::*;
+ use crate::command::list::{Package, PackageDetails};
+ use node_semver::Version;
+
+ #[test]
+ fn none() {
+ let expected = "⚡️ No tools or packages installed.
+
+You can safely install packages by running `volta install <package name>`.
+See `volta help install` for details and more options.";
+
+ assert_eq!(display_packages(&[]), expected);
+ }
+
+ #[test]
+ fn single_default() {
+ let expected = "⚡️ Package versions in your toolchain:
+
+ ember-cli@3.10.1 (default)
+ binary tools: ember
+ platform:
+ runtime: node@12.2.0
+ package manager: npm@built-in";
+
+ let packages = [Package::Default {
+ details: PackageDetails {
+ name: "ember-cli".to_string(),
+ version: Version::from((3, 10, 1)),
+ },
+ node: NODE_12.clone(),
+ tools: vec!["ember".to_string()],
+ }];
+
+ assert_eq!(display_packages(&packages), expected);
+ }
+
+ #[test]
+ fn single_project() {
+ let expected = "⚡️ Package versions in your toolchain:
+
+ ember-cli (current @ ~/path/to/project.json)
+ binary tools: ember";
+
+ let packages = [Package::Project {
+ name: "ember-cli".to_string(),
+ path: PROJECT_PATH.clone(),
+ tools: vec!["ember".to_string()],
+ }];
+
+ assert_eq!(display_packages(&packages), expected);
+ }
+
+ #[test]
+ fn single_fetched() {
+ let expected = "⚡️ Package versions in your toolchain:
+
+ ember-cli@3.10.1
+
+ To make it available to execute, run `volta install ember-cli@3.10.1`.";
+
+ let packages = [Package::Fetched(PackageDetails {
+ name: "ember-cli".to_string(),
+ version: Version::from((3, 10, 1)),
+ })];
+
+ assert_eq!(display_packages(&packages), expected);
+ }
+
+ #[test]
+ fn multi_fetched() {
+ let expected = "⚡️ Package versions in your toolchain:
+
+ ember-cli@3.10.1
+
+ To make it available to execute, run `volta install ember-cli@3.10.1`.
+ ember-cli@3.8.2
+
+ To make it available to execute, run `volta install ember-cli@3.8.2`.";
+
+ let packages = [
+ Package::Fetched(PackageDetails {
+ name: "ember-cli".to_string(),
+ version: Version::from((3, 10, 1)),
+ }),
+ Package::Fetched(PackageDetails {
+ name: "ember-cli".to_string(),
+ version: Version::from((3, 8, 2)),
+ }),
+ ];
+
+ assert_eq!(display_packages(&packages), expected);
+ }
+
+ #[test]
+ fn multi() {
+ let expected = "⚡️ Package versions in your toolchain:
+
+ ember-cli@3.10.1 (default)
+ binary tools: ember
+ platform:
+ runtime: node@12.2.0
+ package manager: npm@built-in
+ ember-cli (current @ ~/path/to/project.json)
+ binary tools: ember";
+
+ let packages = [
+ Package::Default {
+ details: PackageDetails {
+ name: "ember-cli".to_string(),
+ version: Version::from((3, 10, 1)),
+ },
+ node: NODE_12.clone(),
+ tools: vec!["ember".to_string()],
+ },
+ Package::Project {
+ name: "ember-cli".to_string(),
+ path: PROJECT_PATH.clone(),
+ tools: vec!["ember".to_string()],
+ },
+ ];
+
+ assert_eq!(display_packages(&packages), expected);
+ }
+ }
+
+ mod tools {
+ use super::*;
+ use crate::command::list::{Package, PackageDetails};
+ use node_semver::Version;
+
+ #[test]
+ fn none() {
+ let expected = "⚡️ No tools or packages named `ember` installed.
+
+You can safely install packages by running `volta install <package name>`.
+See `volta help install` for details and more options.";
+
+ assert_eq!(display_tool("ember", &[]), expected);
+ }
+
+ #[test]
+ fn single_default() {
+ let expected = "⚡️ Tool `ember` available from:
+
+ ember-cli@3.10.1 (default)
+ binary tools: ember
+ platform:
+ runtime: node@12.2.0
+ package manager: npm@built-in";
+
+ let packages = [Package::Default {
+ details: PackageDetails {
+ name: "ember-cli".to_string(),
+ version: Version::from((3, 10, 1)),
+ },
+ node: NODE_12.clone(),
+ tools: vec!["ember".to_string()],
+ }];
+
+ assert_eq!(display_tool("ember", &packages), expected);
+ }
+
+ #[test]
+ fn single_project() {
+ let expected = "⚡️ Tool `ember` available from:
+
+ ember-cli (current @ ~/path/to/project.json)
+ binary tools: ember";
+
+ let packages = [Package::Project {
+ name: "ember-cli".to_string(),
+ path: PROJECT_PATH.clone(),
+ tools: vec!["ember".to_string()],
+ }];
+
+ assert_eq!(display_tool("ember", &packages), expected);
+ }
+
+ #[test]
+ fn single_fetched() {
+ let expected = "⚡️ Tool `ember` available from:
+
+ ember-cli@3.10.1
+
+ To make it available to execute, run `volta install ember-cli@3.10.1`.";
+
+ let packages = [Package::Fetched(PackageDetails {
+ name: "ember-cli".to_string(),
+ version: Version::from((3, 10, 1)),
+ })];
+
+ assert_eq!(display_tool("ember", &packages), expected);
+ }
+
+ #[test]
+ fn multi_fetched() {
+ let expected = "⚡️ Tool `ember` available from:
+
+ ember-cli@3.10.1
+
+ To make it available to execute, run `volta install ember-cli@3.10.1`.
+ ember-cli@3.8.2
+
+ To make it available to execute, run `volta install ember-cli@3.8.2`.";
+
+ let packages = [
+ Package::Fetched(PackageDetails {
+ name: "ember-cli".to_string(),
+ version: Version::from((3, 10, 1)),
+ }),
+ Package::Fetched(PackageDetails {
+ name: "ember-cli".to_string(),
+ version: Version::from((3, 8, 2)),
+ }),
+ ];
+
+ assert_eq!(display_tool("ember", &packages), expected);
+ }
+
+ #[test]
+ fn multi() {
+ let expected = "⚡️ Tool `ember` available from:
+
+ ember-cli@3.10.1 (default)
+ binary tools: ember
+ platform:
+ runtime: node@12.2.0
+ package manager: npm@built-in
+ ember-cli (current @ ~/path/to/project.json)
+ binary tools: ember";
+
+ let packages = [
+ Package::Default {
+ details: PackageDetails {
+ name: "ember-cli".to_string(),
+ version: Version::from((3, 10, 1)),
+ },
+ node: NODE_12.clone(),
+ tools: vec!["ember".to_string()],
+ },
+ Package::Project {
+ name: "ember-cli".to_string(),
+ path: PROJECT_PATH.clone(),
+ tools: vec!["ember".to_string()],
+ },
+ ];
+
+ assert_eq!(display_tool("ember", &packages), expected);
+ }
+ }
+
+ mod all {
+ use super::*;
+ use crate::command::list::{PackageDetails, PackageManagerKind, Source};
+
+ #[test]
+ fn empty() {
+ let runtimes = [];
+ let package_managers = [];
+ let packages = [];
+
+ assert_eq!(
+ display_all(&runtimes, &package_managers, &packages),
+ NO_RUNTIME
+ );
+ }
+
+ #[test]
+ fn runtime_and_npm() {
+ let expected = "⚡️ User toolchain:
+
+ Node runtimes:
+ v12.2.0 (current @ ~/path/to/project.json)
+ v11.9.0
+ v10.15.3 (default)
+
+ Package managers:
+ npm:
+ v6.13.1 (default)
+ v6.12.0 (current @ ~/path/to/project.json)
+ v5.6.0
+
+ Packages:
+";
+
+ let runtimes = [
+ Node {
+ source: Source::Project(PROJECT_PATH.clone()),
+ version: NODE_12.clone(),
+ },
+ Node {
+ source: Source::None,
+ version: NODE_11.clone(),
+ },
+ Node {
+ source: Source::Default,
+ version: NODE_10.clone(),
+ },
+ ];
+
+ let package_managers = [
+ PackageManager {
+ kind: PackageManagerKind::Npm,
+ source: Source::Default,
+ version: NPM_VERSION.clone(),
+ },
+ PackageManager {
+ kind: PackageManagerKind::Npm,
+ source: Source::Project(PROJECT_PATH.clone()),
+ version: Version::from((6, 12, 0)),
+ },
+ PackageManager {
+ kind: PackageManagerKind::Npm,
+ source: Source::None,
+ version: Version::from((5, 6, 0)),
+ },
+ ];
+
+ let packages = vec![];
+ assert_eq!(
+ display_all(&runtimes, &package_managers, &packages),
+ expected
+ );
+ }
+
+ #[test]
+ fn runtime_and_yarn() {
+ let expected = "⚡️ User toolchain:
+
+ Node runtimes:
+ v12.2.0 (current @ ~/path/to/project.json)
+ v11.9.0
+ v10.15.3 (default)
+
+ Package managers:
+ Yarn:
+ v1.16.0 (default)
+ v1.17.0 (current @ ~/path/to/project.json)
+ v1.4.0
+
+ Packages:
+";
+
+ let runtimes = [
+ Node {
+ source: Source::Project(PROJECT_PATH.clone()),
+ version: NODE_12.clone(),
+ },
+ Node {
+ source: Source::None,
+ version: NODE_11.clone(),
+ },
+ Node {
+ source: Source::Default,
+ version: NODE_10.clone(),
+ },
+ ];
+
+ let package_managers = [
+ PackageManager {
+ kind: PackageManagerKind::Yarn,
+ source: Source::Default,
+ version: YARN_VERSION.clone(),
+ },
+ PackageManager {
+ kind: PackageManagerKind::Yarn,
+ source: Source::Project(PROJECT_PATH.clone()),
+ version: Version::from((1, 17, 0)),
+ },
+ PackageManager {
+ kind: PackageManagerKind::Yarn,
+ source: Source::None,
+ version: Version::from((1, 4, 0)),
+ },
+ ];
+
+ let packages = vec![];
+ assert_eq!(
+ display_all(&runtimes, &package_managers, &packages),
+ expected
+ );
+ }
+
+ #[test]
+ fn full() {
+ let expected = "⚡️ User toolchain:
+
+ Node runtimes:
+ v12.2.0 (current @ ~/path/to/project.json)
+ v11.9.0
+ v10.15.3 (default)
+
+ Package managers:
+ npm:
+ v6.13.1 (default)
+ v6.12.0 (current @ ~/path/to/project.json)
+ v5.6.0
+ Yarn:
+ v1.16.0 (default)
+ v1.17.0 (current @ ~/path/to/project.json)
+ v1.4.0
+
+ Packages:
+ typescript@3.4.3 (default)
+ binary tools: tsc, tsserver
+ platform:
+ runtime: node@12.2.0
+ package manager: npm@built-in
+ typescript (current @ ~/path/to/project.json)
+ binary tools: tsc, tsserver
+ ember-cli (current @ ~/path/to/project.json)
+ binary tools: ember
+ ember-cli@3.8.2 (default)
+ binary tools: ember
+ platform:
+ runtime: node@12.2.0
+ package manager: npm@built-in";
+
+ let runtimes = [
+ Node {
+ source: Source::Project(PROJECT_PATH.clone()),
+ version: NODE_12.clone(),
+ },
+ Node {
+ source: Source::None,
+ version: NODE_11.clone(),
+ },
+ Node {
+ source: Source::Default,
+ version: NODE_10.clone(),
+ },
+ ];
+
+ let package_managers = [
+ PackageManager {
+ kind: PackageManagerKind::Npm,
+ source: Source::Default,
+ version: NPM_VERSION.clone(),
+ },
+ PackageManager {
+ kind: PackageManagerKind::Npm,
+ source: Source::Project(PROJECT_PATH.clone()),
+ version: Version::from((6, 12, 0)),
+ },
+ PackageManager {
+ kind: PackageManagerKind::Npm,
+ source: Source::None,
+ version: Version::from((5, 6, 0)),
+ },
+ PackageManager {
+ kind: PackageManagerKind::Yarn,
+ source: Source::Default,
+ version: YARN_VERSION.clone(),
+ },
+ PackageManager {
+ kind: PackageManagerKind::Yarn,
+ source: Source::Project(PROJECT_PATH.clone()),
+ version: Version::from((1, 17, 0)),
+ },
+ PackageManager {
+ kind: PackageManagerKind::Yarn,
+ source: Source::None,
+ version: Version::from((1, 4, 0)),
+ },
+ ];
+
+ let packages = [
+ Package::Default {
+ details: PackageDetails {
+ name: "typescript".to_string(),
+ version: Version::from((3, 4, 3)),
+ },
+ node: NODE_12.clone(),
+ tools: vec!["tsc".to_string(), "tsserver".to_string()],
+ },
+ Package::Project {
+ name: "typescript".to_string(),
+ path: PROJECT_PATH.clone(),
+ tools: vec!["tsc".to_string(), "tsserver".to_string()],
+ },
+ Package::Project {
+ name: "ember-cli".to_string(),
+ path: PROJECT_PATH.clone(),
+ tools: vec!["ember".to_string()],
+ },
+ Package::Default {
+ details: PackageDetails {
+ name: "ember-cli".to_string(),
+ version: Version::from((3, 8, 2)),
+ },
+ node: NODE_12.clone(),
+ tools: vec!["ember".to_string()],
+ },
+ ];
+ assert_eq!(
+ display_all(&runtimes, &package_managers, &packages),
+ expected
+ );
+ }
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +
mod human;
+mod plain;
+mod toolchain;
+
+use std::io::IsTerminal as _;
+use std::{fmt, path::PathBuf, str::FromStr};
+
+use node_semver::Version;
+
+use crate::command::Command;
+use toolchain::Toolchain;
+use volta_core::error::{ExitCode, Fallible};
+use volta_core::inventory::package_configs;
+use volta_core::project::Project;
+use volta_core::session::{ActivityKind, Session};
+use volta_core::tool::PackageConfig;
+
+#[derive(clap::ValueEnum, Copy, Clone)]
+enum Format {
+ Human,
+ Plain,
+}
+
+/// The source of a given item, from the perspective of a user.
+///
+/// Note: this is distinct from `volta_core::platform::sourced::Source`, which
+/// represents the source only of a `Platform`, which is a composite structure.
+/// By contrast, this `Source` is concerned *only* with a single item.
+#[derive(Clone, PartialEq, Debug)]
+enum Source {
+ /// The item is from a project. The wrapped `PathBuf` is the path to the
+ /// project's `package.json`.
+ Project(PathBuf),
+
+ /// The item is the user's default.
+ Default,
+
+ /// The item is one that has been *fetched* but is not *installed* anywhere.
+ None,
+}
+
+impl Source {
+ fn allowed_with(&self, filter: &Filter) -> bool {
+ match filter {
+ Filter::Default => self == &Source::Default,
+ Filter::Current => matches!(self, Source::Default | Source::Project(_)),
+ Filter::None => true,
+ }
+ }
+}
+
+impl fmt::Display for Source {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(
+ f,
+ "{}",
+ match self {
+ Source::Project(path) => format!(" (current @ {})", path.display()),
+ Source::Default => String::from(" (default)"),
+ Source::None => String::from(""),
+ }
+ )
+ }
+}
+
+/// A package and its associated tools, for displaying to the user as part of
+/// their toolchain.
+struct PackageDetails {
+ /// The name of the package.
+ pub name: String,
+ /// The package's own version.
+ pub version: Version,
+}
+
+enum Package {
+ Default {
+ details: PackageDetails,
+ /// The version of Node the package is installed against.
+ node: Version,
+ /// The names of the tools associated with the package.
+ tools: Vec<String>,
+ },
+ Project {
+ name: String,
+ /// The names of the tools associated with the package.
+ tools: Vec<String>,
+ path: PathBuf,
+ },
+ Fetched(PackageDetails),
+}
+
+impl Package {
+ fn new(config: &PackageConfig, source: &Source) -> Package {
+ let details = PackageDetails {
+ name: config.name.clone(),
+ version: config.version.clone(),
+ };
+
+ match source {
+ Source::Default => Package::Default {
+ details,
+ node: config.platform.node.clone(),
+ tools: config.bins.clone(),
+ },
+ Source::Project(path) => Package::Project {
+ name: details.name,
+ tools: config.bins.clone(),
+ path: path.clone(),
+ },
+ Source::None => Package::Fetched(details),
+ }
+ }
+
+ fn from_inventory_and_project(project: Option<&Project>) -> Fallible<Vec<Package>> {
+ package_configs().map(|configs| {
+ configs
+ .iter()
+ .map(|config| {
+ let source = Self::source(&config.name, project);
+ Package::new(config, &source)
+ })
+ .collect()
+ })
+ }
+
+ fn source(name: &str, project: Option<&Project>) -> Source {
+ match project {
+ Some(project) if project.has_direct_dependency(name) => {
+ Source::Project(project.manifest_file().to_owned())
+ }
+ _ => Source::Default,
+ }
+ }
+}
+
+#[derive(Clone)]
+struct Node {
+ pub source: Source,
+ pub version: Version,
+}
+
+#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
+enum PackageManagerKind {
+ Npm,
+ Pnpm,
+ Yarn,
+}
+
+impl fmt::Display for PackageManagerKind {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(
+ f,
+ "{}",
+ match self {
+ PackageManagerKind::Npm => "npm",
+ PackageManagerKind::Pnpm => "pnpm",
+ PackageManagerKind::Yarn => "yarn",
+ }
+ )
+ }
+}
+
+#[derive(Clone)]
+struct PackageManager {
+ kind: PackageManagerKind,
+ source: Source,
+ version: Version,
+}
+
+/// How (if at all) should the list query be narrowed?
+enum Filter {
+ /// Display only the currently active tool(s).
+ ///
+ /// For example, if the user queries `volta list --current yarn`, show only
+ /// the version of Yarn currently in use: project, default, or none.
+ Current,
+
+ /// Show only the user's default tool(s).
+ ///
+ /// For example, if the user queries `volta list --default node`, show only
+ /// the user's default Node version.
+ Default,
+
+ /// Do not filter at all. Show all tool(s) matching the query.
+ None,
+}
+
+#[derive(clap::Args)]
+pub(crate) struct List {
+ /// The tool to lookup - `all`, `node`, `npm`, `yarn`, `pnpm`, or the name
+ /// of a package or binary.
+ #[arg(value_name = "tool")]
+ subcommand: Option<Subcommand>,
+
+ /// Specify the output format.
+ ///
+ /// Defaults to `human` for TTYs, `plain` otherwise.
+ #[arg(long)]
+ format: Option<Format>,
+
+ /// Show the currently-active tool(s).
+ ///
+ /// Equivalent to `volta list` when not specifying a specific tool.
+ #[arg(short, long, conflicts_with = "default")]
+ current: bool,
+
+ /// Show your default tool(s).
+ #[arg(short, long, conflicts_with = "current")]
+ default: bool,
+}
+
+/// Which tool should we look up?
+#[derive(Clone)]
+enum Subcommand {
+ /// Show every item in the toolchain.
+ All,
+
+ /// Show locally cached Node versions.
+ Node,
+
+ /// Show locally cached npm versions.
+ Npm,
+
+ /// Show locally cached pnpm versions.
+ Pnpm,
+
+ /// Show locally cached Yarn versions.
+ Yarn,
+
+ /// Show locally cached versions of a package or a package binary.
+ PackageOrTool { name: String },
+}
+
+impl FromStr for Subcommand {
+ type Err = std::convert::Infallible;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ Ok(match s {
+ "all" => Subcommand::All,
+ "node" => Subcommand::Node,
+ "npm" => Subcommand::Npm,
+ "pnpm" => Subcommand::Pnpm,
+ "yarn" => Subcommand::Yarn,
+ s => Subcommand::PackageOrTool { name: s.into() },
+ })
+ }
+}
+
+impl List {
+ fn output_format(&self) -> Format {
+ // We start by checking if the user has explicitly set a value: if they
+ // have, that trumps our TTY-checking. Then, if the user has *not*
+ // specified an option, we use `Human` mode for TTYs and `Plain` for
+ // non-TTY contexts.
+ self.format.unwrap_or(if std::io::stdout().is_terminal() {
+ Format::Human
+ } else {
+ Format::Plain
+ })
+ }
+}
+
+impl Command for List {
+ fn run(self, session: &mut Session) -> Fallible<ExitCode> {
+ session.add_event_start(ActivityKind::List);
+
+ let project = session.project()?;
+ let default_platform = session.default_platform()?;
+ let format = match self.output_format() {
+ Format::Human => human::format,
+ Format::Plain => plain::format,
+ };
+
+ let filter = match (self.current, self.default) {
+ (true, false) => Filter::Current,
+ (false, true) => Filter::Default,
+ (true, true) => unreachable!("simultaneous `current` and `default` forbidden by clap"),
+ _ => Filter::None,
+ };
+
+ let toolchain = match self.subcommand {
+ // For no subcommand, show the user's current toolchain
+ None => Toolchain::active(project, default_platform)?,
+ Some(Subcommand::All) => Toolchain::all(project, default_platform)?,
+ Some(Subcommand::Node) => Toolchain::node(project, default_platform, &filter)?,
+ Some(Subcommand::Npm) => Toolchain::npm(project, default_platform, &filter)?,
+ Some(Subcommand::Pnpm) => Toolchain::pnpm(project, default_platform, &filter)?,
+ Some(Subcommand::Yarn) => Toolchain::yarn(project, default_platform, &filter)?,
+ Some(Subcommand::PackageOrTool { name }) => {
+ Toolchain::package_or_tool(&name, project, &filter)?
+ }
+ };
+
+ if let Some(string) = format(&toolchain) {
+ println!("{}", string)
+ };
+
+ session.add_event_end(ActivityKind::List, ExitCode::Success);
+ Ok(ExitCode::Success)
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402 +403 +404 +405 +406 +407 +408 +409 +410 +411 +412 +413 +414 +415 +416 +417 +418 +419 +420 +421 +422 +423 +424 +425 +426 +427 +428 +429 +430 +431 +432 +433 +434 +435 +436 +437 +438 +439 +440 +441 +442 +443 +444 +445 +446 +447 +448 +449 +450 +451 +452 +453 +454 +455 +456 +457 +458 +459 +460 +461 +462 +463 +464 +465 +466 +467 +468 +469 +470 +471 +472 +473 +474 +475 +476 +477 +478 +479 +480 +481 +482 +483 +484 +485 +486 +487 +488 +489 +490 +491 +492 +493 +494 +495 +496 +497 +498 +499 +500 +501 +502 +503 +504 +505 +506 +507 +508 +509 +510 +511 +512 +513 +514 +515 +516 +517 +518 +519 +520 +521 +522 +523 +524 +525 +526 +527 +528 +529 +530 +531 +532 +533 +534 +535 +536 +537 +538 +539 +540 +541 +542 +543 +544 +545 +546 +547 +548 +549 +550 +551 +552 +553 +
//! Define the "plain" format style for list commands.
+
+use node_semver::Version;
+
+use volta_core::style::tool_version;
+
+use super::{Node, Package, PackageManager, Source, Toolchain};
+
+pub(super) fn format(toolchain: &Toolchain) -> Option<String> {
+ let (runtimes, package_managers, packages) = match toolchain {
+ Toolchain::Node(runtimes) => (describe_runtimes(runtimes), None, None),
+ Toolchain::PackageManagers { managers, .. } => {
+ (None, describe_package_managers(managers), None)
+ }
+ Toolchain::Packages(packages) => (None, None, describe_packages(packages)),
+ Toolchain::Tool {
+ name,
+ host_packages,
+ } => (None, None, Some(describe_tool_set(name, host_packages))),
+ Toolchain::Active {
+ runtime,
+ package_managers,
+ packages,
+ } => (
+ runtime
+ .as_ref()
+ .and_then(|r| describe_runtimes(&[(**r).clone()])),
+ describe_package_managers(package_managers),
+ describe_packages(packages),
+ ),
+ Toolchain::All {
+ runtimes,
+ package_managers,
+ packages,
+ } => (
+ describe_runtimes(runtimes),
+ describe_package_managers(package_managers),
+ describe_packages(packages),
+ ),
+ };
+
+ match (runtimes, package_managers, packages) {
+ (Some(runtimes), Some(package_managers), Some(packages)) => {
+ Some(format!("{}\n{}\n{}", runtimes, package_managers, packages))
+ }
+ (Some(runtimes), Some(package_managers), None) => {
+ Some(format!("{}\n{}", runtimes, package_managers))
+ }
+ (Some(runtimes), None, Some(packages)) => Some(format!("{}\n{}", runtimes, packages)),
+ (Some(runtimes), None, None) => Some(runtimes),
+ (None, Some(package_managers), Some(packages)) => {
+ Some(format!("{}\n{}", package_managers, packages))
+ }
+ (None, Some(package_managers), None) => Some(package_managers),
+ (None, None, Some(packages)) => Some(packages),
+ (None, None, None) => None,
+ }
+}
+
+fn describe_runtimes(runtimes: &[Node]) -> Option<String> {
+ if runtimes.is_empty() {
+ None
+ } else {
+ Some(
+ runtimes
+ .iter()
+ .map(|runtime| display_node(&runtime.source, &runtime.version))
+ .collect::<Vec<String>>()
+ .join("\n"),
+ )
+ }
+}
+
+fn describe_package_managers(package_managers: &[PackageManager]) -> Option<String> {
+ if package_managers.is_empty() {
+ None
+ } else {
+ Some(
+ package_managers
+ .iter()
+ .map(display_package_manager)
+ .collect::<Vec<String>>()
+ .join("\n"),
+ )
+ }
+}
+
+fn describe_packages(packages: &[Package]) -> Option<String> {
+ if packages.is_empty() {
+ None
+ } else {
+ Some(
+ packages
+ .iter()
+ .map(display_package)
+ .collect::<Vec<String>>()
+ .join("\n"),
+ )
+ }
+}
+
+fn describe_tool_set(name: &str, hosts: &[Package]) -> String {
+ hosts
+ .iter()
+ .filter_map(|package| display_tool(name, package))
+ .collect::<Vec<String>>()
+ .join("\n")
+}
+
+fn display_node(source: &Source, version: &Version) -> String {
+ format!("runtime {}{}", tool_version("node", version), source)
+}
+
+fn display_package_manager(package_manager: &PackageManager) -> String {
+ format!(
+ "package-manager {}{}",
+ tool_version(package_manager.kind, &package_manager.version),
+ package_manager.source
+ )
+}
+
+fn package_source(package: &Package) -> String {
+ match package {
+ Package::Default { .. } => String::from(" (default)"),
+ Package::Project { path, .. } => format!(" (current @ {})", path.display()),
+ Package::Fetched(..) => String::new(),
+ }
+}
+
+fn display_package(package: &Package) -> String {
+ match package {
+ Package::Default {
+ details,
+ node,
+ tools,
+ ..
+ } => {
+ let tools = match tools.len() {
+ 0 => String::from(" "),
+ _ => format!(" {} ", tools.join(", ")),
+ };
+
+ format!(
+ "package {} /{}/ {} {}{}",
+ tool_version(&details.name, &details.version),
+ tools,
+ tool_version("node", node),
+ // Should be updated when we support installing with custom package_managers,
+ // whether Yarn or non-built-in versions of npm
+ "npm@built-in",
+ package_source(package)
+ )
+ }
+ Package::Project { name, tools, .. } => {
+ let tools = match tools.len() {
+ 0 => String::from(" "),
+ _ => format!(" {} ", tools.join(", ")),
+ };
+
+ format!(
+ "package {} /{}/ {} {}{}",
+ tool_version(name, "project"),
+ tools,
+ "node@project",
+ "npm@project",
+ package_source(package)
+ )
+ }
+ Package::Fetched(details) => format!(
+ "package {} (fetched)",
+ tool_version(&details.name, &details.version)
+ ),
+ }
+}
+
+fn display_tool(name: &str, host: &Package) -> Option<String> {
+ match host {
+ Package::Default { details, node, .. } => Some(format!(
+ "tool {} / {} / {} {}{}",
+ name,
+ tool_version(&details.name, &details.version),
+ tool_version("node", node),
+ "npm@built-in",
+ package_source(host)
+ )),
+ Package::Project {
+ name: host_name, ..
+ } => Some(format!(
+ "tool {} / {} / {} {}{}",
+ name,
+ tool_version(host_name, "project"),
+ "node@project",
+ "npm@project",
+ package_source(host)
+ )),
+ Package::Fetched(..) => None,
+ }
+}
+
+// These tests are organized by way of the *item* being printed, unlike in the
+// `human` module, because the formatting is consistent across command formats.
+#[cfg(test)]
+mod tests {
+ use std::path::PathBuf;
+
+ use node_semver::Version;
+ use once_cell::sync::Lazy;
+
+ use crate::command::list::PackageDetails;
+
+ static NODE_VERSION: Lazy<Version> = Lazy::new(|| Version::from((12, 4, 0)));
+ static TYPESCRIPT_VERSION: Lazy<Version> = Lazy::new(|| Version::from((3, 4, 1)));
+ static NPM_VERSION: Lazy<Version> = Lazy::new(|| Version::from((6, 13, 4)));
+ static YARN_VERSION: Lazy<Version> = Lazy::new(|| Version::from((1, 16, 0)));
+ static PROJECT_PATH: Lazy<PathBuf> = Lazy::new(|| PathBuf::from("/a/b/c"));
+
+ mod node {
+ use super::super::*;
+ use super::*;
+
+ #[test]
+ fn default() {
+ let source = Source::Default;
+ assert_eq!(
+ display_node(&source, &NODE_VERSION).as_str(),
+ "runtime node@12.4.0 (default)"
+ );
+ }
+
+ #[test]
+ fn project() {
+ let source = Source::Project(PROJECT_PATH.clone());
+ assert_eq!(
+ display_node(&source, &NODE_VERSION).as_str(),
+ "runtime node@12.4.0 (current @ /a/b/c)"
+ );
+ }
+
+ #[test]
+ fn installed_not_set() {
+ let source = Source::None;
+ assert_eq!(
+ display_node(&source, &NODE_VERSION).as_str(),
+ "runtime node@12.4.0"
+ );
+ }
+ }
+
+ mod npm {
+ use super::super::*;
+ use super::*;
+ use crate::command::list::*;
+
+ #[test]
+ fn default() {
+ assert_eq!(
+ display_package_manager(&PackageManager {
+ kind: PackageManagerKind::Npm,
+ source: Source::Default,
+ version: NPM_VERSION.clone(),
+ })
+ .as_str(),
+ "package-manager npm@6.13.4 (default)"
+ );
+ }
+
+ #[test]
+ fn project() {
+ assert_eq!(
+ display_package_manager(&PackageManager {
+ kind: PackageManagerKind::Npm,
+ source: Source::Project(PROJECT_PATH.clone()),
+ version: NPM_VERSION.clone(),
+ })
+ .as_str(),
+ "package-manager npm@6.13.4 (current @ /a/b/c)"
+ );
+ }
+
+ #[test]
+ fn installed_not_set() {
+ assert_eq!(
+ display_package_manager(&PackageManager {
+ kind: PackageManagerKind::Npm,
+ source: Source::None,
+ version: NPM_VERSION.clone(),
+ })
+ .as_str(),
+ "package-manager npm@6.13.4"
+ );
+ }
+ }
+
+ mod yarn {
+ use super::super::*;
+ use super::*;
+ use crate::command::list::*;
+
+ #[test]
+ fn default() {
+ assert_eq!(
+ display_package_manager(&PackageManager {
+ kind: PackageManagerKind::Yarn,
+ source: Source::Default,
+ version: YARN_VERSION.clone(),
+ })
+ .as_str(),
+ "package-manager yarn@1.16.0 (default)"
+ );
+ }
+
+ #[test]
+ fn project() {
+ assert_eq!(
+ display_package_manager(&PackageManager {
+ kind: PackageManagerKind::Yarn,
+ source: Source::Project(PROJECT_PATH.clone()),
+ version: YARN_VERSION.clone()
+ })
+ .as_str(),
+ "package-manager yarn@1.16.0 (current @ /a/b/c)"
+ );
+ }
+
+ #[test]
+ fn installed_not_set() {
+ assert_eq!(
+ display_package_manager(&PackageManager {
+ kind: PackageManagerKind::Yarn,
+ source: Source::None,
+ version: YARN_VERSION.clone()
+ })
+ .as_str(),
+ "package-manager yarn@1.16.0"
+ );
+ }
+ }
+
+ mod package {
+ use super::super::*;
+ use super::*;
+
+ #[test]
+ fn single_default() {
+ assert_eq!(
+ describe_packages(&[Package::Default {
+ details: PackageDetails {
+ name: "typescript".into(),
+ version: TYPESCRIPT_VERSION.clone(),
+ },
+ node: NODE_VERSION.clone(),
+ tools: vec!["tsc".into(), "tsserver".into()]
+ }])
+ .expect("Should always return a `String` if given a non-empty set")
+ .as_str(),
+ "package typescript@3.4.1 / tsc, tsserver / node@12.4.0 npm@built-in (default)"
+ );
+ }
+
+ #[test]
+ fn single_project() {
+ assert_eq!(
+ describe_packages(&[Package::Project {
+ name: "typescript".into(),
+ path: PROJECT_PATH.clone(),
+ tools: vec!["tsc".into(), "tsserver".into()]
+ }])
+ .expect("Should always return a `String` if given a non-empty set")
+ .as_str(),
+ "package typescript@project / tsc, tsserver / node@project npm@project (current @ /a/b/c)"
+ );
+ }
+
+ #[test]
+ fn mixed() {
+ assert_eq!(
+ describe_packages(&[
+ Package::Project {
+ name: "typescript".into(),
+ path: PROJECT_PATH.clone(),
+ tools: vec!["tsc".into(), "tsserver".into()]
+ },
+ Package::Default {
+ details: PackageDetails {
+ name: "ember-cli".into(),
+ version: Version::from((3, 10, 0)),
+ },
+ node: NODE_VERSION.clone(),
+ tools: vec!["ember".into()],
+ },
+ Package::Fetched(PackageDetails {
+ name: "create-react-app".into(),
+ version: Version::from((1, 0, 0)),
+ })
+ ])
+ .expect("Should always return a `String` if given a non-empty set")
+ .as_str(),
+ "package typescript@project / tsc, tsserver / node@project npm@project (current @ /a/b/c)\n\
+ package ember-cli@3.10.0 / ember / node@12.4.0 npm@built-in (default)\n\
+ package create-react-app@1.0.0 (fetched)"
+ );
+ }
+
+ #[test]
+ fn installed_not_set() {
+ assert_eq!(
+ describe_packages(&[Package::Fetched(PackageDetails {
+ name: "typescript".into(),
+ version: TYPESCRIPT_VERSION.clone(),
+ })])
+ .expect("Should always return a `String` if given a non-empty set")
+ .as_str(),
+ "package typescript@3.4.1 (fetched)"
+ );
+ }
+ }
+
+ mod tool {
+ use super::super::*;
+ use super::*;
+
+ #[test]
+ fn default() {
+ assert_eq!(
+ display_tool(
+ "tsc",
+ &Package::Default {
+ details: PackageDetails {
+ name: "typescript".into(),
+ version: TYPESCRIPT_VERSION.clone(),
+ },
+ node: NODE_VERSION.clone(),
+ tools: vec!["tsc".into(), "tsserver".into()],
+ }
+ )
+ .expect("should always return `Some` for `Default`")
+ .as_str(),
+ "tool tsc / typescript@3.4.1 / node@12.4.0 npm@built-in (default)"
+ );
+ }
+
+ #[test]
+ fn project() {
+ assert_eq!(
+ display_tool(
+ "tsc",
+ &Package::Project {
+ name: "typescript".into(),
+ path: PROJECT_PATH.clone(),
+ tools: vec!["tsc".into(), "tsserver".into()],
+ }
+ )
+ .expect("should always return `Some` for `Project`")
+ .as_str(),
+ "tool tsc / typescript@project / node@project npm@project (current @ /a/b/c)"
+ );
+ }
+
+ #[test]
+ fn fetched() {
+ assert_eq!(
+ display_tool(
+ "tsc",
+ &Package::Fetched(PackageDetails {
+ name: "typescript".into(),
+ version: TYPESCRIPT_VERSION.clone()
+ })
+ ),
+ None
+ );
+ }
+ }
+
+ mod toolchain {
+ use super::super::*;
+ use super::*;
+ use crate::command::list::{Node, PackageManager, PackageManagerKind, Toolchain};
+
+ #[test]
+ fn full() {
+ assert_eq!(
+ format(&Toolchain::All {
+ runtimes: vec![
+ Node {
+ source: Source::Default,
+ version: NODE_VERSION.clone()
+ },
+ Node {
+ source: Source::None,
+ version: Version::from((8, 2, 4))
+ }
+ ],
+ package_managers: vec![
+ PackageManager {
+ kind: PackageManagerKind::Npm,
+ source: Source::Project(PROJECT_PATH.clone()),
+ version: NPM_VERSION.clone(),
+ },
+ PackageManager {
+ kind: PackageManagerKind::Npm,
+ source: Source::Default,
+ version: Version::from((5, 10, 0))
+ },
+ PackageManager {
+ kind: PackageManagerKind::Yarn,
+ source: Source::Project(PROJECT_PATH.clone()),
+ version: YARN_VERSION.clone()
+ },
+ PackageManager {
+ kind: PackageManagerKind::Yarn,
+ source: Source::Default,
+ version: Version::from((1, 17, 0))
+ }
+ ],
+ packages: vec![
+ Package::Default {
+ details: PackageDetails {
+ name: "ember-cli".into(),
+ version: Version::from((3, 10, 2)),
+ },
+ node: NODE_VERSION.clone(),
+ tools: vec!["ember".into()]
+ },
+ Package::Project {
+ name: "ember-cli".into(),
+ path: PROJECT_PATH.clone(),
+ tools: vec!["ember".into()]
+ },
+ Package::Default {
+ details: PackageDetails {
+ name: "typescript".into(),
+ version: TYPESCRIPT_VERSION.clone(),
+ },
+ node: NODE_VERSION.clone(),
+ tools: vec!["tsc".into(), "tsserver".into()]
+ }
+ ]
+ })
+ .expect("`format` with a non-empty toolchain returns `Some`")
+ .as_str(),
+ "runtime node@12.4.0 (default)\n\
+ runtime node@8.2.4\n\
+ package-manager npm@6.13.4 (current @ /a/b/c)\n\
+ package-manager npm@5.10.0 (default)\n\
+ package-manager yarn@1.16.0 (current @ /a/b/c)\n\
+ package-manager yarn@1.17.0 (default)\n\
+ package ember-cli@3.10.2 / ember / node@12.4.0 npm@built-in (default)\n\
+ package ember-cli@project / ember / node@project npm@project (current @ /a/b/c)\n\
+ package typescript@3.4.1 / tsc, tsserver / node@12.4.0 npm@built-in (default)"
+ )
+ }
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402 +403 +404 +405 +406 +407 +408 +409 +
use super::{Filter, Node, Package, PackageManager, Source};
+use crate::command::list::PackageManagerKind;
+use node_semver::Version;
+use volta_core::error::Fallible;
+use volta_core::inventory::{
+ node_versions, npm_versions, package_configs, pnpm_versions, yarn_versions,
+};
+use volta_core::platform::PlatformSpec;
+use volta_core::project::Project;
+use volta_core::tool::PackageConfig;
+
+pub(super) enum Toolchain {
+ Node(Vec<Node>),
+ PackageManagers {
+ kind: PackageManagerKind,
+ managers: Vec<PackageManager>,
+ },
+ Packages(Vec<Package>),
+ Tool {
+ name: String,
+ host_packages: Vec<Package>,
+ },
+ Active {
+ runtime: Option<Box<Node>>,
+ package_managers: Vec<PackageManager>,
+ packages: Vec<Package>,
+ },
+ All {
+ runtimes: Vec<Node>,
+ package_managers: Vec<PackageManager>,
+ packages: Vec<Package>,
+ },
+}
+
+/// Lightweight rule for which item to get the `Source` for.
+enum Lookup {
+ /// Look up the Node runtime
+ Runtime,
+ /// Look up the npm package manager
+ Npm,
+ /// Look up the pnpm package manager
+ Pnpm,
+ /// Look up the Yarn package manager
+ Yarn,
+}
+
+impl Lookup {
+ fn version_from_spec(&self) -> impl Fn(&PlatformSpec) -> Option<Version> + '_ {
+ move |spec| match self {
+ Lookup::Runtime => Some(spec.node.clone()),
+ Lookup::Npm => spec.npm.clone(),
+ Lookup::Pnpm => spec.pnpm.clone(),
+ Lookup::Yarn => spec.yarn.clone(),
+ }
+ }
+
+ fn version_source(
+ self,
+ project: Option<&Project>,
+ default_platform: Option<&PlatformSpec>,
+ version: &Version,
+ ) -> Source {
+ project
+ .and_then(|proj| {
+ proj.platform()
+ .and_then(self.version_from_spec())
+ .and_then(|project_version| {
+ if &project_version == version {
+ Some(Source::Project(proj.manifest_file().to_owned()))
+ } else {
+ None
+ }
+ })
+ })
+ .or_else(|| {
+ default_platform
+ .and_then(self.version_from_spec())
+ .and_then(|default_version| {
+ if &default_version == version {
+ Some(Source::Default)
+ } else {
+ None
+ }
+ })
+ })
+ .unwrap_or(Source::None)
+ }
+
+ /// Determine the `Source` for a given kind of tool (`Lookup`).
+ fn active_tool(
+ self,
+ project: Option<&Project>,
+ default: Option<&PlatformSpec>,
+ ) -> Option<(Source, Version)> {
+ project
+ .and_then(|proj| {
+ proj.platform()
+ .and_then(self.version_from_spec())
+ .map(|version| (Source::Project(proj.manifest_file().to_owned()), version))
+ })
+ .or_else(|| {
+ default
+ .and_then(self.version_from_spec())
+ .map(|version| (Source::Default, version))
+ })
+ }
+}
+
+/// Look up the `Source` for a tool with a given name.
+fn tool_source(name: &str, project: Option<&Project>) -> Fallible<Source> {
+ match project {
+ Some(project) => {
+ if project.has_direct_bin(name.as_ref())? {
+ Ok(Source::Project(project.manifest_file().to_owned()))
+ } else {
+ Ok(Source::Default)
+ }
+ }
+ _ => Ok(Source::Default),
+ }
+}
+
+impl Toolchain {
+ pub(super) fn active(
+ project: Option<&Project>,
+ default_platform: Option<&PlatformSpec>,
+ ) -> Fallible<Toolchain> {
+ let runtime = Lookup::Runtime
+ .active_tool(project, default_platform)
+ .map(|(source, version)| Box::new(Node { source, version }));
+
+ let package_managers =
+ Lookup::Npm
+ .active_tool(project, default_platform)
+ .map(|(source, version)| PackageManager {
+ kind: PackageManagerKind::Npm,
+ source,
+ version,
+ })
+ .into_iter()
+ .chain(Lookup::Pnpm.active_tool(project, default_platform).map(
+ |(source, version)| PackageManager {
+ kind: PackageManagerKind::Pnpm,
+ source,
+ version,
+ },
+ ))
+ .chain(Lookup::Yarn.active_tool(project, default_platform).map(
+ |(source, version)| PackageManager {
+ kind: PackageManagerKind::Yarn,
+ source,
+ version,
+ },
+ ))
+ .collect();
+
+ let packages = Package::from_inventory_and_project(project)?;
+
+ Ok(Toolchain::Active {
+ runtime,
+ package_managers,
+ packages,
+ })
+ }
+
+ pub(super) fn all(
+ project: Option<&Project>,
+ default_platform: Option<&PlatformSpec>,
+ ) -> Fallible<Toolchain> {
+ let runtimes = node_versions()?
+ .iter()
+ .map(|version| Node {
+ source: Lookup::Runtime.version_source(project, default_platform, version),
+ version: version.clone(),
+ })
+ .collect();
+
+ let package_managers = npm_versions()?
+ .iter()
+ .map(|version| PackageManager {
+ kind: PackageManagerKind::Npm,
+ source: Lookup::Npm.version_source(project, default_platform, version),
+ version: version.clone(),
+ })
+ .chain(pnpm_versions()?.iter().map(|version| PackageManager {
+ kind: PackageManagerKind::Pnpm,
+ source: Lookup::Pnpm.version_source(project, default_platform, version),
+ version: version.clone(),
+ }))
+ .chain(yarn_versions()?.iter().map(|version| PackageManager {
+ kind: PackageManagerKind::Yarn,
+ source: Lookup::Yarn.version_source(project, default_platform, version),
+ version: version.clone(),
+ }))
+ .collect();
+
+ let packages = Package::from_inventory_and_project(project)?;
+
+ Ok(Toolchain::All {
+ runtimes,
+ package_managers,
+ packages,
+ })
+ }
+
+ pub(super) fn node(
+ project: Option<&Project>,
+ default_platform: Option<&PlatformSpec>,
+ filter: &Filter,
+ ) -> Fallible<Toolchain> {
+ let runtimes = node_versions()?
+ .iter()
+ .filter_map(|version| {
+ let source = Lookup::Runtime.version_source(project, default_platform, version);
+ if source.allowed_with(filter) {
+ let version = version.clone();
+ Some(Node { source, version })
+ } else {
+ None
+ }
+ })
+ .collect();
+
+ Ok(Toolchain::Node(runtimes))
+ }
+
+ pub(super) fn npm(
+ project: Option<&Project>,
+ default_platform: Option<&PlatformSpec>,
+ filter: &Filter,
+ ) -> Fallible<Toolchain> {
+ let managers = npm_versions()?
+ .iter()
+ .filter_map(|version| {
+ let source = Lookup::Npm.version_source(project, default_platform, version);
+ if source.allowed_with(filter) {
+ Some(PackageManager {
+ kind: PackageManagerKind::Npm,
+ source,
+ version: version.clone(),
+ })
+ } else {
+ None
+ }
+ })
+ .collect();
+
+ Ok(Toolchain::PackageManagers {
+ kind: PackageManagerKind::Npm,
+ managers,
+ })
+ }
+
+ pub(super) fn pnpm(
+ project: Option<&Project>,
+ default_platform: Option<&PlatformSpec>,
+ filter: &Filter,
+ ) -> Fallible<Toolchain> {
+ let managers = pnpm_versions()?
+ .iter()
+ .filter_map(|version| {
+ let source = Lookup::Pnpm.version_source(project, default_platform, version);
+ if source.allowed_with(filter) {
+ Some(PackageManager {
+ kind: PackageManagerKind::Pnpm,
+ source,
+ version: version.clone(),
+ })
+ } else {
+ None
+ }
+ })
+ .collect();
+
+ Ok(Toolchain::PackageManagers {
+ kind: PackageManagerKind::Pnpm,
+ managers,
+ })
+ }
+
+ pub(super) fn yarn(
+ project: Option<&Project>,
+ default_platform: Option<&PlatformSpec>,
+ filter: &Filter,
+ ) -> Fallible<Toolchain> {
+ let managers = yarn_versions()?
+ .iter()
+ .filter_map(|version| {
+ let source = Lookup::Yarn.version_source(project, default_platform, version);
+ if source.allowed_with(filter) {
+ Some(PackageManager {
+ kind: PackageManagerKind::Yarn,
+ source,
+ version: version.clone(),
+ })
+ } else {
+ None
+ }
+ })
+ .collect();
+
+ Ok(Toolchain::PackageManagers {
+ kind: PackageManagerKind::Yarn,
+ managers,
+ })
+ }
+
+ pub(super) fn package_or_tool(
+ name: &str,
+ project: Option<&Project>,
+ filter: &Filter,
+ ) -> Fallible<Toolchain> {
+ /// An internal-only helper for tracking whether we found a given item
+ /// from the `PackageCollection` as a *package* or as a *tool*.
+ #[derive(PartialEq, Debug)]
+ enum Kind {
+ Package,
+ Tool,
+ }
+
+ /// A convenient name for this tuple, since we have to name it in a few
+ /// spots below.
+ type Triple<'p> = (Kind, &'p PackageConfig, Source);
+
+ let configs = package_configs()?;
+ let packages_and_tools = configs
+ .iter()
+ .filter_map(|config| {
+ // Start with the package itself, since tools often match
+ // the package name and we prioritize packages.
+ if config.name == name {
+ let source = Package::source(name, project);
+ if source.allowed_with(filter) {
+ Some(Ok((Kind::Package, config, source)))
+ } else {
+ None
+ }
+
+ // Then check if the passed name matches an installed package's
+ // binaries. If it does, we have a tool.
+ } else if config.bins.iter().any(|bin| bin.as_str() == name) {
+ tool_source(name, project)
+ .map(|source| {
+ if source.allowed_with(filter) {
+ Some((Kind::Tool, config, source))
+ } else {
+ None
+ }
+ })
+ .transpose()
+
+ // Otherwise, we don't have any match all.
+ } else {
+ None
+ }
+ })
+ // Then eagerly collect the first error (if there are any) and
+ // return it; otherwise we have a totally valid collection.
+ .collect::<Fallible<Vec<Triple>>>()?;
+
+ let (has_packages, has_tools) =
+ packages_and_tools
+ .iter()
+ .fold((false, false), |(packages, tools), (kind, ..)| {
+ (
+ packages || kind == &Kind::Package,
+ tools || kind == &Kind::Tool,
+ )
+ });
+
+ let toolchain = match (has_packages, has_tools) {
+ // If there are neither packages nor tools, treat it as `Packages`,
+ // but don't re-process the data just to construct an empty `Vec`!
+ (false, false) => Toolchain::Packages(vec![]),
+ // If there are any packages, we resolve this *as* `Packages`, even
+ // if there are also matching tools, since we give priority to
+ // listing packages between packages and tools.
+ (true, _) => {
+ let packages = packages_and_tools
+ .into_iter()
+ .filter_map(|(kind, config, source)| match kind {
+ Kind::Package => Some(Package::new(config, &source)),
+ Kind::Tool => None,
+ })
+ .collect();
+
+ Toolchain::Packages(packages)
+ }
+ // If there are no packages matching, but we do have tools matching,
+ // we return `Tool`.
+ (false, true) => {
+ let host_packages = packages_and_tools
+ .into_iter()
+ .filter_map(|(kind, config, source)| match kind {
+ Kind::Tool => Some(Package::new(config, &source)),
+ Kind::Package => None, // should be none of these!
+ })
+ .collect();
+
+ Toolchain::Tool {
+ name: name.into(),
+ host_packages,
+ }
+ }
+ };
+
+ Ok(toolchain)
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +
pub(crate) mod completions;
+pub(crate) mod fetch;
+pub(crate) mod install;
+pub(crate) mod list;
+pub(crate) mod pin;
+pub(crate) mod run;
+pub(crate) mod setup;
+pub(crate) mod uninstall;
+pub(crate) mod r#use;
+pub(crate) mod which;
+
+pub(crate) use self::which::Which;
+pub(crate) use completions::Completions;
+pub(crate) use fetch::Fetch;
+pub(crate) use install::Install;
+pub(crate) use list::List;
+pub(crate) use pin::Pin;
+pub(crate) use r#use::Use;
+pub(crate) use run::Run;
+pub(crate) use setup::Setup;
+pub(crate) use uninstall::Uninstall;
+
+use volta_core::error::{ExitCode, Fallible};
+use volta_core::session::Session;
+
+/// A Volta command.
+pub(crate) trait Command: Sized {
+ /// Executes the command. Returns `Ok(true)` if the process should return 0,
+ /// `Ok(false)` if the process should return 1, and `Err(e)` if the process
+ /// should return `e.exit_code()`.
+ fn run(self, session: &mut Session) -> Fallible<ExitCode>;
+}
+
use volta_core::error::{ExitCode, Fallible};
+use volta_core::session::{ActivityKind, Session};
+use volta_core::tool::Spec;
+
+use crate::command::Command;
+
+#[derive(clap::Args)]
+pub(crate) struct Pin {
+ /// Tools to pin, like `node@lts` or `yarn@^1.14`.
+ #[arg(value_name = "tool[@version]", required = true)]
+ tools: Vec<String>,
+}
+
+impl Command for Pin {
+ fn run(self, session: &mut Session) -> Fallible<ExitCode> {
+ session.add_event_start(ActivityKind::Pin);
+
+ for tool in Spec::from_strings(&self.tools, "pin")? {
+ tool.resolve(session)?.pin(session)?;
+ }
+
+ session.add_event_end(ActivityKind::Pin, ExitCode::Success);
+ Ok(ExitCode::Success)
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +
use std::collections::HashMap;
+use std::ffi::OsString;
+
+use crate::command::Command;
+use crate::common::{Error, IntoResult};
+use log::warn;
+use volta_core::error::{report_error, ExitCode, Fallible};
+use volta_core::platform::{CliPlatform, InheritOption};
+use volta_core::run::execute_tool;
+use volta_core::session::{ActivityKind, Session};
+use volta_core::tool::{node, npm, pnpm, yarn};
+
+#[derive(Debug, clap::Args)]
+pub(crate) struct Run {
+ /// Set the custom Node version
+ #[arg(long, value_name = "version")]
+ node: Option<String>,
+
+ /// Set the custom npm version
+ #[arg(long, value_name = "version", conflicts_with = "bundled_npm")]
+ npm: Option<String>,
+
+ /// Forces npm to be the version bundled with Node
+ #[arg(long, conflicts_with = "npm")]
+ bundled_npm: bool,
+
+ /// Set the custon pnpm version
+ #[arg(long, value_name = "version", conflicts_with = "no_pnpm")]
+ pnpm: Option<String>,
+
+ /// Disables pnpm
+ #[arg(long, conflicts_with = "pnpm")]
+ no_pnpm: bool,
+
+ /// Set the custom Yarn version
+ #[arg(long, value_name = "version", conflicts_with = "no_yarn")]
+ yarn: Option<String>,
+
+ /// Disables Yarn
+ #[arg(long, conflicts_with = "yarn")]
+ no_yarn: bool,
+
+ /// Set an environment variable (can be used multiple times)
+ #[arg(long = "env", value_name = "NAME=value", num_args = 1)]
+ envs: Vec<String>,
+
+ /// The command to run, along with any arguments
+ #[arg(
+ allow_hyphen_values = true,
+ trailing_var_arg = true,
+ value_name = "COMMAND",
+ required = true
+ )]
+ command_and_args: Vec<OsString>,
+}
+
+impl Command for Run {
+ fn run(self, session: &mut Session) -> Fallible<ExitCode> {
+ session.add_event_start(ActivityKind::Run);
+
+ let envs = self.parse_envs();
+ let platform = self.parse_platform(session)?;
+
+ // Safety: At least one value is required for `command_and_args`, so there must be at
+ // least one value in the list. If no value is provided, Clap will show a "required
+ // argument missing" message and this function won't be called.
+ let command = &self.command_and_args[0];
+ let args = &self.command_and_args[1..];
+
+ match execute_tool(command, args, &envs, platform, session).into_result() {
+ Ok(()) => {
+ session.add_event_end(ActivityKind::Run, ExitCode::Success);
+ Ok(ExitCode::Success)
+ }
+ Err(Error::Tool(code)) => {
+ session.add_event_tool_end(ActivityKind::Run, code);
+ Ok(ExitCode::ExecutionFailure)
+ }
+ Err(Error::Volta(err)) => {
+ report_error(env!("CARGO_PKG_VERSION"), &err);
+ session.add_event_error(ActivityKind::Run, &err);
+ session.add_event_end(ActivityKind::Run, err.exit_code());
+ Ok(err.exit_code())
+ }
+ }
+ }
+}
+
+impl Run {
+ /// Builds a CliPlatform from the provided cli options
+ ///
+ /// Will resolve a semver / tag version if necessary
+ fn parse_platform(&self, session: &mut Session) -> Fallible<CliPlatform> {
+ let node = self
+ .node
+ .as_ref()
+ .map(|version| node::resolve(version.parse()?, session))
+ .transpose()?;
+
+ let npm = match (self.bundled_npm, &self.npm) {
+ (true, _) => InheritOption::None,
+ (false, None) => InheritOption::Inherit,
+ (false, Some(version)) => match npm::resolve(version.parse()?, session)? {
+ None => InheritOption::Inherit,
+ Some(npm) => InheritOption::Some(npm),
+ },
+ };
+
+ let pnpm = match (self.no_pnpm, &self.pnpm) {
+ (true, _) => InheritOption::None,
+ (false, None) => InheritOption::Inherit,
+ (false, Some(version)) => {
+ InheritOption::Some(pnpm::resolve(version.parse()?, session)?)
+ }
+ };
+
+ let yarn = match (self.no_yarn, &self.yarn) {
+ (true, _) => InheritOption::None,
+ (false, None) => InheritOption::Inherit,
+ (false, Some(version)) => {
+ InheritOption::Some(yarn::resolve(version.parse()?, session)?)
+ }
+ };
+
+ Ok(CliPlatform {
+ node,
+ npm,
+ pnpm,
+ yarn,
+ })
+ }
+
+ /// Convert the environment variable settings passed to the command line into a map
+ ///
+ /// We ignore any setting that doesn't have a value associated with it
+ /// We also ignore the PATH environment variable as that is set when running a command
+ fn parse_envs(&self) -> HashMap<&str, &str> {
+ self.envs.iter().filter_map(|entry| {
+ let mut key_value = entry.splitn(2, '=');
+
+ match (key_value.next(), key_value.next()) {
+ (None, _) => None,
+ (Some(_), None) => None,
+ (Some(key), _) if key.eq_ignore_ascii_case("PATH") => {
+ warn!("Ignoring {} environment variable as it will be overwritten when executing the command", key);
+ None
+ }
+ (Some(key), Some(value)) => Some((key, value)),
+ }
+ }).collect()
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +
use log::info;
+use volta_core::error::{ExitCode, Fallible};
+use volta_core::layout::volta_home;
+use volta_core::session::{ActivityKind, Session};
+use volta_core::shim::regenerate_shims_for_dir;
+use volta_core::style::success_prefix;
+
+use crate::command::Command;
+
+#[derive(clap::Args)]
+pub(crate) struct Setup {}
+
+impl Command for Setup {
+ fn run(self, session: &mut Session) -> Fallible<ExitCode> {
+ session.add_event_start(ActivityKind::Setup);
+
+ os::setup_environment()?;
+ regenerate_shims_for_dir(volta_home()?.shim_dir())?;
+
+ info!(
+ "{} Setup complete. Open a new terminal to start using Volta!",
+ success_prefix()
+ );
+
+ session.add_event_end(ActivityKind::Setup, ExitCode::Success);
+ Ok(ExitCode::Success)
+ }
+}
+
+#[cfg(unix)]
+mod os {
+ use std::env;
+ use std::fs::File;
+ use std::io::{self, BufRead, BufReader, Write};
+ use std::path::{Path, PathBuf};
+
+ use log::{debug, warn};
+ use volta_core::error::{ErrorKind, Fallible};
+ use volta_core::layout::volta_home;
+
+ pub fn setup_environment() -> Fallible<()> {
+ let home = volta_home()?;
+ let formatted_home = format_home(home.root());
+
+ // Don't update the user's shell config files if VOLTA_HOME and PATH already contain what we need.
+ let home_in_path = match env::var_os("PATH") {
+ Some(paths) => env::split_paths(&paths).find(|p| p == home.shim_dir()),
+ None => None,
+ };
+
+ if env::var_os("VOLTA_HOME").is_some() && home_in_path.is_some() {
+ debug!(
+ "Skipping dot-file modification as VOLTA_HOME is set, and included in the PATH."
+ );
+ return Ok(());
+ }
+
+ debug!("Searching for profiles to update");
+ let profiles = determine_profiles()?;
+
+ let found_profile = profiles.into_iter().fold(false, |prev, profile| {
+ let contents = read_profile_without_volta(&profile).unwrap_or_default();
+
+ let write_profile = match profile.extension() {
+ Some(ext) if ext == "fish" => write_profile_fish,
+ _ => write_profile_sh,
+ };
+
+ match write_profile(&profile, contents, &formatted_home) {
+ Ok(()) => true,
+ Err(err) => {
+ warn!(
+ "Found profile script, but could not modify it: {}",
+ profile.display()
+ );
+ debug!("Profile modification error: {}", err);
+ prev
+ }
+ }
+ });
+
+ if found_profile {
+ Ok(())
+ } else {
+ Err(ErrorKind::NoShellProfile {
+ env_profile: String::new(),
+ bin_dir: home.shim_dir().to_owned(),
+ }
+ .into())
+ }
+ }
+
+ /// Returns a list of profile files to modify / create.
+ ///
+ /// Any file in the list should be created if it doesn't already exist
+ fn determine_profiles() -> Fallible<Vec<PathBuf>> {
+ let home_dir = dirs::home_dir().ok_or(ErrorKind::NoHomeEnvironmentVar)?;
+ let shell = env::var("SHELL").unwrap_or_else(|_| String::new());
+ // Always include `~/.profile`
+ let mut profiles = vec![home_dir.join(".profile")];
+
+ // PROFILE environment variable, if set
+ if let Ok(profile_env) = env::var("PROFILE") {
+ if !profile_env.is_empty() {
+ profiles.push(profile_env.into());
+ }
+ }
+
+ add_zsh_profile(&home_dir, &shell, &mut profiles);
+ add_bash_profiles(&home_dir, &shell, &mut profiles);
+ add_fish_profile(&home_dir, &shell, &mut profiles);
+
+ Ok(profiles)
+ }
+
+ /// Add zsh profile script, if necessary
+ fn add_zsh_profile(home_dir: &Path, shell: &str, profiles: &mut Vec<PathBuf>) {
+ let zdotdir_env = env::var("ZDOTDIR").unwrap_or_else(|_| String::new());
+ let zdotdir = if zdotdir_env.is_empty() {
+ home_dir
+ } else {
+ Path::new(&zdotdir_env)
+ };
+
+ let zshenv = zdotdir.join(".zshenv");
+
+ let zshrc = zdotdir.join(".zshrc");
+
+ if shell.contains("zsh") || zshenv.exists() {
+ profiles.push(zshenv);
+ } else if zshrc.exists() {
+ profiles.push(zshrc);
+ }
+ }
+
+ /// Add bash profile scripts, if necessary
+ ///
+ /// Note: We only add the bash scripts if they already exist, as creating new files can impact
+ /// the processing of existing files in bash (e.g. preventing ~/.profile from being loaded)
+ fn add_bash_profiles(home_dir: &Path, shell: &str, profiles: &mut Vec<PathBuf>) {
+ let mut bash_added = false;
+
+ let bashrc = home_dir.join(".bashrc");
+ if bashrc.exists() {
+ bash_added = true;
+ profiles.push(bashrc);
+ }
+
+ let bash_profile = home_dir.join(".bash_profile");
+ if bash_profile.exists() {
+ bash_added = true;
+ profiles.push(bash_profile);
+ }
+
+ if shell.contains("bash") && !bash_added {
+ let suggested_bash_profile = if cfg!(target_os = "macos") {
+ "~/.bash_profile"
+ } else {
+ "~/.bashrc"
+ };
+
+ warn!(
+ "We detected that you are using bash, however we couldn't find any bash profile scripts.
+If you run into problems running Volta, create {} and run `volta setup` again.",
+ suggested_bash_profile
+ );
+ }
+ }
+
+ /// Add fish profile scripts, if necessary
+ fn add_fish_profile(home_dir: &Path, shell: &str, profiles: &mut Vec<PathBuf>) {
+ let fish_config = home_dir.join(".config/fish/config.fish");
+
+ if shell.contains("fish") || fish_config.exists() {
+ profiles.push(fish_config);
+ }
+ }
+
+ fn read_profile_without_volta(path: &Path) -> Option<String> {
+ let file = File::open(path).ok()?;
+ let reader = BufReader::new(file);
+
+ reader
+ .lines()
+ .filter(|line_result| match line_result {
+ Ok(line) if !line.contains("VOLTA") => true,
+ Ok(_) => false,
+ Err(_) => true,
+ })
+ .collect::<io::Result<Vec<String>>>()
+ .map(|lines| lines.join("\n"))
+ .ok()
+ }
+
+ fn format_home(volta_home: &Path) -> String {
+ if let Some(home_dir) = env::var_os("HOME") {
+ if let Ok(suffix) = volta_home.strip_prefix(home_dir) {
+ // If the HOME environment variable is set _and_ the proposed VOLTA_HOME starts
+ // with that value, use $HOME when writing the profile scripts
+ return format!("$HOME/{}", suffix.display());
+ }
+ }
+
+ volta_home.display().to_string()
+ }
+
+ fn write_profile_sh(path: &Path, contents: String, volta_home: &str) -> io::Result<()> {
+ let mut file = File::create(path)?;
+ write!(
+ file,
+ "{}\nexport VOLTA_HOME=\"{}\"\nexport PATH=\"$VOLTA_HOME/bin:$PATH\"\n",
+ contents, volta_home,
+ )
+ }
+
+ fn write_profile_fish(path: &Path, contents: String, volta_home: &str) -> io::Result<()> {
+ let mut file = File::create(path)?;
+ write!(
+ file,
+ "{}\nset -gx VOLTA_HOME \"{}\"\nset -gx PATH \"$VOLTA_HOME/bin\" $PATH\n",
+ contents, volta_home,
+ )
+ }
+}
+
+#[cfg(windows)]
+mod os {
+ use std::process::Command;
+
+ use log::debug;
+ use volta_core::error::{Context, ErrorKind, Fallible};
+ use volta_core::layout::volta_home;
+ use winreg::enums::HKEY_CURRENT_USER;
+ use winreg::RegKey;
+
+ pub fn setup_environment() -> Fallible<()> {
+ let shim_dir = volta_home()?.shim_dir().to_string_lossy().to_string();
+ let hkcu = RegKey::predef(HKEY_CURRENT_USER);
+ let env = hkcu
+ .open_subkey("Environment")
+ .with_context(|| ErrorKind::ReadUserPathError)?;
+ let path: String = env
+ .get_value("Path")
+ .with_context(|| ErrorKind::ReadUserPathError)?;
+
+ if !path.contains(&shim_dir) {
+ // Use `setx` command to edit the user Path environment variable
+ let mut command = Command::new("setx");
+ command.arg("Path");
+ command.arg(format!("{};{}", shim_dir, path));
+
+ debug!("Modifying User Path with command: {:?}", command);
+ let output = command
+ .output()
+ .with_context(|| ErrorKind::WriteUserPathError)?;
+
+ if !output.status.success() {
+ debug!("[setx stderr]\n{}", String::from_utf8_lossy(&output.stderr));
+ debug!("[setx stdout]\n{}", String::from_utf8_lossy(&output.stdout));
+ return Err(ErrorKind::WriteUserPathError.into());
+ }
+ }
+
+ Ok(())
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +
use volta_core::error::{ErrorKind, ExitCode, Fallible};
+use volta_core::session::{ActivityKind, Session};
+use volta_core::tool;
+use volta_core::version::VersionSpec;
+
+use crate::command::Command;
+
+#[derive(clap::Args)]
+pub(crate) struct Uninstall {
+ /// The tool to uninstall, like `ember-cli-update`, `typescript`, or <package>
+ tool: String,
+}
+
+impl Command for Uninstall {
+ fn run(self, session: &mut Session) -> Fallible<ExitCode> {
+ session.add_event_start(ActivityKind::Uninstall);
+
+ let tool = tool::Spec::try_from_str(&self.tool)?;
+
+ // For packages, specifically report that we do not support uninstalling
+ // specific versions. For runtimes and package managers, we currently
+ // *intentionally* let this fall through to inform the user that we do
+ // not support uninstalling those *at all*.
+ if let tool::Spec::Package(_name, version) = &tool {
+ let VersionSpec::None = version else {
+ return Err(ErrorKind::Unimplemented {
+ feature: "uninstalling specific versions of tools".into(),
+ }
+ .into());
+ };
+ }
+
+ tool.uninstall()?;
+
+ session.add_event_end(ActivityKind::Uninstall, ExitCode::Success);
+ Ok(ExitCode::Success)
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +
use crate::command::Command;
+use volta_core::error::{ErrorKind, ExitCode, Fallible};
+use volta_core::session::{ActivityKind, Session};
+
+// NOTE: These use the same text as the `long_about` in crate::cli.
+// It's hard to abstract since it's in an attribute string.
+
+pub(crate) const USAGE: &str = "The subcommand `use` is deprecated.
+
+ To install a tool in your toolchain, use `volta install`.
+ To pin your project's runtime or package manager, use `volta pin`.
+";
+
+const ADVICE: &str = "
+ To install a tool in your toolchain, use `volta install`.
+ To pin your project's runtime or package manager, use `volta pin`.
+";
+
+#[derive(clap::Args)]
+pub(crate) struct Use {
+ #[allow(dead_code)]
+ anything: Vec<String>, // Prevent Clap argument errors when invoking e.g. `volta use node`
+}
+
+impl Command for Use {
+ fn run(self, session: &mut Session) -> Fallible<ExitCode> {
+ session.add_event_start(ActivityKind::Help);
+ let result = Err(ErrorKind::DeprecatedCommandError {
+ command: "use".to_string(),
+ advice: ADVICE.to_string(),
+ }
+ .into());
+ session.add_event_end(ActivityKind::Help, ExitCode::InvalidArguments);
+ result
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +
use std::env;
+use std::ffi::OsString;
+
+use which::which_in;
+
+use volta_core::error::{Context, ErrorKind, ExitCode, Fallible};
+use volta_core::platform::{Platform, System};
+use volta_core::run::binary::DefaultBinary;
+use volta_core::session::{ActivityKind, Session};
+
+use crate::command::Command;
+
+#[derive(clap::Args)]
+pub(crate) struct Which {
+ /// The binary to find, e.g. `node` or `npm`
+ binary: OsString,
+}
+
+impl Command for Which {
+ // 1. Start by checking if the user has a tool installed in the project or
+ // as a user default. If so, we're done.
+ // 2. Otherwise, use the platform image and/or the system environment to
+ // determine a lookup path to run `which` in.
+ fn run(self, session: &mut Session) -> Fallible<ExitCode> {
+ session.add_event_start(ActivityKind::Which);
+
+ let default_tool = DefaultBinary::from_name(&self.binary, session)?;
+ let project_bin_path = session
+ .project()?
+ .and_then(|project| project.find_bin(&self.binary));
+
+ let tool_path = match (default_tool, project_bin_path) {
+ (Some(_), Some(bin_path)) => Some(bin_path),
+ (Some(tool), _) => Some(tool.bin_path),
+ _ => None,
+ };
+
+ if let Some(path) = tool_path {
+ println!("{}", path.to_string_lossy());
+
+ let exit_code = ExitCode::Success;
+ session.add_event_end(ActivityKind::Which, exit_code);
+ return Ok(exit_code);
+ }
+
+ // Treat any error with obtaining the current platform image as if the image doesn't exist
+ // However, errors in obtaining the current working directory or the System path should
+ // still be treated as errors.
+ let path = match Platform::current(session)
+ .unwrap_or(None)
+ .and_then(|platform| platform.checkout(session).ok())
+ .and_then(|image| image.path().ok())
+ {
+ Some(path) => path,
+ None => System::path()?,
+ };
+
+ let cwd = env::current_dir().with_context(|| ErrorKind::CurrentDirError)?;
+ let exit_code = match which_in(&self.binary, Some(path), cwd) {
+ Ok(result) => {
+ println!("{}", result.to_string_lossy());
+ ExitCode::Success
+ }
+ Err(_) => {
+ // `which_in` Will return an Err if it can't find the binary in the path
+ // In that case, we don't want to print anything out, but we want to return
+ // Exit Code 1 (ExitCode::UnknownError)
+ ExitCode::UnknownError
+ }
+ };
+
+ session.add_event_end(ActivityKind::Which, exit_code);
+ Ok(exit_code)
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +
use std::process::{Command, ExitStatus};
+
+use volta_core::error::{Context, ErrorKind, VoltaError};
+use volta_core::layout::{volta_home, volta_install};
+
+pub enum Error {
+ Volta(VoltaError),
+ Tool(i32),
+}
+
+pub fn ensure_layout() -> Result<(), Error> {
+ let home = volta_home().map_err(Error::Volta)?;
+
+ if !home.layout_file().exists() {
+ let install = volta_install().map_err(Error::Volta)?;
+ Command::new(install.migrate_executable())
+ .env("VOLTA_LOGLEVEL", format!("{}", log::max_level()))
+ .status()
+ .with_context(|| ErrorKind::CouldNotStartMigration)
+ .into_result()?;
+ }
+
+ Ok(())
+}
+
+pub trait IntoResult<T> {
+ fn into_result(self) -> Result<T, Error>;
+}
+
+impl IntoResult<()> for Result<ExitStatus, VoltaError> {
+ fn into_result(self) -> Result<(), Error> {
+ match self {
+ Ok(status) => {
+ if status.success() {
+ Ok(())
+ } else {
+ let code = status.code().unwrap_or(1);
+ Err(Error::Tool(code))
+ }
+ }
+ Err(err) => Err(Error::Volta(err)),
+ }
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +
#[macro_use]
+mod command;
+mod cli;
+
+use clap::Parser;
+
+use volta_core::error::report_error;
+use volta_core::log::{LogContext, LogVerbosity, Logger};
+use volta_core::session::{ActivityKind, Session};
+
+mod common;
+use common::{ensure_layout, Error};
+
+/// The entry point for the `volta` CLI.
+pub fn main() {
+ let volta = cli::Volta::parse();
+ let verbosity = match (&volta.verbose, &volta.quiet) {
+ (false, false) => LogVerbosity::Default,
+ (true, false) => {
+ if volta.very_verbose {
+ LogVerbosity::VeryVerbose
+ } else {
+ LogVerbosity::Verbose
+ }
+ }
+ (false, true) => LogVerbosity::Quiet,
+ (true, true) => {
+ unreachable!("Clap should prevent the user from providing both --verbose and --quiet")
+ }
+ };
+ Logger::init(LogContext::Volta, verbosity).expect("Only a single logger should be initialized");
+ log::trace!("log level: {verbosity:?}");
+
+ let mut session = Session::init();
+ session.add_event_start(ActivityKind::Volta);
+
+ let result = ensure_layout().and_then(|()| volta.run(&mut session).map_err(Error::Volta));
+ match result {
+ Ok(exit_code) => {
+ session.add_event_end(ActivityKind::Volta, exit_code);
+ session.exit(exit_code);
+ }
+ Err(Error::Tool(code)) => {
+ session.add_event_tool_end(ActivityKind::Volta, code);
+ session.exit_tool(code);
+ }
+ Err(Error::Volta(err)) => {
+ report_error(env!("CARGO_PKG_VERSION"), &err);
+ session.add_event_error(ActivityKind::Volta, &err);
+ let code = err.exit_code();
+ session.add_event_end(ActivityKind::Volta, code);
+ session.exit(code);
+ }
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +
use std::ffi::OsStr;
+use std::process::Command;
+
+use cfg_if::cfg_if;
+
+cfg_if! {
+ if #[cfg(windows)] {
+ pub fn create_command<E>(exe: E) -> Command
+ where
+ E: AsRef<OsStr>
+ {
+ // Several of the node utilities are implemented as `.bat` or `.cmd` files
+ // When executing those files with `Command`, we need to call them with:
+ // cmd.exe /C <COMMAND> <ARGUMENTS>
+ // Instead of: <COMMAND> <ARGUMENTS>
+ // See: https://github.com/rust-lang/rust/issues/42791 For a longer discussion
+ let mut command = Command::new("cmd.exe");
+ command.arg("/C");
+ command.arg(exe);
+ command
+ }
+ } else {
+ pub fn create_command<E>(exe: E) -> Command
+ where
+ E: AsRef<OsStr>
+ {
+ Command::new(exe)
+ }
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402 +403 +404 +405 +406 +407 +408 +409 +410 +411 +412 +413 +414 +415 +416 +417 +418 +419 +420 +421 +422 +423 +424 +425 +426 +427 +428 +429 +430 +431 +432 +433 +434 +435 +436 +437 +438 +439 +440 +441 +442 +443 +444 +445 +446 +447 +448 +449 +450 +451 +452 +453 +454 +455 +456 +457 +458 +459 +460 +461 +462 +463 +464 +465 +466 +467 +468 +469 +470 +471 +472 +473 +474 +475 +476 +477 +478 +479 +480 +481 +482 +483 +484 +485 +486 +487 +488 +489 +490 +491 +492 +493 +494 +495 +496 +497 +498 +499 +500 +501 +502 +503 +504 +505 +506 +507 +508 +509 +510 +511 +512 +513 +514 +515 +516 +517 +518 +519 +520 +521 +522 +523 +524 +525 +526 +527 +528 +529 +530 +531 +532 +533 +534 +535 +536 +537 +538 +539 +540 +541 +542 +543 +544 +545 +546 +547 +548 +549 +550 +551 +552 +553 +554 +555 +556 +557 +558 +559 +560 +561 +562 +563 +564 +565 +566 +567 +568 +569 +570 +571 +572 +573 +574 +575 +576 +577 +578 +579 +580 +581 +582 +583 +584 +585 +586 +587 +588 +589 +590 +591 +592 +593 +594 +595 +596 +597 +598 +599 +600 +601 +602 +603 +604 +605 +606 +607 +608 +609 +610 +611 +612 +613 +614 +615 +616 +617 +618 +619 +620 +621 +622 +623 +624 +625 +626 +627 +628 +629 +630 +631 +632 +633 +634 +635 +636 +637 +638 +639 +640 +641 +642 +643 +644 +645 +646 +647 +648 +649 +650 +651 +652 +653 +654 +655 +656 +657 +658 +659 +660 +661 +662 +663 +664 +665 +666 +667 +668 +669 +670 +671 +672 +673 +674 +675 +676 +677 +678 +679 +680 +681 +682 +683 +684 +685 +686 +687 +688 +689 +690 +691 +692 +693 +694 +695 +696 +697 +698 +699 +700 +701 +702 +703 +704 +705 +706 +707 +708 +709 +710 +711 +712 +713 +714 +715 +716 +717 +718 +719 +720 +721 +722 +723 +724 +725 +726 +727 +728 +729 +730 +731 +732 +733 +734 +735 +736 +737 +738 +739 +740 +741 +742 +743 +744 +745 +746 +747 +748 +749 +750 +751 +752 +753 +754 +755 +756 +757 +758 +759 +760 +761 +762 +763 +764 +765 +766 +767 +768 +769 +770 +771 +772 +773 +774 +775 +776 +777 +778 +779 +780 +781 +782 +783 +784 +785 +786 +787 +788 +789 +790 +791 +792 +793 +794 +795 +796 +797 +798 +799 +800 +801 +802 +803 +804 +805 +806 +807 +808 +809 +810 +811 +812 +813 +814 +815 +816 +817 +818 +819 +820 +821 +822 +823 +824 +825 +826 +827 +828 +829 +830 +831 +832 +833 +834 +835 +836 +837 +838 +839 +840 +841 +842 +843 +844 +845 +846 +847 +848 +849 +850 +851 +852 +853 +854 +855 +856 +857 +858 +859 +860 +861 +862 +863 +864 +865 +866 +867 +868 +869 +870 +871 +872 +873 +874 +875 +876 +877 +878 +879 +880 +881 +882 +883 +884 +885 +886 +887 +888 +889 +890 +891 +892 +893 +894 +895 +896 +897 +898 +899 +900 +901 +902 +903 +904 +905 +906 +907 +908 +909 +910 +911 +912 +913 +914 +915 +916 +917 +918 +919 +920 +921 +922 +923 +924 +925 +926 +927 +928 +929 +930 +931 +932 +933 +934 +935 +936 +937 +938 +939 +940 +941 +942 +943 +944 +945 +946 +947 +948 +949 +950 +951 +952 +953 +954 +955 +956 +957 +958 +959 +960 +961 +962 +963 +964 +965 +966 +967 +968 +969 +970 +971 +972 +973 +974 +975 +976 +977 +978 +979 +980 +981 +982 +983 +984 +985 +986 +987 +988 +989 +990 +991 +992 +993 +994 +995 +996 +997 +998 +999 +1000 +1001 +1002 +1003 +1004 +1005 +1006 +1007 +1008 +1009 +1010 +1011 +1012 +1013 +1014 +1015 +1016 +1017 +1018 +1019 +1020 +1021 +1022 +1023 +1024 +1025 +1026 +1027 +1028 +1029 +1030 +1031 +1032 +1033 +1034 +1035 +1036 +1037 +1038 +1039 +1040 +1041 +1042 +1043 +1044 +1045 +1046 +1047 +1048 +1049 +1050 +1051 +1052 +1053 +1054 +1055 +1056 +1057 +1058 +1059 +1060 +1061 +1062 +1063 +1064 +1065 +1066 +1067 +1068 +1069 +1070 +1071 +1072 +1073 +1074 +1075 +1076 +1077 +1078 +1079 +1080 +1081 +1082 +1083 +1084 +1085 +1086 +1087 +1088 +1089 +1090 +1091 +1092 +1093 +1094 +1095 +1096 +1097 +1098 +1099 +1100 +1101 +1102 +1103 +1104 +1105 +1106 +1107 +1108 +1109 +1110 +1111 +1112 +1113 +1114 +1115 +1116 +1117 +1118 +1119 +1120 +1121 +1122 +1123 +1124 +1125 +1126 +1127 +1128 +1129 +1130 +1131 +1132 +1133 +1134 +1135 +1136 +1137 +1138 +1139 +1140 +1141 +1142 +1143 +1144 +1145 +1146 +1147 +1148 +1149 +1150 +1151 +1152 +1153 +1154 +1155 +1156 +1157 +1158 +1159 +1160 +1161 +1162 +1163 +1164 +1165 +1166 +1167 +1168 +1169 +1170 +1171 +1172 +1173 +1174 +1175 +1176 +1177 +1178 +1179 +1180 +1181 +1182 +1183 +1184 +1185 +1186 +1187 +1188 +1189 +1190 +1191 +1192 +1193 +1194 +1195 +1196 +1197 +1198 +1199 +1200 +1201 +1202 +1203 +1204 +1205 +1206 +1207 +1208 +1209 +1210 +1211 +1212 +1213 +1214 +1215 +1216 +1217 +1218 +1219 +1220 +1221 +1222 +1223 +1224 +1225 +1226 +1227 +1228 +1229 +1230 +1231 +1232 +1233 +1234 +1235 +1236 +1237 +1238 +1239 +1240 +1241 +1242 +1243 +1244 +1245 +1246 +1247 +1248 +1249 +1250 +1251 +1252 +1253 +1254 +1255 +1256 +1257 +1258 +1259 +1260 +1261 +1262 +1263 +1264 +1265 +1266 +1267 +1268 +1269 +1270 +1271 +1272 +1273 +1274 +1275 +1276 +1277 +1278 +1279 +1280 +1281 +1282 +1283 +1284 +1285 +1286 +1287 +1288 +1289 +1290 +1291 +1292 +1293 +1294 +1295 +1296 +1297 +1298 +1299 +1300 +1301 +1302 +1303 +1304 +1305 +1306 +1307 +1308 +1309 +1310 +1311 +1312 +1313 +1314 +1315 +1316 +1317 +1318 +1319 +1320 +1321 +1322 +1323 +1324 +1325 +1326 +1327 +1328 +1329 +1330 +1331 +1332 +1333 +1334 +1335 +1336 +1337 +1338 +1339 +1340 +1341 +1342 +1343 +1344 +1345 +1346 +1347 +1348 +1349 +1350 +1351 +1352 +1353 +1354 +1355 +1356 +1357 +1358 +1359 +1360 +1361 +1362 +1363 +1364 +1365 +1366 +1367 +1368 +1369 +1370 +1371 +1372 +1373 +1374 +1375 +1376 +1377 +1378 +1379 +1380 +1381 +1382 +1383 +1384 +1385 +1386 +1387 +1388 +1389 +1390 +1391 +1392 +1393 +1394 +1395 +1396 +1397 +1398 +1399 +1400 +1401 +1402 +1403 +1404 +1405 +1406 +1407 +1408 +1409 +1410 +1411 +1412 +1413 +1414 +1415 +1416 +1417 +1418 +1419 +1420 +1421 +1422 +1423 +1424 +1425 +1426 +1427 +1428 +1429 +1430 +1431 +1432 +1433 +1434 +1435 +1436 +1437 +1438 +1439 +1440 +1441 +1442 +1443 +1444 +1445 +1446 +1447 +1448 +1449 +1450 +1451 +1452 +1453 +1454 +1455 +1456 +1457 +1458 +1459 +1460 +1461 +1462 +1463 +1464 +1465 +1466 +1467 +1468 +1469 +1470 +1471 +1472 +1473 +1474 +1475 +1476 +1477 +1478 +1479 +1480 +1481 +1482 +1483 +1484 +1485 +1486 +1487 +1488 +1489 +1490 +1491 +1492 +1493 +1494 +1495 +1496 +1497 +1498 +1499 +1500 +1501 +1502 +1503 +1504 +1505 +1506 +1507 +1508 +1509 +1510 +1511 +1512 +1513 +1514 +1515 +1516 +1517 +1518 +1519 +1520 +1521 +1522 +1523 +1524 +1525 +1526 +1527 +1528 +1529 +1530 +1531 +1532 +1533 +1534 +1535 +1536 +1537 +1538 +1539 +1540 +1541 +1542 +1543 +1544 +1545 +1546 +1547 +1548 +1549 +1550 +1551 +1552 +1553 +1554 +1555 +1556 +1557 +1558 +1559 +1560 +1561 +1562 +1563 +1564 +1565 +1566 +1567 +1568 +1569 +1570 +1571 +1572 +1573 +1574 +1575 +1576 +1577 +1578 +1579 +1580 +1581 +1582 +
use std::fmt;
+use std::path::PathBuf;
+
+use super::ExitCode;
+use crate::style::{text_width, tool_version};
+use crate::tool;
+use crate::tool::package::PackageManager;
+use textwrap::{fill, indent};
+
+const REPORT_BUG_CTA: &str =
+ "Please rerun the command that triggered this error with the environment
+variable `VOLTA_LOGLEVEL` set to `debug` and open an issue at
+https://github.com/volta-cli/volta/issues with the details!";
+
+const PERMISSIONS_CTA: &str = "Please ensure you have correct permissions to the Volta directory.";
+
+#[derive(Debug)]
+#[cfg_attr(test, derive(PartialEq, Eq))]
+pub enum ErrorKind {
+ /// Thrown when package tries to install a binary that is already installed.
+ BinaryAlreadyInstalled {
+ bin_name: String,
+ existing_package: String,
+ new_package: String,
+ },
+
+ /// Thrown when executing an external binary fails
+ BinaryExecError,
+
+ /// Thrown when a binary could not be found in the local inventory
+ BinaryNotFound {
+ name: String,
+ },
+
+ /// Thrown when building the virtual environment path fails
+ BuildPathError,
+
+ /// Thrown when unable to launch a command with VOLTA_BYPASS set
+ BypassError {
+ command: String,
+ },
+
+ /// Thrown when a user tries to `volta fetch` something other than node/yarn/npm.
+ CannotFetchPackage {
+ package: String,
+ },
+
+ /// Thrown when a user tries to `volta pin` something other than node/yarn/npm.
+ CannotPinPackage {
+ package: String,
+ },
+
+ /// Thrown when the Completions out-dir is not a directory
+ CompletionsOutFileError {
+ path: PathBuf,
+ },
+
+ /// Thrown when the containing directory could not be determined
+ ContainingDirError {
+ path: PathBuf,
+ },
+
+ CouldNotDetermineTool,
+
+ /// Thrown when unable to start the migration executable
+ CouldNotStartMigration,
+
+ CreateDirError {
+ dir: PathBuf,
+ },
+
+ /// Thrown when unable to create the layout file
+ CreateLayoutFileError {
+ file: PathBuf,
+ },
+
+ /// Thrown when unable to create a link to the shared global library directory
+ CreateSharedLinkError {
+ name: String,
+ },
+
+ /// Thrown when creating a temporary directory fails
+ CreateTempDirError {
+ in_dir: PathBuf,
+ },
+
+ /// Thrown when creating a temporary file fails
+ CreateTempFileError {
+ in_dir: PathBuf,
+ },
+
+ CurrentDirError,
+
+ /// Thrown when deleting a directory fails
+ DeleteDirectoryError {
+ directory: PathBuf,
+ },
+
+ /// Thrown when deleting a file fails
+ DeleteFileError {
+ file: PathBuf,
+ },
+
+ DeprecatedCommandError {
+ command: String,
+ advice: String,
+ },
+
+ DownloadToolNetworkError {
+ tool: tool::Spec,
+ from_url: String,
+ },
+
+ /// Thrown when unable to execute a hook command
+ ExecuteHookError {
+ command: String,
+ },
+
+ /// Thrown when `volta.extends` keys result in an infinite cycle
+ ExtensionCycleError {
+ paths: Vec<PathBuf>,
+ duplicate: PathBuf,
+ },
+
+ /// Thrown when determining the path to an extension manifest fails
+ ExtensionPathError {
+ path: PathBuf,
+ },
+
+ /// Thrown when a hook command returns a non-zero exit code
+ HookCommandFailed {
+ command: String,
+ },
+
+ /// Thrown when a hook contains multiple fields (prefix, template, or bin)
+ HookMultipleFieldsSpecified,
+
+ /// Thrown when a hook doesn't contain any of the known fields (prefix, template, or bin)
+ HookNoFieldsSpecified,
+
+ /// Thrown when determining the path to a hook fails
+ HookPathError {
+ command: String,
+ },
+
+ /// Thrown when determining the name of a newly-installed package fails
+ InstalledPackageNameError,
+
+ InvalidHookCommand {
+ command: String,
+ },
+
+ /// Thrown when output from a hook command could not be read
+ InvalidHookOutput {
+ command: String,
+ },
+
+ /// Thrown when a user does e.g. `volta install node 12` instead of
+ /// `volta install node@12`.
+ InvalidInvocation {
+ action: String,
+ name: String,
+ version: String,
+ },
+
+ /// Thrown when a user does e.g. `volta install 12` instead of
+ /// `volta install node@12`.
+ InvalidInvocationOfBareVersion {
+ action: String,
+ version: String,
+ },
+
+ /// Thrown when a format other than "npm" or "github" is given for yarn.index in the hooks
+ InvalidRegistryFormat {
+ format: String,
+ },
+
+ /// Thrown when a tool name is invalid per npm's rules.
+ InvalidToolName {
+ name: String,
+ errors: Vec<String>,
+ },
+
+ /// Thrown when unable to acquire a lock on the Volta directory
+ LockAcquireError,
+
+ /// Thrown when pinning or installing npm@bundled and couldn't detect the bundled version
+ NoBundledNpm {
+ command: String,
+ },
+
+ /// Thrown when pnpm is not set at the command-line
+ NoCommandLinePnpm,
+
+ /// Thrown when Yarn is not set at the command-line
+ NoCommandLineYarn,
+
+ /// Thrown when a user tries to install a Yarn or npm version before installing a Node version.
+ NoDefaultNodeVersion {
+ tool: String,
+ },
+
+ /// Thrown when there is no Node version matching a requested semver specifier.
+ NodeVersionNotFound {
+ matching: String,
+ },
+
+ NoHomeEnvironmentVar,
+
+ /// Thrown when the install dir could not be determined
+ NoInstallDir,
+
+ NoLocalDataDir,
+
+ /// Thrown when a user tries to pin a npm, pnpm, or Yarn version before pinning a Node version.
+ NoPinnedNodeVersion {
+ tool: String,
+ },
+
+ /// Thrown when the platform (Node version) could not be determined
+ NoPlatform,
+
+ /// Thrown when parsing the project manifest and there is a `"volta"` key without Node
+ NoProjectNodeInManifest,
+
+ /// Thrown when Yarn is not set in a project
+ NoProjectYarn,
+
+ /// Thrown when pnpm is not set in a project
+ NoProjectPnpm,
+
+ /// Thrown when no shell profiles could be found
+ NoShellProfile {
+ env_profile: String,
+ bin_dir: PathBuf,
+ },
+
+ /// Thrown when the user tries to pin Node or Yarn versions outside of a package.
+ NotInPackage,
+
+ /// Thrown when default Yarn is not set
+ NoDefaultYarn,
+
+ /// Thrown when default pnpm is not set
+ NoDefaultPnpm,
+
+ /// Thrown when `npm link` is called with a package that isn't available
+ NpmLinkMissingPackage {
+ package: String,
+ },
+
+ /// Thrown when `npm link` is called with a package that was not installed / linked with npm
+ NpmLinkWrongManager {
+ package: String,
+ },
+
+ /// Thrown when there is no npm version matching the requested Semver/Tag
+ NpmVersionNotFound {
+ matching: String,
+ },
+
+ NpxNotAvailable {
+ version: String,
+ },
+
+ /// Thrown when the command to install a global package is not successful
+ PackageInstallFailed {
+ package: String,
+ },
+
+ /// Thrown when parsing the package manifest fails
+ PackageManifestParseError {
+ package: String,
+ },
+
+ /// Thrown when reading the package manifest fails
+ PackageManifestReadError {
+ package: String,
+ },
+
+ /// Thrown when a specified package could not be found on the npm registry
+ PackageNotFound {
+ package: String,
+ },
+
+ /// Thrown when parsing a package manifest fails
+ PackageParseError {
+ file: PathBuf,
+ },
+
+ /// Thrown when reading a package manifest fails
+ PackageReadError {
+ file: PathBuf,
+ },
+
+ /// Thrown when a package has been unpacked but is not formed correctly.
+ PackageUnpackError,
+
+ /// Thrown when writing a package manifest fails
+ PackageWriteError {
+ file: PathBuf,
+ },
+
+ /// Thrown when unable to parse a bin config file
+ ParseBinConfigError,
+
+ /// Thrown when unable to parse a hooks.json file
+ ParseHooksError {
+ file: PathBuf,
+ },
+
+ /// Thrown when unable to parse the node index cache
+ ParseNodeIndexCacheError,
+
+ /// Thrown when unable to parse the node index
+ ParseNodeIndexError {
+ from_url: String,
+ },
+
+ /// Thrown when unable to parse the node index cache expiration
+ ParseNodeIndexExpiryError,
+
+ /// Thrown when unable to parse the npm manifest file from a node install
+ ParseNpmManifestError,
+
+ /// Thrown when unable to parse a package configuration
+ ParsePackageConfigError,
+
+ /// Thrown when unable to parse the platform.json file
+ ParsePlatformError,
+
+ /// Thrown when unable to parse a tool spec (`<tool>[@<version>]`)
+ ParseToolSpecError {
+ tool_spec: String,
+ },
+
+ /// Thrown when persisting an archive to the inventory fails
+ PersistInventoryError {
+ tool: String,
+ },
+
+ /// Thrown when there is no pnpm version matching a requested semver specifier.
+ PnpmVersionNotFound {
+ matching: String,
+ },
+
+ /// Thrown when executing a project-local binary fails
+ ProjectLocalBinaryExecError {
+ command: String,
+ },
+
+ /// Thrown when a project-local binary could not be found
+ ProjectLocalBinaryNotFound {
+ command: String,
+ },
+
+ /// Thrown when a publish hook contains both the url and bin fields
+ PublishHookBothUrlAndBin,
+
+ /// Thrown when a publish hook contains neither url nor bin fields
+ PublishHookNeitherUrlNorBin,
+
+ /// Thrown when there was an error reading the user bin directory
+ ReadBinConfigDirError {
+ dir: PathBuf,
+ },
+
+ /// Thrown when there was an error reading the config for a binary
+ ReadBinConfigError {
+ file: PathBuf,
+ },
+
+ /// Thrown when unable to read the default npm version file
+ ReadDefaultNpmError {
+ file: PathBuf,
+ },
+
+ /// Thrown when unable to read the contents of a directory
+ ReadDirError {
+ dir: PathBuf,
+ },
+
+ /// Thrown when there was an error opening a hooks.json file
+ ReadHooksError {
+ file: PathBuf,
+ },
+
+ /// Thrown when there was an error reading the Node Index Cache
+ ReadNodeIndexCacheError {
+ file: PathBuf,
+ },
+
+ /// Thrown when there was an error reading the Node Index Cache Expiration
+ ReadNodeIndexExpiryError {
+ file: PathBuf,
+ },
+
+ /// Thrown when there was an error reading the npm manifest file
+ ReadNpmManifestError,
+
+ /// Thrown when there was an error reading a package configuration file
+ ReadPackageConfigError {
+ file: PathBuf,
+ },
+
+ /// Thrown when there was an error opening the user platform file
+ ReadPlatformError {
+ file: PathBuf,
+ },
+
+ /// Thrown when unable to read the user Path environment variable from the registry
+ #[cfg(windows)]
+ ReadUserPathError,
+
+ /// Thrown when the public registry for Node or Yarn could not be downloaded.
+ RegistryFetchError {
+ tool: String,
+ from_url: String,
+ },
+
+ /// Thrown when the shim binary is called directly, not through a symlink
+ RunShimDirectly,
+
+ /// Thrown when there was an error setting a tool to executable
+ SetToolExecutable {
+ tool: String,
+ },
+
+ /// Thrown when there was an error copying an unpacked tool to the image directory
+ SetupToolImageError {
+ tool: String,
+ version: String,
+ dir: PathBuf,
+ },
+
+ /// Thrown when Volta is unable to create a shim
+ ShimCreateError {
+ name: String,
+ },
+
+ /// Thrown when Volta is unable to remove a shim
+ ShimRemoveError {
+ name: String,
+ },
+
+ /// Thrown when serializing a bin config to JSON fails
+ StringifyBinConfigError,
+
+ /// Thrown when serializing a package config to JSON fails
+ StringifyPackageConfigError,
+
+ /// Thrown when serializing the platform to JSON fails
+ StringifyPlatformError,
+
+ /// Thrown when a given feature has not yet been implemented
+ Unimplemented {
+ feature: String,
+ },
+
+ /// Thrown when unpacking an archive (tarball or zip) fails
+ UnpackArchiveError {
+ tool: String,
+ version: String,
+ },
+
+ /// Thrown when a package to upgrade was not found
+ UpgradePackageNotFound {
+ package: String,
+ manager: PackageManager,
+ },
+
+ /// Thrown when a package to upgrade was installed with a different package manager
+ UpgradePackageWrongManager {
+ package: String,
+ manager: PackageManager,
+ },
+
+ VersionParseError {
+ version: String,
+ },
+
+ /// Thrown when there was an error writing a bin config file
+ WriteBinConfigError {
+ file: PathBuf,
+ },
+
+ /// Thrown when there was an error writing the default npm to file
+ WriteDefaultNpmError {
+ file: PathBuf,
+ },
+
+ /// Thrown when there was an error writing the npm launcher
+ WriteLauncherError {
+ tool: String,
+ },
+
+ /// Thrown when there was an error writing the node index cache
+ WriteNodeIndexCacheError {
+ file: PathBuf,
+ },
+
+ /// Thrown when there was an error writing the node index expiration
+ WriteNodeIndexExpiryError {
+ file: PathBuf,
+ },
+
+ /// Thrown when there was an error writing a package config
+ WritePackageConfigError {
+ file: PathBuf,
+ },
+
+ /// Thrown when writing the platform.json file fails
+ WritePlatformError {
+ file: PathBuf,
+ },
+
+ /// Thrown when unable to write the user PATH environment variable
+ #[cfg(windows)]
+ WriteUserPathError,
+
+ /// Thrown when a user attempts to install a version of Yarn2
+ Yarn2NotSupported,
+
+ /// Thrown when there is an error fetching the latest version of Yarn
+ YarnLatestFetchError {
+ from_url: String,
+ },
+
+ /// Thrown when there is no Yarn version matching a requested semver specifier.
+ YarnVersionNotFound {
+ matching: String,
+ },
+}
+
+impl fmt::Display for ErrorKind {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match self {
+ ErrorKind::BinaryAlreadyInstalled {
+ bin_name,
+ existing_package,
+ new_package,
+ } => write!(
+ f,
+ "Executable '{}' is already installed by {}
+
+Please remove {} before installing {}",
+ bin_name, existing_package, existing_package, new_package
+ ),
+ ErrorKind::BinaryExecError => write!(
+ f,
+ "Could not execute command.
+
+See `volta help install` and `volta help pin` for info about making tools available."
+ ),
+ ErrorKind::BinaryNotFound { name } => write!(
+ f,
+ r#"Could not find executable "{}"
+
+Use `volta install` to add a package to your toolchain (see `volta help install` for more info)."#,
+ name
+ ),
+ ErrorKind::BuildPathError => write!(
+ f,
+ "Could not create execution environment.
+
+Please ensure your PATH is valid."
+ ),
+ ErrorKind::BypassError { command } => write!(
+ f,
+ "Could not execute command '{}'
+
+VOLTA_BYPASS is enabled, please ensure that the command exists on your system or unset VOLTA_BYPASS",
+ command,
+ ),
+ ErrorKind::CannotFetchPackage { package } => write!(
+ f,
+ "Fetching packages without installing them is not supported.
+
+Use `volta install {}` to update the default version.",
+ package
+ ),
+ ErrorKind::CannotPinPackage { package } => write!(
+ f,
+ "Only node and yarn can be pinned in a project
+
+Use `npm install` or `yarn add` to select a version of {} for this project.",
+ package
+ ),
+ ErrorKind::CompletionsOutFileError { path } => write!(
+ f,
+ "Completions file `{}` already exists.
+
+Please remove the file or pass `-f` or `--force` to override.",
+ path.display()
+ ),
+ ErrorKind::ContainingDirError { path } => write!(
+ f,
+ "Could not create the containing directory for {}
+
+{}",
+ path.display(),
+ PERMISSIONS_CTA
+ ),
+ ErrorKind::CouldNotDetermineTool => write!(
+ f,
+ "Could not determine tool name
+
+{}",
+ REPORT_BUG_CTA
+ ),
+ ErrorKind::CouldNotStartMigration => write!(
+ f,
+ "Could not start migration process to upgrade your Volta directory.
+
+Please ensure you have 'volta-migrate' on your PATH and run it directly."
+ ),
+ ErrorKind::CreateDirError { dir } => write!(
+ f,
+ "Could not create directory {}
+
+Please ensure that you have the correct permissions.",
+ dir.display()
+ ),
+ ErrorKind::CreateLayoutFileError { file } => write!(
+ f,
+ "Could not create layout file {}
+
+{}",
+ file.display(), PERMISSIONS_CTA
+ ),
+ ErrorKind::CreateSharedLinkError { name } => write!(
+ f,
+ "Could not create shared environment for package '{}'
+
+{}",
+ name, PERMISSIONS_CTA
+ ),
+ ErrorKind::CreateTempDirError { in_dir } => write!(
+ f,
+ "Could not create temporary directory
+in {}
+
+{}",
+ in_dir.display(),
+ PERMISSIONS_CTA
+ ),
+ ErrorKind::CreateTempFileError { in_dir } => write!(
+ f,
+ "Could not create temporary file
+in {}
+
+{}",
+ in_dir.display(),
+ PERMISSIONS_CTA
+ ),
+ ErrorKind::CurrentDirError => write!(
+ f,
+ "Could not determine current directory
+
+Please ensure that you have the correct permissions."
+ ),
+ ErrorKind::DeleteDirectoryError { directory } => write!(
+ f,
+ "Could not remove directory
+at {}
+
+{}",
+ directory.display(),
+ PERMISSIONS_CTA
+ ),
+ ErrorKind::DeleteFileError { file } => write!(
+ f,
+ "Could not remove file
+at {}
+
+{}",
+ file.display(),
+ PERMISSIONS_CTA
+ ),
+ ErrorKind::DeprecatedCommandError { command, advice } => {
+ write!(f, "The subcommand `{}` is deprecated.\n{}", command, advice)
+ }
+ ErrorKind::DownloadToolNetworkError { tool, from_url } => write!(
+ f,
+ "Could not download {}
+from {}
+
+Please verify your internet connection and ensure the correct version is specified.",
+ tool, from_url
+ ),
+ ErrorKind::ExecuteHookError { command } => write!(
+ f,
+ "Could not execute hook command: '{}'
+
+Please ensure that the correct command is specified.",
+ command
+ ),
+ ErrorKind::ExtensionCycleError { paths, duplicate } => {
+ // Detected infinite loop in project workspace:
+ //
+ // --> /home/user/workspace/project/package.json
+ // /home/user/workspace/package.json
+ // --> /home/user/workspace/project/package.json
+ //
+ // Please ensure that project workspaces do not depend on each other.
+ f.write_str("Detected infinite loop in project workspace:\n\n")?;
+
+ for path in paths {
+ if path == duplicate {
+ f.write_str("--> ")?;
+ } else {
+ f.write_str(" ")?;
+ }
+
+ writeln!(f, "{}", path.display())?;
+ }
+
+ writeln!(f, "--> {}", duplicate.display())?;
+ writeln!(f)?;
+
+ f.write_str("Please ensure that project workspaces do not depend on each other.")
+ }
+ ErrorKind::ExtensionPathError { path } => write!(
+ f,
+ "Could not determine path to project workspace: '{}'
+
+Please ensure that the file exists and is accessible.",
+ path.display(),
+ ),
+ ErrorKind::HookCommandFailed { command } => write!(
+ f,
+ "Hook command '{}' indicated a failure.
+
+Please verify the requested tool and version.",
+ command
+ ),
+ ErrorKind::HookMultipleFieldsSpecified => write!(
+ f,
+ "Hook configuration includes multiple hook types.
+
+Please include only one of 'bin', 'prefix', or 'template'"
+ ),
+ ErrorKind::HookNoFieldsSpecified => write!(
+ f,
+ "Hook configuration includes no hook types.
+
+Please include one of 'bin', 'prefix', or 'template'"
+ ),
+ ErrorKind::HookPathError { command } => write!(
+ f,
+ "Could not determine path to hook command: '{}'
+
+Please ensure that the correct command is specified.",
+ command
+ ),
+ ErrorKind::InstalledPackageNameError => write!(
+ f,
+ "Could not determine the name of the package that was just installed.
+
+{}",
+ REPORT_BUG_CTA
+ ),
+ ErrorKind::InvalidHookCommand { command } => write!(
+ f,
+ "Invalid hook command: '{}'
+
+Please ensure that the correct command is specified.",
+ command
+ ),
+ ErrorKind::InvalidHookOutput { command } => write!(
+ f,
+ "Could not read output from hook command: '{}'
+
+Please ensure that the command output is valid UTF-8 text.",
+ command
+ ),
+
+ ErrorKind::InvalidInvocation {
+ action,
+ name,
+ version,
+ } => {
+ let error = format!(
+ "`volta {action} {name} {version}` is not supported.",
+ action = action,
+ name = name,
+ version = version
+ );
+
+ let call_to_action = format!(
+"To {action} '{name}' version '{version}', please run `volta {action} {formatted}`. \
+To {action} the packages '{name}' and '{version}', please {action} them in separate commands, or with explicit versions.",
+ action=action,
+ name=name,
+ version=version,
+ formatted=tool_version(name, version)
+ );
+
+ let wrapped_cta = match text_width() {
+ Some(width) => fill(&call_to_action, width),
+ None => call_to_action,
+ };
+
+ write!(f, "{}\n\n{}", error, wrapped_cta)
+ }
+
+ ErrorKind::InvalidInvocationOfBareVersion {
+ action,
+ version,
+ } => {
+ let error = format!(
+ "`volta {action} {version}` is not supported.",
+ action = action,
+ version = version
+ );
+
+ let call_to_action = format!(
+"To {action} node version '{version}', please run `volta {action} {formatted}`. \
+To {action} the package '{version}', please use an explicit version such as '{version}@latest'.",
+ action=action,
+ version=version,
+ formatted=tool_version("node", version)
+ );
+
+ let wrapped_cta = match text_width() {
+ Some(width) => fill(&call_to_action, width),
+ None => call_to_action,
+ };
+
+ write!(f, "{}\n\n{}", error, wrapped_cta)
+ }
+
+ ErrorKind::InvalidRegistryFormat { format } => write!(
+ f,
+ "Unrecognized index registry format: '{}'
+
+Please specify either 'npm' or 'github' for the format.",
+format
+ ),
+
+ ErrorKind::InvalidToolName { name, errors } => {
+ let indentation = " ";
+ let wrapped = match text_width() {
+ Some(width) => fill(&errors.join("\n"), width - indentation.len()),
+ None => errors.join("\n"),
+ };
+ let formatted_errs = indent(&wrapped, indentation);
+
+ let call_to_action = if errors.len() > 1 {
+ "Please fix the following errors:"
+ } else {
+ "Please fix the following error:"
+ };
+
+ write!(
+ f,
+ "Invalid tool name `{}`\n\n{}\n{}",
+ name, call_to_action, formatted_errs
+ )
+ }
+ // Note: No CTA as this error is purely informational and shouldn't be exposed to the user
+ ErrorKind::LockAcquireError => write!(
+ f,
+ "Unable to acquire lock on Volta directory"
+ ),
+ ErrorKind::NoBundledNpm { command } => write!(
+ f,
+ "Could not detect bundled npm version.
+
+Please ensure you have a Node version selected with `volta {} node` (see `volta help {0}` for more info).",
+ command
+ ),
+ ErrorKind::NoCommandLinePnpm => write!(
+ f,
+ "No pnpm version specified.
+
+Use `volta run --pnpm` to select a version (see `volta help run` for more info)."
+ ),
+ ErrorKind::NoCommandLineYarn => write!(
+ f,
+ "No Yarn version specified.
+
+Use `volta run --yarn` to select a version (see `volta help run` for more info)."
+ ),
+ ErrorKind::NoDefaultNodeVersion { tool } => write!(
+ f,
+ "Cannot install {} because the default Node version is not set.
+
+Use `volta install node` to select a default Node first, then install a {0} version.",
+ tool
+ ),
+ ErrorKind::NodeVersionNotFound { matching } => write!(
+ f,
+ r#"Could not find Node version matching "{}" in the version registry.
+
+Please verify that the version is correct."#,
+ matching
+ ),
+ ErrorKind::NoHomeEnvironmentVar => write!(
+ f,
+ "Could not determine home directory.
+
+Please ensure the environment variable 'HOME' is set."
+ ),
+ ErrorKind::NoInstallDir => write!(
+ f,
+ "Could not determine Volta install directory.
+
+Please ensure Volta was installed correctly"
+ ),
+ ErrorKind::NoLocalDataDir => write!(
+ f,
+ "Could not determine LocalAppData directory.
+
+Please ensure the directory is available."
+ ),
+ ErrorKind::NoPinnedNodeVersion { tool } => write!(
+ f,
+ "Cannot pin {} because the Node version is not pinned in this project.
+
+Use `volta pin node` to pin Node first, then pin a {0} version.",
+ tool
+ ),
+ ErrorKind::NoPlatform => write!(
+ f,
+ "Node is not available.
+
+To run any Node command, first set a default version using `volta install node`"
+ ),
+ ErrorKind::NoProjectNodeInManifest => write!(
+ f,
+ "No Node version found in this project.
+
+Use `volta pin node` to select a version (see `volta help pin` for more info)."
+ ),
+ ErrorKind::NoProjectPnpm => write!(
+ f,
+ "No pnpm version found in this project.
+
+Use `volta pin pnpm` to select a version (see `volta help pin` for more info)."
+ ),
+ ErrorKind::NoProjectYarn => write!(
+ f,
+ "No Yarn version found in this project.
+
+Use `volta pin yarn` to select a version (see `volta help pin` for more info)."
+ ),
+ ErrorKind::NoShellProfile { env_profile, bin_dir } => write!(
+ f,
+ "Could not locate user profile.
+Tried $PROFILE ({}), ~/.bashrc, ~/.bash_profile, ~/.zshenv ~/.zshrc, ~/.profile, and ~/.config/fish/config.fish
+
+Please create one of these and try again; or you can edit your profile manually to add '{}' to your PATH",
+ env_profile, bin_dir.display()
+ ),
+ ErrorKind::NotInPackage => write!(
+ f,
+ "Not in a node package.
+
+Use `volta install` to select a default version of a tool."
+ ),
+ ErrorKind::NoDefaultPnpm => write!(
+ f,
+ "pnpm is not available.
+
+Use `volta install pnpm` to select a default version (see `volta help install` for more info)."
+ ),
+ ErrorKind::NoDefaultYarn => write!(
+ f,
+ "Yarn is not available.
+
+Use `volta install yarn` to select a default version (see `volta help install` for more info)."
+ ),
+ ErrorKind::NpmLinkMissingPackage { package } => write!(
+ f,
+ "Could not locate the package '{}'
+
+Please ensure it is available by running `npm link` in its source directory.",
+ package
+ ),
+ ErrorKind::NpmLinkWrongManager { package } => write!(
+ f,
+ "The package '{}' was not installed using npm and cannot be linked with `npm link`
+
+Please ensure it is linked with `npm link` or installed with `npm i -g {0}`.",
+ package
+ ),
+ ErrorKind::NpmVersionNotFound { matching } => write!(
+ f,
+ r#"Could not find Node version matching "{}" in the version registry.
+
+Please verify that the version is correct."#,
+ matching
+ ),
+ ErrorKind::NpxNotAvailable { version } => write!(
+ f,
+ "'npx' is only available with npm >= 5.2.0
+
+This project is configured to use version {} of npm.",
+ version
+ ),
+ ErrorKind::PackageInstallFailed { package } => write!(
+ f,
+ "Could not install package '{}'
+
+Please confirm the package is valid and run with `--verbose` for more diagnostics.",
+ package
+ ),
+ ErrorKind::PackageManifestParseError { package } => write!(
+ f,
+ "Could not parse package.json manifest for {}
+
+Please ensure the package includes a valid manifest file.",
+ package
+ ),
+ ErrorKind::PackageManifestReadError { package } => write!(
+ f,
+ "Could not read package.json manifest for {}
+
+Please ensure the package includes a valid manifest file.",
+ package
+ ),
+ ErrorKind::PackageNotFound { package } => write!(
+ f,
+ "Could not find '{}' in the package registry.
+
+Please verify the requested package is correct.",
+ package
+ ),
+ ErrorKind::PackageParseError { file } => write!(
+ f,
+ "Could not parse project manifest
+at {}
+
+Please ensure that the file is correctly formatted.",
+ file.display()
+ ),
+ ErrorKind::PackageReadError { file } => write!(
+ f,
+ "Could not read project manifest
+from {}
+
+Please ensure that the file exists.",
+ file.display()
+ ),
+ ErrorKind::PackageUnpackError => write!(
+ f,
+ "Could not determine package directory layout.
+
+Please ensure the package is correctly formatted."
+ ),
+ ErrorKind::PackageWriteError { file } => write!(
+ f,
+ "Could not write project manifest
+to {}
+
+Please ensure you have correct permissions.",
+ file.display()
+ ),
+ ErrorKind::ParseBinConfigError => write!(
+ f,
+ "Could not parse executable configuration file.
+
+{}",
+ REPORT_BUG_CTA
+ ),
+ ErrorKind::ParseHooksError { file } => write!(
+ f,
+ "Could not parse hooks configuration file.
+from {}
+
+Please ensure the file is correctly formatted.",
+ file.display()
+ ),
+ ErrorKind::ParseNodeIndexCacheError => write!(
+ f,
+ "Could not parse Node index cache file.
+
+{}",
+ REPORT_BUG_CTA
+ ),
+ ErrorKind::ParseNodeIndexError { from_url } => write!(
+ f,
+ "Could not parse Node version index
+from {}
+
+Please verify your internet connection.",
+ from_url
+ ),
+ ErrorKind::ParseNodeIndexExpiryError => write!(
+ f,
+ "Could not parse Node index cache expiration file.
+
+{}",
+ REPORT_BUG_CTA
+ ),
+ ErrorKind::ParseNpmManifestError => write!(
+ f,
+ "Could not parse package.json file for bundled npm.
+
+Please ensure the version of Node is correct."
+ ),
+ ErrorKind::ParsePackageConfigError => write!(
+ f,
+ "Could not parse package configuration file.
+
+{}",
+ REPORT_BUG_CTA
+ ),
+ ErrorKind::ParsePlatformError => write!(
+ f,
+ "Could not parse platform settings file.
+
+{}",
+ REPORT_BUG_CTA
+ ),
+ ErrorKind::ParseToolSpecError { tool_spec } => write!(
+ f,
+ "Could not parse tool spec `{}`
+
+Please supply a spec in the format `<tool name>[@<version>]`.",
+ tool_spec
+ ),
+ ErrorKind::PersistInventoryError { tool } => write!(
+ f,
+ "Could not store {} archive in inventory cache
+
+{}",
+ tool, PERMISSIONS_CTA
+ ),
+ ErrorKind::PnpmVersionNotFound { matching } => write!(
+ f,
+ r#"Could not find pnpm version matching "{}" in the version registry.
+
+Please verify that the version is correct."#,
+ matching
+ ),
+ ErrorKind::ProjectLocalBinaryExecError { command } => write!(
+ f,
+ "Could not execute `{}`
+
+Please ensure you have correct permissions to access the file.",
+ command
+ ),
+ ErrorKind::ProjectLocalBinaryNotFound { command } => write!(
+ f,
+ "Could not locate executable `{}` in your project.
+
+Please ensure that all project dependencies are installed with `npm install` or `yarn install`",
+ command
+ ),
+ ErrorKind::PublishHookBothUrlAndBin => write!(
+ f,
+ "Publish hook configuration includes both hook types.
+
+Please include only one of 'bin' or 'url'"
+ ),
+ ErrorKind::PublishHookNeitherUrlNorBin => write!(
+ f,
+ "Publish hook configuration includes no hook types.
+
+Please include one of 'bin' or 'url'"
+ ),
+ ErrorKind::ReadBinConfigDirError { dir } => write!(
+ f,
+ "Could not read executable metadata directory
+at {}
+
+{}",
+ dir.display(),
+ PERMISSIONS_CTA
+ ),
+ ErrorKind::ReadBinConfigError { file } => write!(
+ f,
+ "Could not read executable configuration
+from {}
+
+{}",
+ file.display(),
+ PERMISSIONS_CTA
+ ),
+ ErrorKind::ReadDefaultNpmError { file } => write!(
+ f,
+ "Could not read default npm version
+from {}
+
+{}",
+ file.display(),
+ PERMISSIONS_CTA
+ ),
+ ErrorKind::ReadDirError { dir } => write!(
+ f,
+ "Could not read contents from directory {}
+
+{}",
+ dir.display(), PERMISSIONS_CTA
+ ),
+ ErrorKind::ReadHooksError { file } => write!(
+ f,
+ "Could not read hooks file
+from {}
+
+{}",
+ file.display(),
+ PERMISSIONS_CTA
+ ),
+ ErrorKind::ReadNodeIndexCacheError { file } => write!(
+ f,
+ "Could not read Node index cache
+from {}
+
+{}",
+ file.display(),
+ PERMISSIONS_CTA
+ ),
+ ErrorKind::ReadNodeIndexExpiryError { file } => write!(
+ f,
+ "Could not read Node index cache expiration
+from {}
+
+{}",
+ file.display(),
+ PERMISSIONS_CTA
+ ),
+ ErrorKind::ReadNpmManifestError => write!(
+ f,
+ "Could not read package.json file for bundled npm.
+
+Please ensure the version of Node is correct."
+ ),
+ ErrorKind::ReadPackageConfigError { file } => write!(
+ f,
+ "Could not read package configuration file
+from {}
+
+{}",
+ file.display(),
+ PERMISSIONS_CTA
+ ),
+ ErrorKind::ReadPlatformError { file } => write!(
+ f,
+ "Could not read default platform file
+from {}
+
+{}",
+ file.display(),
+ PERMISSIONS_CTA
+ ),
+ #[cfg(windows)]
+ ErrorKind::ReadUserPathError => write!(
+ f,
+ "Could not read user Path environment variable.
+
+Please ensure you have access to the your environment variables."
+ ),
+ ErrorKind::RegistryFetchError { tool, from_url } => write!(
+ f,
+ "Could not download {} version registry
+from {}
+
+Please verify your internet connection.",
+ tool, from_url
+ ),
+ ErrorKind::RunShimDirectly => write!(
+ f,
+ "'volta-shim' should not be called directly.
+
+Please use the existing shims provided by Volta (node, yarn, etc.) to run tools."
+ ),
+ ErrorKind::SetToolExecutable { tool } => write!(
+ f,
+ r#"Could not set "{}" to executable
+
+{}"#,
+ tool, PERMISSIONS_CTA
+ ),
+ ErrorKind::SetupToolImageError { tool, version, dir } => write!(
+ f,
+ "Could not create environment for {} v{}
+at {}
+
+{}",
+ tool,
+ version,
+ dir.display(),
+ PERMISSIONS_CTA
+ ),
+ ErrorKind::ShimCreateError { name } => write!(
+ f,
+ r#"Could not create shim for "{}"
+
+{}"#,
+ name, PERMISSIONS_CTA
+ ),
+ ErrorKind::ShimRemoveError { name } => write!(
+ f,
+ r#"Could not remove shim for "{}"
+
+{}"#,
+ name, PERMISSIONS_CTA
+ ),
+ ErrorKind::StringifyBinConfigError => write!(
+ f,
+ "Could not serialize executable configuration.
+
+{}",
+ REPORT_BUG_CTA
+ ),
+ ErrorKind::StringifyPackageConfigError => write!(
+ f,
+ "Could not serialize package configuration.
+
+{}",
+ REPORT_BUG_CTA
+ ),
+ ErrorKind::StringifyPlatformError => write!(
+ f,
+ "Could not serialize platform settings.
+
+{}",
+ REPORT_BUG_CTA
+ ),
+ ErrorKind::Unimplemented { feature } => {
+ write!(f, "{} is not supported yet.", feature)
+ }
+ ErrorKind::UnpackArchiveError { tool, version } => write!(
+ f,
+ "Could not unpack {} v{}
+
+Please ensure the correct version is specified.",
+ tool, version
+ ),
+ ErrorKind::UpgradePackageNotFound { package, manager } => write!(
+ f,
+ r#"Could not locate the package '{}' to upgrade.
+
+Please ensure it is installed with `{} {0}`"#,
+ package,
+ match manager {
+ PackageManager::Npm => "npm i -g",
+ PackageManager::Pnpm => "pnpm add -g",
+ PackageManager::Yarn => "yarn global add",
+ }
+ ),
+ ErrorKind::UpgradePackageWrongManager { package, manager } => {
+ let (name, command) = match manager {
+ PackageManager::Npm => ("npm", "npm update -g"),
+ PackageManager::Pnpm => ("pnpm", "pnpm update -g"),
+ PackageManager::Yarn => ("Yarn", "yarn global upgrade"),
+ };
+ write!(
+ f,
+ r#"The package '{}' was installed using {}.
+
+To upgrade it, please use the command `{} {0}`"#,
+ package, name, command
+ )
+ }
+ ErrorKind::VersionParseError { version } => write!(
+ f,
+ r#"Could not parse version "{}"
+
+Please verify the intended version."#,
+ version
+ ),
+ ErrorKind::WriteBinConfigError { file } => write!(
+ f,
+ "Could not write executable configuration
+to {}
+
+{}",
+ file.display(),
+ PERMISSIONS_CTA
+ ),
+ ErrorKind::WriteDefaultNpmError { file } => write!(
+ f,
+ "Could not write bundled npm version
+to {}
+
+{}",
+ file.display(),
+ PERMISSIONS_CTA
+ ),
+ ErrorKind::WriteLauncherError { tool } => write!(
+ f,
+ "Could not set up launcher for {}
+
+This is most likely an intermittent failure, please try again.",
+ tool
+ ),
+ ErrorKind::WriteNodeIndexCacheError { file } => write!(
+ f,
+ "Could not write Node index cache
+to {}
+
+{}",
+ file.display(),
+ PERMISSIONS_CTA
+ ),
+ ErrorKind::WriteNodeIndexExpiryError { file } => write!(
+ f,
+ "Could not write Node index cache expiration
+to {}
+
+{}",
+ file.display(),
+ PERMISSIONS_CTA
+ ),
+ ErrorKind::WritePackageConfigError { file } => write!(
+ f,
+ "Could not write package configuration
+to {}
+
+{}",
+ file.display(),
+ PERMISSIONS_CTA
+ ),
+ ErrorKind::WritePlatformError { file } => write!(
+ f,
+ "Could not save platform settings
+to {}
+
+{}",
+ file.display(),
+ PERMISSIONS_CTA
+ ),
+ #[cfg(windows)]
+ ErrorKind::WriteUserPathError => write!(
+ f,
+ "Could not write Path environment variable.
+
+Please ensure you have permissions to edit your environment variables."
+ ),
+ ErrorKind::Yarn2NotSupported => write!(
+ f,
+ "Yarn version 2 is not recommended for use, and not supported by Volta.
+
+Please use version 3 or greater instead."
+ ),
+ ErrorKind::YarnLatestFetchError { from_url } => write!(
+ f,
+ "Could not fetch latest version of Yarn
+from {}
+
+Please verify your internet connection.",
+ from_url
+ ),
+ ErrorKind::YarnVersionNotFound { matching } => write!(
+ f,
+ r#"Could not find Yarn version matching "{}" in the version registry.
+
+Please verify that the version is correct."#,
+ matching
+ ),
+ }
+ }
+}
+
+impl ErrorKind {
+ pub fn exit_code(&self) -> ExitCode {
+ match self {
+ ErrorKind::BinaryAlreadyInstalled { .. } => ExitCode::FileSystemError,
+ ErrorKind::BinaryExecError => ExitCode::ExecutionFailure,
+ ErrorKind::BinaryNotFound { .. } => ExitCode::ExecutableNotFound,
+ ErrorKind::BuildPathError => ExitCode::EnvironmentError,
+ ErrorKind::BypassError { .. } => ExitCode::ExecutionFailure,
+ ErrorKind::CannotFetchPackage { .. } => ExitCode::InvalidArguments,
+ ErrorKind::CannotPinPackage { .. } => ExitCode::InvalidArguments,
+ ErrorKind::CompletionsOutFileError { .. } => ExitCode::InvalidArguments,
+ ErrorKind::ContainingDirError { .. } => ExitCode::FileSystemError,
+ ErrorKind::CouldNotDetermineTool => ExitCode::UnknownError,
+ ErrorKind::CouldNotStartMigration => ExitCode::EnvironmentError,
+ ErrorKind::CreateDirError { .. } => ExitCode::FileSystemError,
+ ErrorKind::CreateLayoutFileError { .. } => ExitCode::FileSystemError,
+ ErrorKind::CreateSharedLinkError { .. } => ExitCode::FileSystemError,
+ ErrorKind::CreateTempDirError { .. } => ExitCode::FileSystemError,
+ ErrorKind::CreateTempFileError { .. } => ExitCode::FileSystemError,
+ ErrorKind::CurrentDirError => ExitCode::EnvironmentError,
+ ErrorKind::DeleteDirectoryError { .. } => ExitCode::FileSystemError,
+ ErrorKind::DeleteFileError { .. } => ExitCode::FileSystemError,
+ ErrorKind::DeprecatedCommandError { .. } => ExitCode::InvalidArguments,
+ ErrorKind::DownloadToolNetworkError { .. } => ExitCode::NetworkError,
+ ErrorKind::ExecuteHookError { .. } => ExitCode::ExecutionFailure,
+ ErrorKind::ExtensionCycleError { .. } => ExitCode::ConfigurationError,
+ ErrorKind::ExtensionPathError { .. } => ExitCode::FileSystemError,
+ ErrorKind::HookCommandFailed { .. } => ExitCode::ConfigurationError,
+ ErrorKind::HookMultipleFieldsSpecified => ExitCode::ConfigurationError,
+ ErrorKind::HookNoFieldsSpecified => ExitCode::ConfigurationError,
+ ErrorKind::HookPathError { .. } => ExitCode::ConfigurationError,
+ ErrorKind::InstalledPackageNameError => ExitCode::UnknownError,
+ ErrorKind::InvalidHookCommand { .. } => ExitCode::ExecutableNotFound,
+ ErrorKind::InvalidHookOutput { .. } => ExitCode::ExecutionFailure,
+ ErrorKind::InvalidInvocation { .. } => ExitCode::InvalidArguments,
+ ErrorKind::InvalidInvocationOfBareVersion { .. } => ExitCode::InvalidArguments,
+ ErrorKind::InvalidRegistryFormat { .. } => ExitCode::ConfigurationError,
+ ErrorKind::InvalidToolName { .. } => ExitCode::InvalidArguments,
+ ErrorKind::LockAcquireError => ExitCode::FileSystemError,
+ ErrorKind::NoBundledNpm { .. } => ExitCode::ConfigurationError,
+ ErrorKind::NoCommandLinePnpm => ExitCode::ConfigurationError,
+ ErrorKind::NoCommandLineYarn => ExitCode::ConfigurationError,
+ ErrorKind::NoDefaultNodeVersion { .. } => ExitCode::ConfigurationError,
+ ErrorKind::NodeVersionNotFound { .. } => ExitCode::NoVersionMatch,
+ ErrorKind::NoHomeEnvironmentVar => ExitCode::EnvironmentError,
+ ErrorKind::NoInstallDir => ExitCode::EnvironmentError,
+ ErrorKind::NoLocalDataDir => ExitCode::EnvironmentError,
+ ErrorKind::NoPinnedNodeVersion { .. } => ExitCode::ConfigurationError,
+ ErrorKind::NoPlatform => ExitCode::ConfigurationError,
+ ErrorKind::NoProjectNodeInManifest => ExitCode::ConfigurationError,
+ ErrorKind::NoProjectPnpm => ExitCode::ConfigurationError,
+ ErrorKind::NoProjectYarn => ExitCode::ConfigurationError,
+ ErrorKind::NoShellProfile { .. } => ExitCode::EnvironmentError,
+ ErrorKind::NotInPackage => ExitCode::ConfigurationError,
+ ErrorKind::NoDefaultPnpm => ExitCode::ConfigurationError,
+ ErrorKind::NoDefaultYarn => ExitCode::ConfigurationError,
+ ErrorKind::NpmLinkMissingPackage { .. } => ExitCode::ConfigurationError,
+ ErrorKind::NpmLinkWrongManager { .. } => ExitCode::ConfigurationError,
+ ErrorKind::NpmVersionNotFound { .. } => ExitCode::NoVersionMatch,
+ ErrorKind::NpxNotAvailable { .. } => ExitCode::ExecutableNotFound,
+ ErrorKind::PackageInstallFailed { .. } => ExitCode::UnknownError,
+ ErrorKind::PackageManifestParseError { .. } => ExitCode::ConfigurationError,
+ ErrorKind::PackageManifestReadError { .. } => ExitCode::FileSystemError,
+ ErrorKind::PackageNotFound { .. } => ExitCode::InvalidArguments,
+ ErrorKind::PackageParseError { .. } => ExitCode::ConfigurationError,
+ ErrorKind::PackageReadError { .. } => ExitCode::FileSystemError,
+ ErrorKind::PackageUnpackError => ExitCode::ConfigurationError,
+ ErrorKind::PackageWriteError { .. } => ExitCode::FileSystemError,
+ ErrorKind::ParseBinConfigError => ExitCode::UnknownError,
+ ErrorKind::ParseHooksError { .. } => ExitCode::ConfigurationError,
+ ErrorKind::ParseToolSpecError { .. } => ExitCode::InvalidArguments,
+ ErrorKind::ParseNodeIndexCacheError => ExitCode::UnknownError,
+ ErrorKind::ParseNodeIndexError { .. } => ExitCode::NetworkError,
+ ErrorKind::ParseNodeIndexExpiryError => ExitCode::UnknownError,
+ ErrorKind::ParseNpmManifestError => ExitCode::UnknownError,
+ ErrorKind::ParsePackageConfigError => ExitCode::UnknownError,
+ ErrorKind::ParsePlatformError => ExitCode::ConfigurationError,
+ ErrorKind::PersistInventoryError { .. } => ExitCode::FileSystemError,
+ ErrorKind::PnpmVersionNotFound { .. } => ExitCode::NoVersionMatch,
+ ErrorKind::ProjectLocalBinaryExecError { .. } => ExitCode::ExecutionFailure,
+ ErrorKind::ProjectLocalBinaryNotFound { .. } => ExitCode::FileSystemError,
+ ErrorKind::PublishHookBothUrlAndBin => ExitCode::ConfigurationError,
+ ErrorKind::PublishHookNeitherUrlNorBin => ExitCode::ConfigurationError,
+ ErrorKind::ReadBinConfigDirError { .. } => ExitCode::FileSystemError,
+ ErrorKind::ReadBinConfigError { .. } => ExitCode::FileSystemError,
+ ErrorKind::ReadDefaultNpmError { .. } => ExitCode::FileSystemError,
+ ErrorKind::ReadDirError { .. } => ExitCode::FileSystemError,
+ ErrorKind::ReadHooksError { .. } => ExitCode::FileSystemError,
+ ErrorKind::ReadNodeIndexCacheError { .. } => ExitCode::FileSystemError,
+ ErrorKind::ReadNodeIndexExpiryError { .. } => ExitCode::FileSystemError,
+ ErrorKind::ReadNpmManifestError => ExitCode::UnknownError,
+ ErrorKind::ReadPackageConfigError { .. } => ExitCode::FileSystemError,
+ ErrorKind::ReadPlatformError { .. } => ExitCode::FileSystemError,
+ #[cfg(windows)]
+ ErrorKind::ReadUserPathError => ExitCode::EnvironmentError,
+ ErrorKind::RegistryFetchError { .. } => ExitCode::NetworkError,
+ ErrorKind::RunShimDirectly => ExitCode::InvalidArguments,
+ ErrorKind::SetupToolImageError { .. } => ExitCode::FileSystemError,
+ ErrorKind::SetToolExecutable { .. } => ExitCode::FileSystemError,
+ ErrorKind::ShimCreateError { .. } => ExitCode::FileSystemError,
+ ErrorKind::ShimRemoveError { .. } => ExitCode::FileSystemError,
+ ErrorKind::StringifyBinConfigError => ExitCode::UnknownError,
+ ErrorKind::StringifyPackageConfigError => ExitCode::UnknownError,
+ ErrorKind::StringifyPlatformError => ExitCode::UnknownError,
+ ErrorKind::Unimplemented { .. } => ExitCode::UnknownError,
+ ErrorKind::UnpackArchiveError { .. } => ExitCode::UnknownError,
+ ErrorKind::UpgradePackageNotFound { .. } => ExitCode::ConfigurationError,
+ ErrorKind::UpgradePackageWrongManager { .. } => ExitCode::ConfigurationError,
+ ErrorKind::VersionParseError { .. } => ExitCode::NoVersionMatch,
+ ErrorKind::WriteBinConfigError { .. } => ExitCode::FileSystemError,
+ ErrorKind::WriteDefaultNpmError { .. } => ExitCode::FileSystemError,
+ ErrorKind::WriteLauncherError { .. } => ExitCode::FileSystemError,
+ ErrorKind::WriteNodeIndexCacheError { .. } => ExitCode::FileSystemError,
+ ErrorKind::WriteNodeIndexExpiryError { .. } => ExitCode::FileSystemError,
+ ErrorKind::WritePackageConfigError { .. } => ExitCode::FileSystemError,
+ ErrorKind::WritePlatformError { .. } => ExitCode::FileSystemError,
+ #[cfg(windows)]
+ ErrorKind::WriteUserPathError => ExitCode::EnvironmentError,
+ ErrorKind::Yarn2NotSupported => ExitCode::NoVersionMatch,
+ ErrorKind::YarnLatestFetchError { .. } => ExitCode::NetworkError,
+ ErrorKind::YarnVersionNotFound { .. } => ExitCode::NoVersionMatch,
+ }
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +
use std::error::Error;
+use std::fmt;
+use std::process::exit;
+
+mod kind;
+mod reporter;
+
+pub use kind::ErrorKind;
+pub use reporter::report_error;
+
+pub type Fallible<T> = Result<T, VoltaError>;
+
+/// Error type for Volta
+#[derive(Debug)]
+pub struct VoltaError {
+ inner: Box<Inner>,
+}
+
+#[derive(Debug)]
+struct Inner {
+ kind: ErrorKind,
+ source: Option<Box<dyn Error>>,
+}
+
+impl VoltaError {
+ /// The exit code Volta should use when this error stops execution
+ pub fn exit_code(&self) -> ExitCode {
+ self.inner.kind.exit_code()
+ }
+
+ /// Create a new VoltaError instance including a source error
+ pub fn from_source<E>(source: E, kind: ErrorKind) -> Self
+ where
+ E: Into<Box<dyn Error>>,
+ {
+ VoltaError {
+ inner: Box::new(Inner {
+ kind,
+ source: Some(source.into()),
+ }),
+ }
+ }
+
+ /// Get a reference to the ErrorKind for this error
+ pub fn kind(&self) -> &ErrorKind {
+ &self.inner.kind
+ }
+}
+
+impl fmt::Display for VoltaError {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ self.inner.kind.fmt(f)
+ }
+}
+
+impl Error for VoltaError {
+ fn source(&self) -> Option<&(dyn Error + 'static)> {
+ self.inner.source.as_ref().map(|b| b.as_ref())
+ }
+}
+
+impl From<ErrorKind> for VoltaError {
+ fn from(kind: ErrorKind) -> Self {
+ VoltaError {
+ inner: Box::new(Inner { kind, source: None }),
+ }
+ }
+}
+
+/// Trait providing the with_context method to easily convert any Result error into a VoltaError
+pub trait Context<T> {
+ fn with_context<F>(self, f: F) -> Fallible<T>
+ where
+ F: FnOnce() -> ErrorKind;
+}
+
+impl<T, E> Context<T> for Result<T, E>
+where
+ E: Error + 'static,
+{
+ fn with_context<F>(self, f: F) -> Fallible<T>
+ where
+ F: FnOnce() -> ErrorKind,
+ {
+ self.map_err(|e| VoltaError::from_source(e, f()))
+ }
+}
+
+/// Exit codes supported by Volta Errors
+#[derive(Copy, Clone, Debug)]
+pub enum ExitCode {
+ /// No error occurred.
+ Success = 0,
+
+ /// An unknown error occurred.
+ UnknownError = 1,
+
+ /// An invalid combination of command-line arguments was supplied.
+ InvalidArguments = 3,
+
+ /// No match could be found for the requested version string.
+ NoVersionMatch = 4,
+
+ /// A network error occurred.
+ NetworkError = 5,
+
+ /// A required environment variable was unset or invalid.
+ EnvironmentError = 6,
+
+ /// A file could not be read or written.
+ FileSystemError = 7,
+
+ /// Package configuration is missing or incorrect.
+ ConfigurationError = 8,
+
+ /// The command or feature is not yet implemented.
+ NotYetImplemented = 9,
+
+ /// The requested executable could not be run.
+ ExecutionFailure = 126,
+
+ /// The requested executable is not available.
+ ExecutableNotFound = 127,
+}
+
+impl ExitCode {
+ pub fn exit(self) -> ! {
+ exit(self as i32);
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +
use std::env::args_os;
+use std::error::Error;
+use std::fs::File;
+use std::io::Write;
+use std::path::PathBuf;
+
+use super::VoltaError;
+use crate::layout::volta_home;
+use crate::style::format_error_cause;
+use chrono::Local;
+use ci_info::is_ci;
+use console::strip_ansi_codes;
+use fs_utils::ensure_containing_dir_exists;
+use log::{debug, error};
+
+/// Report an error, both to the console and to error logs
+pub fn report_error(volta_version: &str, err: &VoltaError) {
+ let message = err.to_string();
+ error!("{}", message);
+
+ if let Some(details) = compose_error_details(err) {
+ if is_ci() {
+ // In CI, we write the error details to the log so that they are available in the CI logs
+ // A log file may not even exist by the time the user is reviewing a failure
+ error!("{}", details);
+ } else {
+ // Outside of CI, we write the error details as Debug (Verbose) information
+ // And we write an actual error log that the user can review
+ debug!("{}", details);
+
+ // Note: Writing the error log info directly to stderr as it is a message for the user
+ // Any custom logs will have all of the details already, so showing a message about writing
+ // the error log would be redundant
+ match write_error_log(volta_version, message, details) {
+ Ok(log_file) => {
+ eprintln!("Error details written to {}", log_file.to_string_lossy());
+ }
+ Err(_) => {
+ eprintln!("Unable to write error log!");
+ }
+ }
+ }
+ }
+}
+
+/// Write an error log with all details about the error
+fn write_error_log(
+ volta_version: &str,
+ message: String,
+ details: String,
+) -> Result<PathBuf, Box<dyn Error>> {
+ let file_name = Local::now()
+ .format("volta-error-%Y-%m-%d_%H_%M_%S%.3f.log")
+ .to_string();
+ let log_file_path = volta_home()?.log_dir().join(file_name);
+
+ ensure_containing_dir_exists(&log_file_path)?;
+ let mut log_file = File::create(&log_file_path)?;
+
+ writeln!(log_file, "{}", collect_arguments())?;
+ writeln!(log_file, "Volta v{}", volta_version)?;
+ writeln!(log_file)?;
+ writeln!(log_file, "{}", strip_ansi_codes(&message))?;
+ writeln!(log_file)?;
+ writeln!(log_file, "{}", strip_ansi_codes(&details))?;
+
+ Ok(log_file_path)
+}
+
+fn compose_error_details(err: &VoltaError) -> Option<String> {
+ // Only compose details if there is an underlying cause for the error
+ let mut current = err.source()?;
+ let mut details = String::new();
+
+ // Walk up the tree of causes and include all of them
+ loop {
+ details.push_str(&format_error_cause(current));
+
+ match current.source() {
+ Some(cause) => {
+ details.push_str("\n\n");
+ current = cause;
+ }
+ None => {
+ break;
+ }
+ };
+ }
+
+ Some(details)
+}
+
+/// Combines all the arguments into a single String
+fn collect_arguments() -> String {
+ // The Debug formatter for OsString properly quotes and escapes each value
+ args_os()
+ .map(|arg| format!("{:?}", arg))
+ .collect::<Vec<String>>()
+ .join(" ")
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +
//! Events for the sessions in executables and shims and everything
+
+use std::env;
+use std::time::{SystemTime, UNIX_EPOCH};
+
+use serde::{Deserialize, Serialize};
+
+use crate::error::{ExitCode, VoltaError};
+use crate::hook::Publish;
+use crate::monitor::send_events;
+use crate::session::ActivityKind;
+
+// the Event data that is serialized to JSON and sent the plugin
+#[derive(Deserialize, Serialize)]
+pub struct Event {
+ timestamp: u64,
+ pub name: String,
+ pub event: EventKind,
+}
+
+#[derive(Deserialize, Serialize, PartialEq, Eq, Debug)]
+pub struct ErrorEnv {
+ argv: String,
+ exec_path: String,
+ path: String,
+ platform: String,
+ platform_version: String,
+}
+
+#[derive(Deserialize, Serialize, PartialEq, Eq, Debug)]
+#[serde(rename_all = "lowercase")]
+pub enum EventKind {
+ Start,
+ End {
+ exit_code: i32,
+ },
+ Error {
+ exit_code: i32,
+ error: String,
+ env: ErrorEnv,
+ },
+ ToolEnd {
+ exit_code: i32,
+ },
+ Args {
+ argv: String,
+ },
+}
+
+impl EventKind {
+ pub fn into_event(self, activity_kind: ActivityKind) -> Event {
+ Event {
+ timestamp: unix_timestamp(),
+ name: activity_kind.to_string(),
+ event: self,
+ }
+ }
+}
+
+// returns the current number of milliseconds since the epoch
+fn unix_timestamp() -> u64 {
+ let start = SystemTime::now();
+ let duration = start
+ .duration_since(UNIX_EPOCH)
+ .expect("Time went backwards");
+ let nanosecs_since_epoch = duration.as_secs() * 1_000_000_000 + duration.subsec_nanos() as u64;
+ nanosecs_since_epoch / 1_000_000
+}
+
+fn get_error_env() -> ErrorEnv {
+ let path = match env::var("PATH") {
+ Ok(p) => p,
+ Err(_e) => "error: Unable to get path from environment".to_string(),
+ };
+ let argv = env::args().collect::<Vec<String>>().join(" ");
+ let exec_path = match env::current_exe() {
+ Ok(ep) => ep.display().to_string(),
+ Err(_e) => "error: Unable to get executable path from environment".to_string(),
+ };
+
+ let info = os_info::get();
+ let platform = info.os_type().to_string();
+ let platform_version = info.version().to_string();
+
+ ErrorEnv {
+ argv,
+ exec_path,
+ path,
+ platform,
+ platform_version,
+ }
+}
+
+pub struct EventLog {
+ events: Vec<Event>,
+}
+
+impl EventLog {
+ /// Constructs a new 'EventLog'
+ pub fn init() -> Self {
+ EventLog { events: Vec::new() }
+ }
+
+ pub fn add_event_start(&mut self, activity_kind: ActivityKind) {
+ self.add_event(EventKind::Start, activity_kind)
+ }
+ pub fn add_event_end(&mut self, activity_kind: ActivityKind, exit_code: ExitCode) {
+ self.add_event(
+ EventKind::End {
+ exit_code: exit_code as i32,
+ },
+ activity_kind,
+ )
+ }
+ pub fn add_event_tool_end(&mut self, activity_kind: ActivityKind, exit_code: i32) {
+ self.add_event(EventKind::ToolEnd { exit_code }, activity_kind)
+ }
+ pub fn add_event_error(&mut self, activity_kind: ActivityKind, error: &VoltaError) {
+ self.add_event(
+ EventKind::Error {
+ exit_code: error.exit_code() as i32,
+ error: error.to_string(),
+ env: get_error_env(),
+ },
+ activity_kind,
+ )
+ }
+ pub fn add_event_args(&mut self) {
+ let argv = env::args_os()
+ .enumerate()
+ .fold(String::new(), |mut result, (i, arg)| {
+ if i > 0 {
+ result.push(' ');
+ }
+ result.push_str(&arg.to_string_lossy());
+ result
+ });
+ self.add_event(EventKind::Args { argv }, ActivityKind::Args)
+ }
+
+ fn add_event(&mut self, event_kind: EventKind, activity_kind: ActivityKind) {
+ let event = event_kind.into_event(activity_kind);
+ self.events.push(event);
+ }
+
+ pub fn publish(&self, plugin: Option<&Publish>) {
+ match plugin {
+ // Note: This call to unimplemented is left in, as it's not a Fallible operation that can use ErrorKind::Unimplemented
+ Some(Publish::Url(_)) => unimplemented!(),
+ Some(Publish::Bin(command)) => {
+ send_events(command, &self.events);
+ }
+ None => {}
+ }
+ }
+}
+
+#[cfg(test)]
+pub mod tests {
+
+ use super::{EventKind, EventLog};
+ use crate::error::{ErrorKind, ExitCode};
+ use crate::session::ActivityKind;
+ use regex::Regex;
+
+ #[test]
+ fn test_adding_events() {
+ let mut event_log = EventLog::init();
+ assert_eq!(event_log.events.len(), 0);
+
+ event_log.add_event_start(ActivityKind::Current);
+ assert_eq!(event_log.events.len(), 1);
+ assert_eq!(event_log.events[0].name, "current");
+ assert_eq!(event_log.events[0].event, EventKind::Start);
+
+ event_log.add_event_end(ActivityKind::Pin, ExitCode::NetworkError);
+ assert_eq!(event_log.events.len(), 2);
+ assert_eq!(event_log.events[1].name, "pin");
+ assert_eq!(event_log.events[1].event, EventKind::End { exit_code: 5 });
+
+ event_log.add_event_tool_end(ActivityKind::Version, 12);
+ assert_eq!(event_log.events.len(), 3);
+ assert_eq!(event_log.events[2].name, "version");
+ assert_eq!(
+ event_log.events[2].event,
+ EventKind::ToolEnd { exit_code: 12 }
+ );
+
+ let error = ErrorKind::BinaryExecError.into();
+ event_log.add_event_error(ActivityKind::Install, &error);
+ assert_eq!(event_log.events.len(), 4);
+ assert_eq!(event_log.events[3].name, "install");
+ // not checking the error because it has too much machine-specific info
+
+ event_log.add_event_args();
+ assert_eq!(event_log.events.len(), 5);
+ assert_eq!(event_log.events[4].name, "args");
+ match event_log.events[4].event {
+ EventKind::Args { ref argv } => {
+ let re = Regex::new("volta_core").unwrap();
+ assert!(re.is_match(argv));
+ }
+ _ => {
+ panic!(
+ "Expected EventKind::Args {{ argv }}, Got: {:?}",
+ event_log.events[4].event
+ );
+ }
+ }
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +
//! Provides utilities for operating on the filesystem.
+
+use std::fs::{self, create_dir_all, read_dir, DirEntry, File, Metadata};
+use std::io;
+#[cfg(unix)]
+use std::os::unix::fs::PermissionsExt;
+use std::path::Path;
+
+use crate::error::{Context, ErrorKind, Fallible};
+use crate::layout::volta_home;
+use retry::delay::Fibonacci;
+use retry::{retry, OperationResult};
+use tempfile::{tempdir_in, NamedTempFile, TempDir};
+
+/// Opens a file, creating it if it doesn't exist
+pub fn touch(path: &Path) -> io::Result<File> {
+ if !path.is_file() {
+ if let Some(basedir) = path.parent() {
+ create_dir_all(basedir)?;
+ }
+ File::create(path)?;
+ }
+ File::open(path)
+}
+
+/// Removes the target directory, if it exists. If the directory doesn't exist, that is treated as
+/// success.
+pub fn remove_dir_if_exists<P: AsRef<Path>>(path: P) -> Fallible<()> {
+ fs::remove_dir_all(&path)
+ .or_else(ok_if_not_found)
+ .with_context(|| ErrorKind::DeleteDirectoryError {
+ directory: path.as_ref().to_owned(),
+ })
+}
+
+/// Removes the target file, if it exists. If the file doesn't exist, that is treated as success.
+pub fn remove_file_if_exists<P: AsRef<Path>>(path: P) -> Fallible<()> {
+ fs::remove_file(&path)
+ .or_else(ok_if_not_found)
+ .with_context(|| ErrorKind::DeleteFileError {
+ file: path.as_ref().to_owned(),
+ })
+}
+
+/// Converts a failure because of file not found into a success.
+///
+/// Handling the error is preferred over checking if a file exists before removing it, since
+/// that avoids a potential race condition between the check and the removal.
+pub fn ok_if_not_found<T: Default>(err: io::Error) -> io::Result<T> {
+ match err.kind() {
+ io::ErrorKind::NotFound => Ok(T::default()),
+ _ => Err(err),
+ }
+}
+
+/// Reads a file, if it exists.
+pub fn read_file<P: AsRef<Path>>(path: P) -> io::Result<Option<String>> {
+ let result: io::Result<String> = fs::read_to_string(path);
+
+ match result {
+ Ok(string) => Ok(Some(string)),
+ Err(error) => match error.kind() {
+ io::ErrorKind::NotFound => Ok(None),
+ _ => Err(error),
+ },
+ }
+}
+
+/// Reads the full contents of a directory, eagerly extracting each directory entry
+/// and its metadata and returning an iterator over them. Returns `Error` if any of
+/// these steps fails.
+///
+/// This function makes it easier to write high level logic for manipulating the
+/// contents of directories (map, filter, etc).
+///
+/// Note that this function allocates an intermediate vector of directory entries to
+/// construct the iterator from, so if a directory is expected to be very large, it
+/// will allocate temporary data proportional to the number of entries.
+pub fn read_dir_eager(dir: &Path) -> io::Result<impl Iterator<Item = (DirEntry, Metadata)>> {
+ let entries = read_dir(dir)?;
+ let vec = entries
+ .map(|entry| {
+ let entry = entry?;
+ let metadata = entry.metadata()?;
+ Ok((entry, metadata))
+ })
+ .collect::<io::Result<Vec<(DirEntry, Metadata)>>>()?;
+
+ Ok(vec.into_iter())
+}
+
+/// Reads the contents of a directory and returns a Vec of the matched results
+/// from the input function
+pub fn dir_entry_match<T, F>(dir: &Path, mut f: F) -> io::Result<Vec<T>>
+where
+ F: FnMut(&DirEntry) -> Option<T>,
+{
+ let entries = read_dir_eager(dir)?;
+ Ok(entries
+ .filter(|(_, metadata)| metadata.is_file())
+ .filter_map(|(entry, _)| f(&entry))
+ .collect::<Vec<T>>())
+}
+
+/// Creates a NamedTempFile in the Volta tmp directory
+pub fn create_staging_file() -> Fallible<NamedTempFile> {
+ let tmp_dir = volta_home()?.tmp_dir();
+ NamedTempFile::new_in(tmp_dir).with_context(|| ErrorKind::CreateTempFileError {
+ in_dir: tmp_dir.to_owned(),
+ })
+}
+
+/// Creates a staging directory in the Volta tmp directory
+pub fn create_staging_dir() -> Fallible<TempDir> {
+ let tmp_root = volta_home()?.tmp_dir();
+ tempdir_in(tmp_root).with_context(|| ErrorKind::CreateTempDirError {
+ in_dir: tmp_root.to_owned(),
+ })
+}
+
+/// Create a file symlink. The `dst` path will be a symbolic link pointing to the `src` path.
+pub fn symlink_file<S, D>(src: S, dest: D) -> io::Result<()>
+where
+ S: AsRef<Path>,
+ D: AsRef<Path>,
+{
+ #[cfg(windows)]
+ return std::os::windows::fs::symlink_file(src, dest);
+
+ #[cfg(unix)]
+ return std::os::unix::fs::symlink(src, dest);
+}
+
+/// Create a directory symlink. The `dst` path will be a symbolic link pointing to the `src` path
+pub fn symlink_dir<S, D>(src: S, dest: D) -> io::Result<()>
+where
+ S: AsRef<Path>,
+ D: AsRef<Path>,
+{
+ #[cfg(windows)]
+ return junction::create(src, dest);
+
+ #[cfg(unix)]
+ return std::os::unix::fs::symlink(src, dest);
+}
+
+/// Ensure that a given file has 'executable' permissions, otherwise we won't be able to call it
+#[cfg(unix)]
+pub fn set_executable(bin: &Path) -> io::Result<()> {
+ let mut permissions = fs::metadata(bin)?.permissions();
+ let mode = permissions.mode();
+
+ if mode & 0o111 != 0o111 {
+ permissions.set_mode(mode | 0o111);
+ fs::set_permissions(bin, permissions)
+ } else {
+ Ok(())
+ }
+}
+
+/// Ensure that a given file has 'executable' permissions, otherwise we won't be able to call it
+///
+/// Note: This is a no-op on Windows, which has no concept of 'executable' permissions
+#[cfg(windows)]
+pub fn set_executable(_bin: &Path) -> io::Result<()> {
+ Ok(())
+}
+
+/// Rename a file or directory to a new name, retrying if the operation fails because of permissions
+///
+/// Will retry for ~30 seconds with longer and longer delays between each, to allow for virus scan
+/// and other automated operations to complete.
+pub fn rename<F, T>(from: F, to: T) -> io::Result<()>
+where
+ F: AsRef<Path>,
+ T: AsRef<Path>,
+{
+ // 21 Fibonacci steps starting at 1 ms is ~28 seconds total
+ // See https://github.com/rust-lang/rustup/pull/1873 where this was used by Rustup to work around
+ // virus scanning file locks
+ let from = from.as_ref();
+ let to = to.as_ref();
+
+ retry(Fibonacci::from_millis(1).take(21), || {
+ match fs::rename(from, to) {
+ Ok(_) => OperationResult::Ok(()),
+ Err(e) => match e.kind() {
+ io::ErrorKind::PermissionDenied => OperationResult::Retry(e),
+ _ => OperationResult::Err(e),
+ },
+ }
+ })
+ .map_err(|e| e.error)
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402 +403 +404 +405 +406 +407 +408 +409 +410 +411 +412 +413 +414 +415 +416 +417 +418 +419 +420 +421 +422 +423 +424 +425 +426 +427 +428 +429 +430 +431 +432 +433 +434 +435 +436 +437 +438 +439 +440 +441 +442 +443 +444 +445 +446 +447 +448 +449 +450 +451 +452 +453 +454 +455 +456 +457 +458 +459 +460 +461 +462 +463 +464 +465 +466 +467 +468 +469 +470 +471 +472 +473 +474 +475 +476 +477 +478 +479 +480 +481 +482 +483 +484 +485 +486 +487 +488 +489 +490 +491 +492 +493 +494 +495 +496 +497 +498 +499 +500 +501 +502 +503 +504 +505 +506 +507 +508 +509 +510 +511 +512 +513 +514 +515 +516 +517 +518 +519 +520 +521 +522 +523 +524 +525 +526 +527 +528 +529 +530 +531 +532 +533 +534 +535 +536 +537 +538 +539 +540 +541 +542 +543 +544 +545 +546 +547 +548 +549 +550 +551 +552 +553 +554 +555 +556 +557 +558 +559 +560 +561 +562 +563 +564 +565 +566 +567 +568 +569 +570 +571 +572 +573 +574 +575 +576 +577 +578 +579 +580 +581 +582 +583 +584 +585 +586 +587 +588 +589 +590 +591 +592 +593 +594 +595 +596 +597 +598 +599 +600 +601 +602 +603 +604 +605 +606 +607 +608 +609 +610 +611 +612 +613 +614 +615 +616 +617 +618 +619 +620 +621 +622 +623 +624 +625 +626 +627 +628 +629 +630 +631 +632 +633 +634 +635 +636 +637 +638 +639 +640 +641 +642 +643 +644 +645 +646 +647 +648 +649 +650 +651 +652 +653 +654 +655 +656 +657 +658 +659 +660 +661 +662 +663 +664 +665 +666 +667 +668 +669 +670 +671 +672 +673 +674 +675 +676 +677 +678 +679 +680 +681 +682 +683 +684 +685 +686 +687 +688 +689 +690 +691 +692 +693 +694 +695 +696 +697 +698 +699 +700 +701 +702 +703 +704 +705 +706 +707 +708 +709 +710 +711 +712 +713 +714 +715 +716 +717 +718 +719 +720 +721 +722 +723 +724 +725 +726 +727 +728 +729 +730 +731 +732 +733 +734 +735 +736 +737 +738 +739 +740 +741 +742 +743 +744 +745 +746 +747 +748 +749 +
//! Provides types for working with Volta hooks.
+
+use std::borrow::Cow;
+use std::fs::File;
+use std::iter::once;
+use std::marker::PhantomData;
+use std::path::Path;
+
+use crate::error::{Context, ErrorKind, Fallible};
+use crate::layout::volta_home;
+use crate::project::Project;
+use crate::tool::{Node, Npm, Pnpm, Tool};
+use log::debug;
+use once_cell::unsync::OnceCell;
+
+pub(crate) mod serial;
+pub mod tool;
+
+/// A hook for publishing Volta events.
+#[derive(PartialEq, Eq, Debug)]
+pub enum Publish {
+ /// Reports an event by sending a POST request to a URL.
+ Url(String),
+
+ /// Reports an event by forking a process and sending the event by IPC.
+ Bin(String),
+}
+
+/// Lazily loaded Volta hook configuration
+pub struct LazyHookConfig {
+ settings: OnceCell<HookConfig>,
+}
+
+impl LazyHookConfig {
+ /// Constructs a new `LazyHookConfig`
+ pub fn init() -> LazyHookConfig {
+ LazyHookConfig {
+ settings: OnceCell::new(),
+ }
+ }
+
+ /// Forces the loading of the hook configuration from both project-local and user-default hooks
+ pub fn get(&self, project: Option<&Project>) -> Fallible<&HookConfig> {
+ self.settings
+ .get_or_try_init(|| HookConfig::current(project))
+ }
+}
+
+/// Volta hook configuration
+pub struct HookConfig {
+ node: Option<ToolHooks<Node>>,
+ npm: Option<ToolHooks<Npm>>,
+ pnpm: Option<ToolHooks<Pnpm>>,
+ yarn: Option<YarnHooks>,
+ events: Option<EventHooks>,
+}
+
+/// Volta hooks for an individual tool
+pub struct ToolHooks<T: Tool> {
+ /// The hook for resolving the URL for a distro version
+ pub distro: Option<tool::DistroHook>,
+ /// The hook for resolving the URL for the latest version
+ pub latest: Option<tool::MetadataHook>,
+ /// The hook for resolving the Tool Index URL
+ pub index: Option<tool::MetadataHook>,
+
+ phantom: PhantomData<T>,
+}
+
+/// Volta hooks for Yarn
+pub struct YarnHooks {
+ /// The hook for resolving the URL for a distro version
+ pub distro: Option<tool::DistroHook>,
+ /// The hook for resolving the URL for the latest version
+ pub latest: Option<tool::MetadataHook>,
+ /// The hook for resolving the Tool Index URL
+ pub index: Option<tool::YarnIndexHook>,
+}
+
+impl<T: Tool> ToolHooks<T> {
+ /// Extends this ToolHooks with another, giving precendence to the current instance
+ fn merge(self, other: Self) -> Self {
+ Self {
+ distro: self.distro.or(other.distro),
+ latest: self.latest.or(other.latest),
+ index: self.index.or(other.index),
+ phantom: PhantomData,
+ }
+ }
+}
+
+impl YarnHooks {
+ /// Extends this YarnHooks with another, giving precendence to the current instance
+ fn merge(self, other: Self) -> Self {
+ Self {
+ distro: self.distro.or(other.distro),
+ latest: self.latest.or(other.latest),
+ index: self.index.or(other.index),
+ }
+ }
+}
+
+macro_rules! merge_hooks {
+ ($self:ident, $other:ident, $field:ident) => {
+ match ($self.$field, $other.$field) {
+ (Some(current), Some(other)) => Some(current.merge(other)),
+ (Some(single), None) | (None, Some(single)) => Some(single),
+ (None, None) => None,
+ }
+ };
+}
+
+impl HookConfig {
+ pub fn node(&self) -> Option<&ToolHooks<Node>> {
+ self.node.as_ref()
+ }
+
+ pub fn npm(&self) -> Option<&ToolHooks<Npm>> {
+ self.npm.as_ref()
+ }
+
+ pub fn pnpm(&self) -> Option<&ToolHooks<Pnpm>> {
+ self.pnpm.as_ref()
+ }
+
+ pub fn yarn(&self) -> Option<&YarnHooks> {
+ self.yarn.as_ref()
+ }
+
+ pub fn events(&self) -> Option<&EventHooks> {
+ self.events.as_ref()
+ }
+
+ /// Returns the current hooks, which are a merge between the user hooks and
+ /// the project hooks (if any).
+ fn current(project: Option<&Project>) -> Fallible<Self> {
+ let default_hooks_file = volta_home()?.default_hooks_file();
+
+ // Since `from_paths` expects the paths to be sorted in descending precedence order, we
+ // include all project hooks first (workspace_roots is already sorted in descending
+ // precedence order)
+ // See the per-project configuration RFC for more details on the configuration precedence:
+ // https://github.com/volta-cli/rfcs/blob/main/text/0033-per-project-config.md#configuration-precedence
+ let paths = project
+ .into_iter()
+ .flat_map(Project::workspace_roots)
+ .map(|root| {
+ let mut path = root.join(".volta");
+ path.push("hooks.json");
+ Cow::Owned(path)
+ })
+ .chain(once(Cow::Borrowed(default_hooks_file)));
+
+ Self::from_paths(paths)
+ }
+
+ /// Returns the merged hooks loaded from an iterator of potential hook files
+ ///
+ /// `paths` should be sorted in order of descending precedence.
+ fn from_paths<P, I>(paths: I) -> Fallible<Self>
+ where
+ P: AsRef<Path>,
+ I: IntoIterator<Item = P>,
+ {
+ paths
+ .into_iter()
+ .try_fold(None, |acc: Option<Self>, hooks_file| {
+ // Try to load the hooks and merge with any already loaded hooks
+ match Self::from_file(hooks_file.as_ref())? {
+ Some(hooks) => {
+ debug!(
+ "Loaded custom hooks file: {}",
+ hooks_file.as_ref().display()
+ );
+ Ok(Some(match acc {
+ Some(loaded) => loaded.merge(hooks),
+ None => hooks,
+ }))
+ }
+ None => Ok(acc),
+ }
+ })
+ // If there were no hooks loaded at all, provide a default empty HookConfig
+ .map(|maybe_config| {
+ maybe_config.unwrap_or_else(|| {
+ debug!("No custom hooks found");
+ Self {
+ node: None,
+ npm: None,
+ pnpm: None,
+ yarn: None,
+ events: None,
+ }
+ })
+ })
+ }
+
+ fn from_file(file_path: &Path) -> Fallible<Option<Self>> {
+ if !file_path.is_file() {
+ return Ok(None);
+ }
+
+ let file = File::open(file_path).with_context(|| ErrorKind::ReadHooksError {
+ file: file_path.to_path_buf(),
+ })?;
+
+ let raw: serial::RawHookConfig =
+ serde_json::de::from_reader(file).with_context(|| ErrorKind::ParseHooksError {
+ file: file_path.to_path_buf(),
+ })?;
+
+ // Invariant: Since we successfully loaded it, we know we have a valid file path
+ let hooks_path = file_path.parent().expect("File paths always have a parent");
+
+ raw.into_hook_config(hooks_path).map(Some)
+ }
+
+ /// Merges this HookConfig with another, giving precedence to the current instance
+ fn merge(self, other: Self) -> Self {
+ Self {
+ node: merge_hooks!(self, other, node),
+ npm: merge_hooks!(self, other, npm),
+ pnpm: merge_hooks!(self, other, pnpm),
+ yarn: merge_hooks!(self, other, yarn),
+ events: merge_hooks!(self, other, events),
+ }
+ }
+}
+
+/// Format of the registry used for Yarn (Npm or Github)
+#[derive(PartialEq, Eq, Debug)]
+pub enum RegistryFormat {
+ Npm,
+ Github,
+}
+
+impl RegistryFormat {
+ pub fn from_str(raw_format: &str) -> Fallible<RegistryFormat> {
+ match raw_format {
+ "npm" => Ok(RegistryFormat::Npm),
+ "github" => Ok(RegistryFormat::Github),
+ other => Err(ErrorKind::InvalidRegistryFormat {
+ format: String::from(other),
+ }
+ .into()),
+ }
+ }
+}
+
+/// Volta hooks related to events.
+pub struct EventHooks {
+ /// The hook for publishing events, if any.
+ pub publish: Option<Publish>,
+}
+
+impl EventHooks {
+ /// Merges this EventHooks with another, giving precedence to the current instance
+ fn merge(self, other: Self) -> Self {
+ Self {
+ publish: self.publish.or(other.publish),
+ }
+ }
+}
+
+#[cfg(test)]
+pub mod tests {
+
+ use super::{tool, HookConfig, Publish, RegistryFormat};
+ use std::path::PathBuf;
+
+ fn fixture_path(fixture_dir: &str) -> PathBuf {
+ let mut cargo_manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
+ cargo_manifest_dir.push("fixtures");
+ cargo_manifest_dir.push(fixture_dir);
+ cargo_manifest_dir
+ }
+
+ #[test]
+ fn test_from_str_event_url() {
+ let fixture_dir = fixture_path("hooks");
+ let url_file = fixture_dir.join("event_url.json");
+ let hooks = HookConfig::from_file(&url_file).unwrap().unwrap();
+
+ assert_eq!(
+ hooks.events.unwrap().publish,
+ Some(Publish::Url("https://google.com".to_string()))
+ );
+ }
+
+ #[test]
+ fn test_from_str_bins() {
+ let fixture_dir = fixture_path("hooks");
+ let bin_file = fixture_dir.join("bins.json");
+ let hooks = HookConfig::from_file(&bin_file).unwrap().unwrap();
+ let node = hooks.node.unwrap();
+ let pnpm = hooks.pnpm.unwrap();
+ let yarn = hooks.yarn.unwrap();
+
+ assert_eq!(
+ node.distro,
+ Some(tool::DistroHook::Bin {
+ bin: "/some/bin/for/node/distro".to_string(),
+ base_path: fixture_dir.clone(),
+ })
+ );
+ assert_eq!(
+ node.latest,
+ Some(tool::MetadataHook::Bin {
+ bin: "/some/bin/for/node/latest".to_string(),
+ base_path: fixture_dir.clone(),
+ })
+ );
+ assert_eq!(
+ node.index,
+ Some(tool::MetadataHook::Bin {
+ bin: "/some/bin/for/node/index".to_string(),
+ base_path: fixture_dir.clone(),
+ })
+ );
+ // pnpm
+ assert_eq!(
+ pnpm.distro,
+ Some(tool::DistroHook::Bin {
+ bin: "/bin/to/pnpm/distro".to_string(),
+ base_path: fixture_dir.clone(),
+ })
+ );
+ assert_eq!(
+ pnpm.latest,
+ Some(tool::MetadataHook::Bin {
+ bin: "/bin/to/pnpm/latest".to_string(),
+ base_path: fixture_dir.clone(),
+ })
+ );
+ assert_eq!(
+ pnpm.index,
+ Some(tool::MetadataHook::Bin {
+ bin: "/bin/to/pnpm/index".to_string(),
+ base_path: fixture_dir.clone(),
+ })
+ );
+ // Yarn
+ assert_eq!(
+ yarn.distro,
+ Some(tool::DistroHook::Bin {
+ bin: "/bin/to/yarn/distro".to_string(),
+ base_path: fixture_dir.clone(),
+ })
+ );
+ assert_eq!(
+ yarn.latest,
+ Some(tool::MetadataHook::Bin {
+ bin: "/bin/to/yarn/latest".to_string(),
+ base_path: fixture_dir.clone(),
+ })
+ );
+ assert_eq!(
+ yarn.index,
+ Some(tool::YarnIndexHook {
+ format: RegistryFormat::Github,
+ metadata: tool::MetadataHook::Bin {
+ bin: "/bin/to/yarn/index".to_string(),
+ base_path: fixture_dir,
+ },
+ })
+ );
+ assert_eq!(
+ hooks.events.unwrap().publish,
+ Some(Publish::Bin("/events/bin".to_string()))
+ );
+ }
+
+ #[test]
+ fn test_from_str_prefixes() {
+ let fixture_dir = fixture_path("hooks");
+ let prefix_file = fixture_dir.join("prefixes.json");
+ let hooks = HookConfig::from_file(&prefix_file).unwrap().unwrap();
+ let node = hooks.node.unwrap();
+ let pnpm = hooks.pnpm.unwrap();
+ let yarn = hooks.yarn.unwrap();
+
+ assert_eq!(
+ node.distro,
+ Some(tool::DistroHook::Prefix(
+ "http://localhost/node/distro/".to_string()
+ ))
+ );
+ assert_eq!(
+ node.latest,
+ Some(tool::MetadataHook::Prefix(
+ "http://localhost/node/latest/".to_string()
+ ))
+ );
+ assert_eq!(
+ node.index,
+ Some(tool::MetadataHook::Prefix(
+ "http://localhost/node/index/".to_string()
+ ))
+ );
+ // pnpm
+ assert_eq!(
+ pnpm.distro,
+ Some(tool::DistroHook::Prefix(
+ "http://localhost/pnpm/distro/".to_string()
+ ))
+ );
+ assert_eq!(
+ pnpm.latest,
+ Some(tool::MetadataHook::Prefix(
+ "http://localhost/pnpm/latest/".to_string()
+ ))
+ );
+ assert_eq!(
+ pnpm.index,
+ Some(tool::MetadataHook::Prefix(
+ "http://localhost/pnpm/index/".to_string()
+ ))
+ );
+ // Yarn
+ assert_eq!(
+ yarn.distro,
+ Some(tool::DistroHook::Prefix(
+ "http://localhost/yarn/distro/".to_string()
+ ))
+ );
+ assert_eq!(
+ yarn.latest,
+ Some(tool::MetadataHook::Prefix(
+ "http://localhost/yarn/latest/".to_string()
+ ))
+ );
+ assert_eq!(
+ yarn.index,
+ Some(tool::YarnIndexHook {
+ format: RegistryFormat::Github,
+ metadata: tool::MetadataHook::Prefix("http://localhost/yarn/index/".to_string())
+ })
+ );
+ }
+
+ #[test]
+ fn test_from_str_templates() {
+ let fixture_dir = fixture_path("hooks");
+ let template_file = fixture_dir.join("templates.json");
+ let hooks = HookConfig::from_file(&template_file).unwrap().unwrap();
+ let node = hooks.node.unwrap();
+ let pnpm = hooks.pnpm.unwrap();
+ let yarn = hooks.yarn.unwrap();
+ assert_eq!(
+ node.distro,
+ Some(tool::DistroHook::Template(
+ "http://localhost/node/distro/{{version}}/".to_string()
+ ))
+ );
+ assert_eq!(
+ node.latest,
+ Some(tool::MetadataHook::Template(
+ "http://localhost/node/latest/{{version}}/".to_string()
+ ))
+ );
+ assert_eq!(
+ node.index,
+ Some(tool::MetadataHook::Template(
+ "http://localhost/node/index/{{version}}/".to_string()
+ ))
+ );
+ // pnpm
+ assert_eq!(
+ pnpm.distro,
+ Some(tool::DistroHook::Template(
+ "http://localhost/pnpm/distro/{{version}}/".to_string()
+ ))
+ );
+ assert_eq!(
+ pnpm.latest,
+ Some(tool::MetadataHook::Template(
+ "http://localhost/pnpm/latest/{{version}}/".to_string()
+ ))
+ );
+ assert_eq!(
+ pnpm.index,
+ Some(tool::MetadataHook::Template(
+ "http://localhost/pnpm/index/{{version}}/".to_string()
+ ))
+ );
+ // Yarn
+ assert_eq!(
+ yarn.distro,
+ Some(tool::DistroHook::Template(
+ "http://localhost/yarn/distro/{{version}}/".to_string()
+ ))
+ );
+ assert_eq!(
+ yarn.latest,
+ Some(tool::MetadataHook::Template(
+ "http://localhost/yarn/latest/{{version}}/".to_string()
+ ))
+ );
+ assert_eq!(
+ yarn.index,
+ Some(tool::YarnIndexHook {
+ format: RegistryFormat::Github,
+ metadata: tool::MetadataHook::Template(
+ "http://localhost/yarn/index/{{version}}/".to_string()
+ )
+ })
+ );
+ }
+
+ #[test]
+ fn test_from_str_format_npm() {
+ let fixture_dir = fixture_path("hooks");
+ let format_npm_file = fixture_dir.join("format_npm.json");
+ let hooks = HookConfig::from_file(&format_npm_file).unwrap().unwrap();
+ let yarn = hooks.yarn.unwrap();
+ let node = hooks.node.unwrap();
+ let npm = hooks.npm.unwrap();
+ let pnpm = hooks.pnpm.unwrap();
+ assert_eq!(
+ yarn.index,
+ Some(tool::YarnIndexHook {
+ format: RegistryFormat::Npm,
+ metadata: tool::MetadataHook::Prefix("http://localhost/yarn/index/".to_string())
+ })
+ );
+ // node and npm don't have format
+ assert_eq!(
+ node.index,
+ Some(tool::MetadataHook::Prefix(
+ "http://localhost/node/index/".to_string()
+ ))
+ );
+ assert_eq!(
+ npm.index,
+ Some(tool::MetadataHook::Prefix(
+ "http://localhost/npm/index/".to_string()
+ ))
+ );
+ // pnpm also doesn't have format
+ assert_eq!(
+ pnpm.index,
+ Some(tool::MetadataHook::Prefix(
+ "http://localhost/pnpm/index/".to_string()
+ ))
+ );
+ }
+
+ #[test]
+ fn test_from_str_format_github() {
+ let fixture_dir = fixture_path("hooks");
+ let format_github_file = fixture_dir.join("format_github.json");
+ let hooks = HookConfig::from_file(&format_github_file).unwrap().unwrap();
+ let yarn = hooks.yarn.unwrap();
+ let node = hooks.node.unwrap();
+ let npm = hooks.npm.unwrap();
+ let pnpm = hooks.pnpm.unwrap();
+ assert_eq!(
+ yarn.index,
+ Some(tool::YarnIndexHook {
+ format: RegistryFormat::Github,
+ metadata: tool::MetadataHook::Prefix("http://localhost/yarn/index/".to_string())
+ })
+ );
+ // node and npm don't have format
+ assert_eq!(
+ node.index,
+ Some(tool::MetadataHook::Prefix(
+ "http://localhost/node/index/".to_string()
+ ))
+ );
+ assert_eq!(
+ npm.index,
+ Some(tool::MetadataHook::Prefix(
+ "http://localhost/npm/index/".to_string()
+ ))
+ );
+ // pnpm also doesn't have format
+ assert_eq!(
+ pnpm.index,
+ Some(tool::MetadataHook::Prefix(
+ "http://localhost/pnpm/index/".to_string()
+ ))
+ );
+ }
+
+ #[test]
+ fn test_merge() {
+ let fixture_dir = fixture_path("hooks");
+ let default_hooks = HookConfig::from_file(&fixture_dir.join("templates.json"))
+ .unwrap()
+ .unwrap();
+ let project_hooks_dir = fixture_path("hooks/project/.volta");
+ let project_hooks_file = project_hooks_dir.join("hooks.json");
+ let project_hooks = HookConfig::from_file(&project_hooks_file)
+ .expect("Could not read project hooks.json")
+ .expect("Could not find project hooks.json");
+ let merged_hooks = project_hooks.merge(default_hooks);
+ let node = merged_hooks.node.expect("No node config found");
+ let pnpm = merged_hooks.pnpm.expect("No pnpm config found");
+ let yarn = merged_hooks.yarn.expect("No yarn config found");
+
+ assert_eq!(
+ node.distro,
+ Some(tool::DistroHook::Bin {
+ bin: "/some/bin/for/node/distro".to_string(),
+ base_path: project_hooks_dir.clone(),
+ })
+ );
+ assert_eq!(
+ node.latest,
+ Some(tool::MetadataHook::Bin {
+ bin: "/some/bin/for/node/latest".to_string(),
+ base_path: project_hooks_dir.clone(),
+ })
+ );
+ assert_eq!(
+ node.index,
+ Some(tool::MetadataHook::Bin {
+ bin: "/some/bin/for/node/index".to_string(),
+ base_path: project_hooks_dir,
+ })
+ );
+ // pnpm
+ assert_eq!(
+ pnpm.distro,
+ Some(tool::DistroHook::Template(
+ "http://localhost/pnpm/distro/{{version}}/".to_string()
+ ))
+ );
+ assert_eq!(
+ pnpm.latest,
+ Some(tool::MetadataHook::Template(
+ "http://localhost/pnpm/latest/{{version}}/".to_string()
+ ))
+ );
+ assert_eq!(
+ pnpm.index,
+ Some(tool::MetadataHook::Template(
+ "http://localhost/pnpm/index/{{version}}/".to_string()
+ ))
+ );
+ // Yarn
+ assert_eq!(
+ yarn.distro,
+ Some(tool::DistroHook::Template(
+ "http://localhost/yarn/distro/{{version}}/".to_string()
+ ))
+ );
+ assert_eq!(
+ yarn.latest,
+ Some(tool::MetadataHook::Template(
+ "http://localhost/yarn/latest/{{version}}/".to_string()
+ ))
+ );
+ assert_eq!(
+ yarn.index,
+ Some(tool::YarnIndexHook {
+ format: RegistryFormat::Github,
+ metadata: tool::MetadataHook::Template(
+ "http://localhost/yarn/index/{{version}}/".to_string()
+ )
+ })
+ );
+ assert_eq!(
+ merged_hooks.events.expect("No events config found").publish,
+ Some(Publish::Bin("/events/bin".to_string()))
+ );
+ }
+
+ #[test]
+ fn test_from_paths() {
+ let project_hooks_dir = fixture_path("hooks/project/.volta");
+ let project_hooks_file = project_hooks_dir.join("hooks.json");
+ let default_hooks_file = fixture_path("hooks/templates.json");
+
+ let merged_hooks =
+ HookConfig::from_paths([project_hooks_file, default_hooks_file]).unwrap();
+ let node = merged_hooks.node.expect("No node config found");
+ let pnpm = merged_hooks.pnpm.expect("No pnpm config found");
+ let yarn = merged_hooks.yarn.expect("No yarn config found");
+
+ assert_eq!(
+ node.distro,
+ Some(tool::DistroHook::Bin {
+ bin: "/some/bin/for/node/distro".to_string(),
+ base_path: project_hooks_dir.clone(),
+ })
+ );
+ assert_eq!(
+ node.latest,
+ Some(tool::MetadataHook::Bin {
+ bin: "/some/bin/for/node/latest".to_string(),
+ base_path: project_hooks_dir.clone(),
+ })
+ );
+ assert_eq!(
+ node.index,
+ Some(tool::MetadataHook::Bin {
+ bin: "/some/bin/for/node/index".to_string(),
+ base_path: project_hooks_dir,
+ })
+ );
+ // pnpm
+ assert_eq!(
+ pnpm.distro,
+ Some(tool::DistroHook::Template(
+ "http://localhost/pnpm/distro/{{version}}/".to_string()
+ ))
+ );
+ assert_eq!(
+ pnpm.latest,
+ Some(tool::MetadataHook::Template(
+ "http://localhost/pnpm/latest/{{version}}/".to_string()
+ ))
+ );
+ assert_eq!(
+ pnpm.index,
+ Some(tool::MetadataHook::Template(
+ "http://localhost/pnpm/index/{{version}}/".to_string()
+ ))
+ );
+ // Yarn
+ assert_eq!(
+ yarn.distro,
+ Some(tool::DistroHook::Template(
+ "http://localhost/yarn/distro/{{version}}/".to_string()
+ ))
+ );
+ assert_eq!(
+ yarn.latest,
+ Some(tool::MetadataHook::Template(
+ "http://localhost/yarn/latest/{{version}}/".to_string()
+ ))
+ );
+ assert_eq!(
+ yarn.index,
+ Some(tool::YarnIndexHook {
+ format: RegistryFormat::Github,
+ metadata: tool::MetadataHook::Template(
+ "http://localhost/yarn/index/{{version}}/".to_string()
+ )
+ })
+ );
+ assert_eq!(
+ merged_hooks.events.expect("No events config found").publish,
+ Some(Publish::Bin("/events/bin".to_string()))
+ );
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +
use std::marker::PhantomData;
+use std::path::Path;
+
+use super::tool;
+use super::RegistryFormat;
+use crate::error::{ErrorKind, Fallible, VoltaError};
+use crate::tool::{Node, Npm, Pnpm, Tool};
+use serde::{Deserialize, Serialize};
+
+#[derive(Serialize, Deserialize)]
+pub struct RawResolveHook {
+ prefix: Option<String>,
+ template: Option<String>,
+ bin: Option<String>,
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct RawIndexHook {
+ prefix: Option<String>,
+ template: Option<String>,
+ bin: Option<String>,
+ format: Option<String>,
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct RawPublishHook {
+ url: Option<String>,
+ bin: Option<String>,
+}
+
+impl RawResolveHook {
+ fn into_hook<H, P, T, B>(self, to_prefix: P, to_template: T, to_bin: B) -> Fallible<H>
+ where
+ P: FnOnce(String) -> H,
+ T: FnOnce(String) -> H,
+ B: FnOnce(String) -> H,
+ {
+ match self {
+ RawResolveHook {
+ prefix: Some(prefix),
+ template: None,
+ bin: None,
+ } => Ok(to_prefix(prefix)),
+ RawResolveHook {
+ prefix: None,
+ template: Some(template),
+ bin: None,
+ } => Ok(to_template(template)),
+ RawResolveHook {
+ prefix: None,
+ template: None,
+ bin: Some(bin),
+ } => Ok(to_bin(bin)),
+ RawResolveHook {
+ prefix: None,
+ template: None,
+ bin: None,
+ } => Err(ErrorKind::HookNoFieldsSpecified.into()),
+ _ => Err(ErrorKind::HookMultipleFieldsSpecified.into()),
+ }
+ }
+
+ pub fn into_distro_hook(self, base_dir: &Path) -> Fallible<tool::DistroHook> {
+ self.into_hook(
+ tool::DistroHook::Prefix,
+ tool::DistroHook::Template,
+ |bin| tool::DistroHook::Bin {
+ bin,
+ base_path: base_dir.to_owned(),
+ },
+ )
+ }
+
+ pub fn into_metadata_hook(self, base_dir: &Path) -> Fallible<tool::MetadataHook> {
+ self.into_hook(
+ tool::MetadataHook::Prefix,
+ tool::MetadataHook::Template,
+ |bin| tool::MetadataHook::Bin {
+ bin,
+ base_path: base_dir.to_owned(),
+ },
+ )
+ }
+}
+
+impl RawIndexHook {
+ pub fn into_index_hook(self, base_dir: &Path) -> Fallible<tool::YarnIndexHook> {
+ // use user-specified format, or default to Github (legacy)
+ let format = match self.format {
+ Some(format_str) => RegistryFormat::from_str(&format_str)?,
+ None => RegistryFormat::Github,
+ };
+ Ok(tool::YarnIndexHook {
+ format,
+ metadata: RawResolveHook {
+ prefix: self.prefix,
+ template: self.template,
+ bin: self.bin,
+ }
+ .into_metadata_hook(base_dir)?,
+ })
+ }
+}
+
+impl TryFrom<RawPublishHook> for super::Publish {
+ type Error = VoltaError;
+
+ fn try_from(raw: RawPublishHook) -> Fallible<super::Publish> {
+ match raw {
+ RawPublishHook {
+ url: Some(url),
+ bin: None,
+ } => Ok(super::Publish::Url(url)),
+ RawPublishHook {
+ url: None,
+ bin: Some(bin),
+ } => Ok(super::Publish::Bin(bin)),
+ RawPublishHook {
+ url: None,
+ bin: None,
+ } => Err(ErrorKind::PublishHookNeitherUrlNorBin.into()),
+ _ => Err(ErrorKind::PublishHookBothUrlAndBin.into()),
+ }
+ }
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct RawHookConfig {
+ pub node: Option<RawToolHooks<Node>>,
+ pub npm: Option<RawToolHooks<Npm>>,
+ pub pnpm: Option<RawToolHooks<Pnpm>>,
+ pub yarn: Option<RawYarnHooks>,
+ pub events: Option<RawEventHooks>,
+}
+
+#[derive(Serialize, Deserialize)]
+#[serde(rename = "events")]
+pub struct RawEventHooks {
+ pub publish: Option<RawPublishHook>,
+}
+
+impl TryFrom<RawEventHooks> for super::EventHooks {
+ type Error = VoltaError;
+
+ fn try_from(raw: RawEventHooks) -> Fallible<super::EventHooks> {
+ let publish = raw.publish.map(|p| p.try_into()).transpose()?;
+
+ Ok(super::EventHooks { publish })
+ }
+}
+
+#[derive(Serialize, Deserialize)]
+#[serde(rename = "tool")]
+pub struct RawToolHooks<T: Tool> {
+ pub distro: Option<RawResolveHook>,
+ pub latest: Option<RawResolveHook>,
+ pub index: Option<RawResolveHook>,
+
+ #[serde(skip)]
+ phantom: PhantomData<T>,
+}
+
+#[derive(Serialize, Deserialize)]
+#[serde(rename = "yarn")]
+pub struct RawYarnHooks {
+ pub distro: Option<RawResolveHook>,
+ pub latest: Option<RawResolveHook>,
+ pub index: Option<RawIndexHook>,
+}
+
+impl RawHookConfig {
+ pub fn into_hook_config(self, base_dir: &Path) -> Fallible<super::HookConfig> {
+ let node = self.node.map(|n| n.into_tool_hooks(base_dir)).transpose()?;
+ let npm = self.npm.map(|n| n.into_tool_hooks(base_dir)).transpose()?;
+ let pnpm = self.pnpm.map(|p| p.into_tool_hooks(base_dir)).transpose()?;
+ let yarn = self.yarn.map(|y| y.into_yarn_hooks(base_dir)).transpose()?;
+ let events = self.events.map(|e| e.try_into()).transpose()?;
+ Ok(super::HookConfig {
+ node,
+ npm,
+ pnpm,
+ yarn,
+ events,
+ })
+ }
+}
+
+impl<T: Tool> RawToolHooks<T> {
+ pub fn into_tool_hooks(self, base_dir: &Path) -> Fallible<super::ToolHooks<T>> {
+ let distro = self
+ .distro
+ .map(|d| d.into_distro_hook(base_dir))
+ .transpose()?;
+ let latest = self
+ .latest
+ .map(|d| d.into_metadata_hook(base_dir))
+ .transpose()?;
+ let index = self
+ .index
+ .map(|d| d.into_metadata_hook(base_dir))
+ .transpose()?;
+
+ Ok(super::ToolHooks {
+ distro,
+ latest,
+ index,
+ phantom: PhantomData,
+ })
+ }
+}
+
+impl RawYarnHooks {
+ pub fn into_yarn_hooks(self, base_dir: &Path) -> Fallible<super::YarnHooks> {
+ let distro = self
+ .distro
+ .map(|d| d.into_distro_hook(base_dir))
+ .transpose()?;
+ let latest = self
+ .latest
+ .map(|d| d.into_metadata_hook(base_dir))
+ .transpose()?;
+ let index = self
+ .index
+ .map(|d| d.into_index_hook(base_dir))
+ .transpose()?;
+
+ Ok(super::YarnHooks {
+ distro,
+ latest,
+ index,
+ })
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +
//! Types representing Volta Tool Hooks.
+
+use std::ffi::OsString;
+use std::path::{Path, PathBuf};
+use std::process::Stdio;
+
+use crate::command::create_command;
+use crate::error::{Context, ErrorKind, Fallible};
+use crate::hook::RegistryFormat;
+use crate::tool::{NODE_DISTRO_ARCH, NODE_DISTRO_OS};
+use cmdline_words_parser::parse_posix;
+use dunce::canonicalize;
+use log::debug;
+use node_semver::Version;
+use once_cell::sync::Lazy;
+
+const ARCH_TEMPLATE: &str = "{{arch}}";
+const OS_TEMPLATE: &str = "{{os}}";
+const VERSION_TEMPLATE: &str = "{{version}}";
+const EXTENSION_TEMPLATE: &str = "{{ext}}";
+const FILENAME_TEMPLATE: &str = "{{filename}}";
+
+static REL_PATH: Lazy<String> = Lazy::new(|| format!(".{}", std::path::MAIN_SEPARATOR));
+static REL_PATH_PARENT: Lazy<String> = Lazy::new(|| format!("..{}", std::path::MAIN_SEPARATOR));
+
+/// A hook for resolving the distro URL for a given tool version
+#[derive(PartialEq, Eq, Debug)]
+pub enum DistroHook {
+ Prefix(String),
+ Template(String),
+ Bin { bin: String, base_path: PathBuf },
+}
+
+impl DistroHook {
+ /// Performs resolution of the distro URL based on the given version and file name
+ pub fn resolve(&self, version: &Version, filename: &str) -> Fallible<String> {
+ let extension = calculate_extension(filename).unwrap_or("");
+
+ match &self {
+ DistroHook::Prefix(prefix) => Ok(format!("{}{}", prefix, filename)),
+ DistroHook::Template(template) => Ok(template
+ .replace(ARCH_TEMPLATE, NODE_DISTRO_ARCH)
+ .replace(OS_TEMPLATE, NODE_DISTRO_OS)
+ .replace(EXTENSION_TEMPLATE, extension)
+ .replace(FILENAME_TEMPLATE, filename)
+ .replace(VERSION_TEMPLATE, &version.to_string())),
+ DistroHook::Bin { bin, base_path } => {
+ execute_binary(bin, base_path, Some(version.to_string()))
+ }
+ }
+ }
+}
+
+/// Use the expected filename to determine the extension for this hook
+///
+/// This will include the multi-part `tar.gz` extension if it is present, otherwise it will use
+/// the standard extension.
+fn calculate_extension(filename: &str) -> Option<&str> {
+ let mut parts = filename.rsplit('.');
+ match (parts.next(), parts.next(), parts.next()) {
+ (Some(ext), Some("tar"), Some(_)) => {
+ // .tar.gz style extension, return both parts
+ // tar . gz
+ let index = filename.len() - 3 - 1 - ext.len();
+ filename.get(index..)
+ }
+ (Some(_), Some(""), None) => {
+ // Dotfile, e.g. `.npmrc`, where the `.` character is at the beginning - No extension
+ None
+ }
+ (Some(ext), Some(_), _) => {
+ // Standard File Extension
+ Some(ext)
+ }
+ _ => None,
+ }
+}
+
+/// A hook for resolving the URL for metadata about a tool
+#[derive(PartialEq, Eq, Debug)]
+pub enum MetadataHook {
+ Prefix(String),
+ Template(String),
+ Bin { bin: String, base_path: PathBuf },
+}
+
+impl MetadataHook {
+ /// Performs resolution of the metadata URL based on the given default file name
+ pub fn resolve(&self, filename: &str) -> Fallible<String> {
+ match &self {
+ MetadataHook::Prefix(prefix) => Ok(format!("{}{}", prefix, filename)),
+ MetadataHook::Template(template) => Ok(template
+ .replace(ARCH_TEMPLATE, NODE_DISTRO_ARCH)
+ .replace(OS_TEMPLATE, NODE_DISTRO_OS)
+ .replace(FILENAME_TEMPLATE, filename)),
+ MetadataHook::Bin { bin, base_path } => execute_binary(bin, base_path, None),
+ }
+ }
+}
+
+/// A hook for resolving the URL for the Yarn index
+#[derive(PartialEq, Eq, Debug)]
+pub struct YarnIndexHook {
+ pub format: RegistryFormat,
+ pub metadata: MetadataHook,
+}
+
+impl YarnIndexHook {
+ /// Performs resolution of the metadata URL based on the given default file name
+ pub fn resolve(&self, filename: &str) -> Fallible<String> {
+ match &self.metadata {
+ MetadataHook::Prefix(prefix) => Ok(format!("{}{}", prefix, filename)),
+ MetadataHook::Template(template) => Ok(template
+ .replace(ARCH_TEMPLATE, NODE_DISTRO_ARCH)
+ .replace(OS_TEMPLATE, NODE_DISTRO_OS)
+ .replace(FILENAME_TEMPLATE, filename)),
+ MetadataHook::Bin { bin, base_path } => execute_binary(bin, base_path, None),
+ }
+ }
+}
+
+/// Execute a shell command and return the trimmed stdout from that command
+fn execute_binary(bin: &str, base_path: &Path, extra_arg: Option<String>) -> Fallible<String> {
+ let mut trimmed = bin.trim().to_string();
+ let mut words = parse_posix(&mut trimmed);
+ let cmd = match words.next() {
+ Some(word) => {
+ // Treat any path that starts with a './' or '../' as a relative path (using OS separator)
+ if word.starts_with(REL_PATH.as_str()) || word.starts_with(REL_PATH_PARENT.as_str()) {
+ canonicalize(base_path.join(word)).with_context(|| ErrorKind::HookPathError {
+ command: String::from(word),
+ })?
+ } else {
+ PathBuf::from(word)
+ }
+ }
+ None => {
+ return Err(ErrorKind::InvalidHookCommand {
+ command: String::from(bin.trim()),
+ }
+ .into())
+ }
+ };
+
+ let mut args: Vec<OsString> = words.map(OsString::from).collect();
+ if let Some(arg) = extra_arg {
+ args.push(OsString::from(arg));
+ }
+
+ let mut command = create_command(cmd);
+ command
+ .args(&args)
+ .current_dir(base_path)
+ .stdin(Stdio::null())
+ .stdout(Stdio::piped())
+ .stderr(Stdio::inherit());
+
+ debug!("Running hook command: {:?}", command);
+ let output = command
+ .output()
+ .with_context(|| ErrorKind::ExecuteHookError {
+ command: String::from(bin.trim()),
+ })?;
+
+ if !output.status.success() {
+ return Err(ErrorKind::HookCommandFailed {
+ command: bin.trim().into(),
+ }
+ .into());
+ }
+
+ let url = String::from_utf8(output.stdout).with_context(|| ErrorKind::InvalidHookOutput {
+ command: String::from(bin.trim()),
+ })?;
+
+ Ok(url.trim().to_string())
+}
+
+#[cfg(test)]
+pub mod tests {
+ use super::{calculate_extension, DistroHook, MetadataHook};
+ use crate::tool::{NODE_DISTRO_ARCH, NODE_DISTRO_OS};
+ use node_semver::Version;
+
+ #[test]
+ fn test_distro_prefix_resolve() {
+ let prefix = "http://localhost/node/distro/";
+ let filename = "node.tar.gz";
+ let hook = DistroHook::Prefix(prefix.to_string());
+ let version = Version::parse("1.0.0").unwrap();
+
+ assert_eq!(
+ hook.resolve(&version, filename)
+ .expect("Could not resolve URL"),
+ format!("{}{}", prefix, filename)
+ );
+ }
+
+ #[test]
+ fn test_distro_template_resolve() {
+ let hook = DistroHook::Template(
+ "http://localhost/node/{{os}}/{{arch}}/{{version}}/{{ext}}/{{filename}}".to_string(),
+ );
+ let version = Version::parse("1.0.0").unwrap();
+
+ // tar.gz format has extra handling, to support a multi-part extension
+ let expected = format!(
+ "http://localhost/node/{}/{}/{}/tar.gz/node-v1.0.0.tar.gz",
+ NODE_DISTRO_OS, NODE_DISTRO_ARCH, version
+ );
+ assert_eq!(
+ hook.resolve(&version, "node-v1.0.0.tar.gz")
+ .expect("Could not resolve URL"),
+ expected
+ );
+
+ // zip is a standard extension
+ let expected = format!(
+ "http://localhost/node/{}/{}/{}/zip/node-v1.0.0.zip",
+ NODE_DISTRO_OS, NODE_DISTRO_ARCH, version
+ );
+ assert_eq!(
+ hook.resolve(&version, "node-v1.0.0.zip")
+ .expect("Could not resolve URL"),
+ expected
+ );
+ }
+
+ #[test]
+ fn test_metadata_prefix_resolve() {
+ let prefix = "http://localhost/node/index/";
+ let filename = "index.json";
+ let hook = MetadataHook::Prefix(prefix.to_string());
+
+ assert_eq!(
+ hook.resolve(filename).expect("Could not resolve URL"),
+ format!("{}{}", prefix, filename)
+ );
+ }
+
+ #[test]
+ fn test_metadata_template_resolve() {
+ let hook = MetadataHook::Template(
+ "http://localhost/node/{{os}}/{{arch}}/{{filename}}".to_string(),
+ );
+ let expected = format!(
+ "http://localhost/node/{}/{}/index.json",
+ NODE_DISTRO_OS, NODE_DISTRO_ARCH
+ );
+
+ assert_eq!(
+ hook.resolve("index.json").expect("Could not resolve URL"),
+ expected
+ );
+ }
+
+ #[test]
+ fn test_calculate_extension() {
+ // Handles .tar.* files
+ assert_eq!(calculate_extension("file.tar.gz"), Some("tar.gz"));
+ assert_eq!(calculate_extension("file.tar.xz"), Some("tar.xz"));
+ assert_eq!(calculate_extension("file.tar.xyz"), Some("tar.xyz"));
+
+ // Handles dotfiles
+ assert_eq!(calculate_extension(".filerc"), None);
+
+ // Handles standard extensions
+ assert_eq!(calculate_extension("tar.gz"), Some("gz"));
+ assert_eq!(calculate_extension("file.zip"), Some("zip"));
+
+ // Handles files with no extension at all
+ assert_eq!(calculate_extension("bare_file"), None);
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +
//! Provides types for working with Volta's _inventory_, the local repository
+//! of available tool versions.
+
+use std::collections::BTreeSet;
+use std::ffi::OsStr;
+use std::path::Path;
+
+use crate::error::{Context, ErrorKind, Fallible};
+use crate::fs::read_dir_eager;
+use crate::layout::volta_home;
+use crate::tool::PackageConfig;
+use crate::version::parse_version;
+use log::debug;
+use node_semver::Version;
+use walkdir::WalkDir;
+
+/// Checks if a given Node version image is available on the local machine
+pub fn node_available(version: &Version) -> Fallible<bool> {
+ volta_home().map(|home| {
+ home.node_image_root_dir()
+ .join(version.to_string())
+ .exists()
+ })
+}
+
+/// Collects a set of all Node versions fetched on the local machine
+pub fn node_versions() -> Fallible<BTreeSet<Version>> {
+ volta_home().and_then(|home| read_versions(home.node_image_root_dir()))
+}
+
+/// Checks if a given npm version image is available on the local machine
+pub fn npm_available(version: &Version) -> Fallible<bool> {
+ volta_home().map(|home| home.npm_image_dir(&version.to_string()).exists())
+}
+
+/// Collects a set of all npm versions fetched on the local machine
+pub fn npm_versions() -> Fallible<BTreeSet<Version>> {
+ volta_home().and_then(|home| read_versions(home.npm_image_root_dir()))
+}
+
+/// Checks if a given pnpm version image is available on the local machine
+pub fn pnpm_available(version: &Version) -> Fallible<bool> {
+ volta_home().map(|home| home.pnpm_image_dir(&version.to_string()).exists())
+}
+
+/// Collects a set of all pnpm versions fetched on the local machine
+pub fn pnpm_versions() -> Fallible<BTreeSet<Version>> {
+ volta_home().and_then(|home| read_versions(home.pnpm_image_root_dir()))
+}
+
+/// Checks if a given Yarn version image is available on the local machine
+pub fn yarn_available(version: &Version) -> Fallible<bool> {
+ volta_home().map(|home| home.yarn_image_dir(&version.to_string()).exists())
+}
+
+/// Collects a set of all Yarn versions fetched on the local machine
+pub fn yarn_versions() -> Fallible<BTreeSet<Version>> {
+ volta_home().and_then(|home| read_versions(home.yarn_image_root_dir()))
+}
+
+/// Collects a set of all Package Configs on the local machine
+pub fn package_configs() -> Fallible<BTreeSet<PackageConfig>> {
+ let package_dir = volta_home()?.default_package_dir();
+
+ WalkDir::new(package_dir)
+ .max_depth(2)
+ .into_iter()
+ // Ignore any items which didn't resolve as `DirEntry` correctly.
+ // There is no point trying to do anything with those, and no error
+ // we can report to the user in any case. Log the failure in the
+ // debug output, though
+ .filter_map(|entry| match entry {
+ Ok(dir_entry) => {
+ // Ignore directory entries and any files that don't have a .json extension.
+ // This will prevent us from trying to parse OS-generated files as package
+ // configs (e.g. `.DS_Store` on macOS)
+ let extension = dir_entry.path().extension().and_then(OsStr::to_str);
+ match (dir_entry.file_type().is_file(), extension) {
+ (true, Some(ext)) if ext.eq_ignore_ascii_case("json") => {
+ Some(dir_entry.into_path())
+ }
+ _ => None,
+ }
+ }
+ Err(e) => {
+ debug!("{}", e);
+ None
+ }
+ })
+ .map(PackageConfig::from_file)
+ .collect()
+}
+
+/// Reads the contents of a directory and returns the set of all versions found
+/// in the directory's listing by parsing the directory names as semantic versions
+fn read_versions(dir: &Path) -> Fallible<BTreeSet<Version>> {
+ let contents = read_dir_eager(dir).with_context(|| ErrorKind::ReadDirError {
+ dir: dir.to_owned(),
+ })?;
+
+ Ok(contents
+ .filter(|(_, metadata)| metadata.is_dir())
+ .filter_map(|(entry, _)| parse_version(entry.file_name().to_string_lossy()).ok())
+ .collect())
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +
use std::env;
+use std::path::PathBuf;
+
+use crate::error::{Context, ErrorKind, Fallible};
+use cfg_if::cfg_if;
+use dunce::canonicalize;
+use once_cell::sync::OnceCell;
+use volta_layout::v4::{VoltaHome, VoltaInstall};
+
+cfg_if! {
+ if #[cfg(unix)] {
+ mod unix;
+ pub use unix::*;
+ } else if #[cfg(windows)] {
+ mod windows;
+ pub use windows::*;
+ }
+}
+
+static VOLTA_HOME: OnceCell<VoltaHome> = OnceCell::new();
+static VOLTA_INSTALL: OnceCell<VoltaInstall> = OnceCell::new();
+
+pub fn volta_home<'a>() -> Fallible<&'a VoltaHome> {
+ VOLTA_HOME.get_or_try_init(|| {
+ let home_dir = match env::var_os("VOLTA_HOME") {
+ Some(home) => PathBuf::from(home),
+ None => default_home_dir()?,
+ };
+
+ Ok(VoltaHome::new(home_dir))
+ })
+}
+
+pub fn volta_install<'a>() -> Fallible<&'a VoltaInstall> {
+ VOLTA_INSTALL.get_or_try_init(|| {
+ let install_dir = match env::var_os("VOLTA_INSTALL_DIR") {
+ Some(install) => PathBuf::from(install),
+ None => default_install_dir()?,
+ };
+
+ Ok(VoltaInstall::new(install_dir))
+ })
+}
+
+/// Determine the binary install directory from the currently running executable
+///
+/// The volta-shim and volta binaries will be installed in the same location, so we can use the
+/// currently running executable to find the binary install directory. Note that we need to
+/// canonicalize the path we get from current_exe to make sure we resolve symlinks and find the
+/// actual binary files
+fn default_install_dir() -> Fallible<PathBuf> {
+ env::current_exe()
+ .and_then(canonicalize)
+ .map(|mut path| {
+ path.pop(); // Remove the executable name from the path
+ path
+ })
+ .with_context(|| ErrorKind::NoInstallDir)
+}
+
use std::path::PathBuf;
+
+use super::volta_home;
+use crate::error::{ErrorKind, Fallible};
+
+pub(super) fn default_home_dir() -> Fallible<PathBuf> {
+ let mut home = dirs::home_dir().ok_or(ErrorKind::NoHomeEnvironmentVar)?;
+ home.push(".volta");
+ Ok(home)
+}
+
+pub fn env_paths() -> Fallible<Vec<PathBuf>> {
+ let home = volta_home()?;
+ Ok(vec![home.shim_dir().to_owned()])
+}
+
//! The main implementation crate for the core of Volta.
+
+mod command;
+pub mod error;
+pub mod event;
+pub mod fs;
+mod hook;
+pub mod inventory;
+pub mod layout;
+pub mod log;
+pub mod monitor;
+pub mod platform;
+pub mod project;
+pub mod run;
+pub mod session;
+pub mod shim;
+pub mod signal;
+pub mod style;
+pub mod sync;
+pub mod tool;
+pub mod toolchain;
+pub mod version;
+
+const VOLTA_FEATURE_PNPM: &str = "VOLTA_FEATURE_PNPM";
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +
//! This module provides a custom Logger implementation for use with the `log` crate
+use console::style;
+use log::{trace, Level, LevelFilter, Log, Metadata, Record, SetLoggerError};
+use std::env;
+use std::fmt::Display;
+use std::io::IsTerminal;
+use textwrap::{fill, Options, WordSplitter};
+
+use crate::style::text_width;
+
+const ERROR_PREFIX: &str = "error:";
+const WARNING_PREFIX: &str = "warning:";
+const SHIM_ERROR_PREFIX: &str = "Volta error:";
+const SHIM_WARNING_PREFIX: &str = "Volta warning:";
+const MIGRATION_ERROR_PREFIX: &str = "Volta update error:";
+const MIGRATION_WARNING_PREFIX: &str = "Volta update warning:";
+const VOLTA_LOGLEVEL: &str = "VOLTA_LOGLEVEL";
+const ALLOWED_PREFIXES: [&str; 5] = [
+ "volta",
+ "archive",
+ "fs-utils",
+ "progress-read",
+ "validate-npm-package-name",
+];
+const WRAP_INDENT: &str = " ";
+
+/// Represents the context from which the logger was created
+pub enum LogContext {
+ /// Log messages from the `volta` executable
+ Volta,
+
+ /// Log messages from one of the shims
+ Shim,
+
+ /// Log messages from the migration
+ Migration,
+}
+
+/// Represents the level of verbosity that was requested by the user
+#[derive(Debug, Copy, Clone)]
+pub enum LogVerbosity {
+ Quiet,
+ Default,
+ Verbose,
+ VeryVerbose,
+}
+
+pub struct Logger {
+ context: LogContext,
+ level: LevelFilter,
+}
+
+impl Log for Logger {
+ fn enabled(&self, metadata: &Metadata) -> bool {
+ metadata.level() <= self.level
+ }
+
+ fn log(&self, record: &Record) {
+ let level_allowed = self.enabled(record.metadata());
+
+ let is_valid_target = ALLOWED_PREFIXES
+ .iter()
+ .any(|prefix| record.target().starts_with(prefix));
+
+ if level_allowed && is_valid_target {
+ match record.level() {
+ Level::Error => self.log_error(record.args()),
+ Level::Warn => self.log_warning(record.args()),
+ // all info-level messages go to stdout
+ Level::Info => println!("{}", record.args()),
+ // all debug- and trace-level messages go to stderr
+ Level::Debug => eprintln!("[verbose] {}", record.args()),
+ Level::Trace => eprintln!("[trace] {}", record.args()),
+ }
+ }
+ }
+
+ fn flush(&self) {}
+}
+
+impl Logger {
+ /// Initialize the global logger with a Logger instance
+ /// Will use the requested level of Verbosity
+ /// If set to Default, will use the environment to determine the level of verbosity
+ pub fn init(context: LogContext, verbosity: LogVerbosity) -> Result<(), SetLoggerError> {
+ let logger = Logger::new(context, verbosity);
+ log::set_max_level(logger.level);
+ log::set_boxed_logger(Box::new(logger))?;
+ Ok(())
+ }
+
+ fn new(context: LogContext, verbosity: LogVerbosity) -> Self {
+ let level = match verbosity {
+ LogVerbosity::Quiet => LevelFilter::Error,
+ LogVerbosity::Default => level_from_env(),
+ LogVerbosity::Verbose => LevelFilter::Debug,
+ LogVerbosity::VeryVerbose => LevelFilter::Trace,
+ };
+
+ Logger { context, level }
+ }
+
+ fn log_error<D>(&self, message: &D)
+ where
+ D: Display,
+ {
+ let prefix = match &self.context {
+ LogContext::Volta => ERROR_PREFIX,
+ LogContext::Shim => SHIM_ERROR_PREFIX,
+ LogContext::Migration => MIGRATION_ERROR_PREFIX,
+ };
+
+ eprintln!("{} {}", style(prefix).red().bold(), message);
+ }
+
+ fn log_warning<D>(&self, message: &D)
+ where
+ D: Display,
+ {
+ let prefix = match &self.context {
+ LogContext::Volta => WARNING_PREFIX,
+ LogContext::Shim => SHIM_WARNING_PREFIX,
+ LogContext::Migration => MIGRATION_WARNING_PREFIX,
+ };
+
+ eprintln!(
+ "{} {}",
+ style(prefix).yellow().bold(),
+ wrap_content(prefix, message)
+ );
+ }
+}
+
+/// Wraps the supplied content to the terminal width, if we are in a terminal.
+/// If not, returns the content as a String
+///
+/// Note: Uses the supplied prefix to calculate the terminal width, but then removes
+/// it so that it can be styled (style characters are counted against the wrapped width)
+fn wrap_content<D>(prefix: &str, content: &D) -> String
+where
+ D: Display,
+{
+ match text_width() {
+ Some(width) => {
+ let options = Options::new(width)
+ .word_splitter(WordSplitter::NoHyphenation)
+ .subsequent_indent(WRAP_INDENT)
+ .break_words(false);
+
+ fill(&format!("{} {}", prefix, content), options).replace(prefix, "")
+ }
+ None => format!(" {}", content),
+ }
+}
+
+/// Determines the correct logging level based on the environment
+/// If VOLTA_LOGLEVEL is set to a valid level, we use that
+/// If not, we check the current stdout to determine whether it is a TTY or not
+/// If it is a TTY, we use Info
+/// If it is NOT a TTY, we use Error as we don't want to show warnings when running as a script
+fn level_from_env() -> LevelFilter {
+ env::var(VOLTA_LOGLEVEL)
+ .ok()
+ .and_then(|level| level.to_uppercase().parse().ok())
+ .unwrap_or_else(|| {
+ if std::io::stdout().is_terminal() {
+ trace!("using fallback log level (info)");
+ LevelFilter::Info
+ } else {
+ LevelFilter::Error
+ }
+ })
+}
+
+#[cfg(test)]
+mod tests {}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +
use std::env;
+use std::io::Write;
+use std::path::PathBuf;
+use std::process::{Child, Stdio};
+
+use log::debug;
+use tempfile::NamedTempFile;
+
+use crate::command::create_command;
+use crate::event::Event;
+
+/// Send event to the spawned command process
+// if hook command is not configured, this is not called
+pub fn send_events(command: &str, events: &[Event]) {
+ match serde_json::to_string_pretty(&events) {
+ Ok(events_json) => {
+ let tempfile_path = env::var_os("VOLTA_WRITE_EVENTS_FILE")
+ .and_then(|_| write_events_file(events_json.clone()));
+ if let Some(ref mut child_process) = spawn_process(command, tempfile_path) {
+ if let Some(ref mut p_stdin) = child_process.stdin.as_mut() {
+ if let Err(error) = writeln!(p_stdin, "{}", events_json) {
+ debug!("Could not write events to executable stdin: {:?}", error);
+ }
+ }
+ }
+ }
+ Err(error) => {
+ debug!("Could not serialize events data to JSON: {:?}", error);
+ }
+ }
+}
+
+// Write the events JSON to a file in the temporary directory
+fn write_events_file(events_json: String) -> Option<PathBuf> {
+ match NamedTempFile::new() {
+ Ok(mut events_file) => {
+ match events_file.write_all(events_json.as_bytes()) {
+ Ok(()) => {
+ let path = events_file.into_temp_path();
+ // if it's not persisted, the temp file will be automatically deleted
+ // (and the executable won't be able to read it)
+ match path.keep() {
+ Ok(tempfile_path) => Some(tempfile_path),
+ Err(error) => {
+ debug!("Failed to persist temp file for events data: {:?}", error);
+ None
+ }
+ }
+ }
+ Err(error) => {
+ debug!("Failed to write events to the temp file: {:?}", error);
+ None
+ }
+ }
+ }
+ Err(error) => {
+ debug!("Failed to create a temp file for events data: {:?}", error);
+ None
+ }
+ }
+}
+
+// Spawn a child process to receive the events data, setting the path to the events file as an env var
+fn spawn_process(command: &str, tempfile_path: Option<PathBuf>) -> Option<Child> {
+ command.split(' ').take(1).next().and_then(|executable| {
+ let mut child = create_command(executable);
+ child.args(command.split(' ').skip(1));
+ child.stdin(Stdio::piped());
+ if let Some(events_file) = tempfile_path {
+ child.env("EVENTS_FILE", events_file);
+ }
+
+ #[cfg(not(debug_assertions))]
+ // Hide stdout and stderr of spawned process in release mode
+ child.stdout(Stdio::null()).stderr(Stdio::null());
+
+ match child.spawn() {
+ Err(err) => {
+ debug!("Unable to run executable command: '{}'\n{}", command, err);
+ None
+ }
+ Ok(c) => Some(c),
+ }
+ })
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +
use std::ffi::OsString;
+use std::path::PathBuf;
+
+use super::{build_path_error, Sourced};
+use crate::error::{Context, Fallible};
+use crate::layout::volta_home;
+use crate::tool::load_default_npm_version;
+use node_semver::Version;
+
+/// A platform image.
+pub struct Image {
+ /// The pinned version of Node.
+ pub node: Sourced<Version>,
+ /// The custom version of npm, if any. `None` represents using the npm that is bundled with Node
+ pub npm: Option<Sourced<Version>>,
+ /// The pinned version of pnpm, if any.
+ pub pnpm: Option<Sourced<Version>>,
+ /// The pinned version of Yarn, if any.
+ pub yarn: Option<Sourced<Version>>,
+}
+
+impl Image {
+ fn bins(&self) -> Fallible<Vec<PathBuf>> {
+ let home = volta_home()?;
+ let mut bins = Vec::with_capacity(3);
+
+ if let Some(npm) = &self.npm {
+ let npm_str = npm.value.to_string();
+ bins.push(home.npm_image_bin_dir(&npm_str));
+ }
+
+ if let Some(pnpm) = &self.pnpm {
+ let pnpm_str = pnpm.value.to_string();
+ bins.push(home.pnpm_image_bin_dir(&pnpm_str));
+ }
+
+ if let Some(yarn) = &self.yarn {
+ let yarn_str = yarn.value.to_string();
+ bins.push(home.yarn_image_bin_dir(&yarn_str));
+ }
+
+ // Add Node path to the bins last, so that any custom version of npm will be earlier in the PATH
+ let node_str = self.node.value.to_string();
+ bins.push(home.node_image_bin_dir(&node_str));
+ Ok(bins)
+ }
+
+ /// Produces a modified version of the current `PATH` environment variable that
+ /// will find toolchain executables (Node, npm, pnpm, Yarn) in the installation directories
+ /// for the given versions instead of in the Volta shim directory.
+ pub fn path(&self) -> Fallible<OsString> {
+ let old_path = envoy::path().unwrap_or_else(|| envoy::Var::from(""));
+
+ old_path
+ .split()
+ .prefix(self.bins()?)
+ .join()
+ .with_context(build_path_error)
+ }
+
+ /// Determines the sourced version of npm that will be available, resolving the version bundled with Node, if needed
+ pub fn resolve_npm(&self) -> Fallible<Sourced<Version>> {
+ match &self.npm {
+ Some(npm) => Ok(npm.clone()),
+ None => load_default_npm_version(&self.node.value).map(|npm| Sourced {
+ value: npm,
+ source: self.node.source,
+ }),
+ }
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +
use std::env;
+use std::fmt;
+
+use crate::error::{ErrorKind, Fallible};
+use crate::session::Session;
+use crate::tool::{Node, Npm, Pnpm, Yarn};
+use crate::VOLTA_FEATURE_PNPM;
+use node_semver::Version;
+
+mod image;
+mod system;
+// Note: The tests get their own module because we need them to run as a single unit to prevent
+// clobbering environment variable changes
+#[cfg(test)]
+mod tests;
+
+pub use image::Image;
+pub use system::System;
+
+/// The source with which a version is associated
+#[derive(Clone, Copy)]
+#[cfg_attr(test, derive(Eq, PartialEq, Debug))]
+pub enum Source {
+ /// Represents a version from the user default platform
+ Default,
+
+ /// Represents a version from a project manifest
+ Project,
+
+ /// Represents a version from a pinned Binary platform
+ Binary,
+
+ /// Represents a version from the command line (via `volta run`)
+ CommandLine,
+}
+
+impl fmt::Display for Source {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Source::Default => write!(f, "default"),
+ Source::Project => write!(f, "project"),
+ Source::Binary => write!(f, "binary"),
+ Source::CommandLine => write!(f, "command-line"),
+ }
+ }
+}
+
+pub struct Sourced<T> {
+ pub value: T,
+ pub source: Source,
+}
+
+impl<T> Sourced<T> {
+ pub fn with_default(value: T) -> Self {
+ Sourced {
+ value,
+ source: Source::Default,
+ }
+ }
+
+ pub fn with_project(value: T) -> Self {
+ Sourced {
+ value,
+ source: Source::Project,
+ }
+ }
+
+ pub fn with_binary(value: T) -> Self {
+ Sourced {
+ value,
+ source: Source::Binary,
+ }
+ }
+
+ pub fn with_command_line(value: T) -> Self {
+ Sourced {
+ value,
+ source: Source::CommandLine,
+ }
+ }
+}
+
+impl<T> Sourced<T> {
+ pub fn as_ref(&self) -> Sourced<&T> {
+ Sourced {
+ value: &self.value,
+ source: self.source,
+ }
+ }
+}
+
+impl<'a, T> Sourced<&'a T>
+where
+ T: Clone,
+{
+ pub fn cloned(self) -> Sourced<T> {
+ Sourced {
+ value: self.value.clone(),
+ source: self.source,
+ }
+ }
+}
+
+impl<T> Clone for Sourced<T>
+where
+ T: Clone,
+{
+ fn clone(&self) -> Sourced<T> {
+ Sourced {
+ value: self.value.clone(),
+ source: self.source,
+ }
+ }
+}
+
+/// Represents 3 possible states: Having a value, not having a value, and inheriting a value
+#[cfg_attr(test, derive(Eq, PartialEq, Debug))]
+#[derive(Clone, Default)]
+pub enum InheritOption<T> {
+ Some(T),
+ None,
+ #[default]
+ Inherit,
+}
+
+impl<T> InheritOption<T> {
+ /// Applies a function to the contained value (if any)
+ pub fn map<U, F>(self, f: F) -> InheritOption<U>
+ where
+ F: FnOnce(T) -> U,
+ {
+ match self {
+ InheritOption::Some(value) => InheritOption::Some(f(value)),
+ InheritOption::None => InheritOption::None,
+ InheritOption::Inherit => InheritOption::Inherit,
+ }
+ }
+
+ /// Converts the `InheritOption` into a regular `Option` by inheriting from the provided value if needed
+ pub fn inherit(self, other: Option<T>) -> Option<T> {
+ match self {
+ InheritOption::Some(value) => Some(value),
+ InheritOption::None => None,
+ InheritOption::Inherit => other,
+ }
+ }
+}
+
+impl<T> From<InheritOption<T>> for Option<T> {
+ fn from(base: InheritOption<T>) -> Option<T> {
+ base.inherit(None)
+ }
+}
+
+#[derive(Clone, PartialOrd, Ord, PartialEq, Eq)]
+#[cfg_attr(test, derive(Debug))]
+/// Represents the specification of a single Platform, regardless of the source
+pub struct PlatformSpec {
+ pub node: Version,
+ pub npm: Option<Version>,
+ pub pnpm: Option<Version>,
+ pub yarn: Option<Version>,
+}
+
+impl PlatformSpec {
+ /// Convert this PlatformSpec into a Platform with all sources set to `Default`
+ pub fn as_default(&self) -> Platform {
+ Platform {
+ node: Sourced::with_default(self.node.clone()),
+ npm: self.npm.clone().map(Sourced::with_default),
+ pnpm: self.pnpm.clone().map(Sourced::with_default),
+ yarn: self.yarn.clone().map(Sourced::with_default),
+ }
+ }
+
+ /// Convert this PlatformSpec into a Platform with all sources set to `Project`
+ pub fn as_project(&self) -> Platform {
+ Platform {
+ node: Sourced::with_project(self.node.clone()),
+ npm: self.npm.clone().map(Sourced::with_project),
+ pnpm: self.pnpm.clone().map(Sourced::with_project),
+ yarn: self.yarn.clone().map(Sourced::with_project),
+ }
+ }
+
+ /// Convert this PlatformSpec into a Platform with all sources set to `Binary`
+ pub fn as_binary(&self) -> Platform {
+ Platform {
+ node: Sourced::with_binary(self.node.clone()),
+ npm: self.npm.clone().map(Sourced::with_binary),
+ pnpm: self.pnpm.clone().map(Sourced::with_binary),
+ yarn: self.yarn.clone().map(Sourced::with_binary),
+ }
+ }
+}
+
+/// Represents a (maybe) platform with values from the command line
+#[derive(Clone)]
+pub struct CliPlatform {
+ pub node: Option<Version>,
+ pub npm: InheritOption<Version>,
+ pub pnpm: InheritOption<Version>,
+ pub yarn: InheritOption<Version>,
+}
+
+impl CliPlatform {
+ /// Merges the `CliPlatform` with a `Platform`, inheriting from the base where needed
+ pub fn merge(self, base: Platform) -> Platform {
+ Platform {
+ node: self.node.map_or(base.node, Sourced::with_command_line),
+ npm: self.npm.map(Sourced::with_command_line).inherit(base.npm),
+ pnpm: self.pnpm.map(Sourced::with_command_line).inherit(base.pnpm),
+ yarn: self.yarn.map(Sourced::with_command_line).inherit(base.yarn),
+ }
+ }
+}
+
+impl From<CliPlatform> for Option<Platform> {
+ /// Converts the `CliPlatform` into a possible Platform without a base from which to inherit
+ fn from(base: CliPlatform) -> Option<Platform> {
+ match base.node {
+ None => None,
+ Some(node) => Some(Platform {
+ node: Sourced::with_command_line(node),
+ npm: base.npm.map(Sourced::with_command_line).into(),
+ pnpm: base.pnpm.map(Sourced::with_command_line).into(),
+ yarn: base.yarn.map(Sourced::with_command_line).into(),
+ }),
+ }
+ }
+}
+
+/// Represents a real Platform, with Versions pulled from one or more `PlatformSpec`s
+#[derive(Clone)]
+pub struct Platform {
+ pub node: Sourced<Version>,
+ pub npm: Option<Sourced<Version>>,
+ pub pnpm: Option<Sourced<Version>>,
+ pub yarn: Option<Sourced<Version>>,
+}
+
+impl Platform {
+ /// Returns the user's currently active platform, if any
+ ///
+ /// Active platform is determined by first looking at the Project Platform
+ ///
+ /// - If there is a project platform then we use it
+ /// - If there is no pnpm/Yarn version in the project platform, we pull
+ /// pnpm/Yarn from the default platform if available, and merge the two
+ /// platforms into a final one
+ /// - If there is no Project platform, then we use the user Default Platform
+ pub fn current(session: &mut Session) -> Fallible<Option<Self>> {
+ if let Some(mut platform) = session.project_platform()?.map(PlatformSpec::as_project) {
+ if platform.pnpm.is_none() {
+ platform.pnpm = session
+ .default_platform()?
+ .and_then(|default_platform| default_platform.pnpm.clone())
+ .map(Sourced::with_default);
+ }
+
+ if platform.yarn.is_none() {
+ platform.yarn = session
+ .default_platform()?
+ .and_then(|default_platform| default_platform.yarn.clone())
+ .map(Sourced::with_default);
+ }
+
+ Ok(Some(platform))
+ } else {
+ Ok(session.default_platform()?.map(PlatformSpec::as_default))
+ }
+ }
+
+ /// Check out a `Platform` into a fully-realized `Image`
+ ///
+ /// This will ensure that all necessary tools are fetched and available for execution
+ pub fn checkout(self, session: &mut Session) -> Fallible<Image> {
+ Node::new(self.node.value.clone()).ensure_fetched(session)?;
+
+ if let Some(Sourced { value: version, .. }) = &self.npm {
+ Npm::new(version.clone()).ensure_fetched(session)?;
+ }
+
+ // Only force download of the pnpm version if the pnpm feature flag is set. If it isn't,
+ // then we won't be using the `Pnpm` tool to execute (we will be relying on the global
+ // package logic), so fetching the Pnpm version would only be redundant work.
+ if env::var_os(VOLTA_FEATURE_PNPM).is_some() {
+ if let Some(Sourced { value: version, .. }) = &self.pnpm {
+ Pnpm::new(version.clone()).ensure_fetched(session)?;
+ }
+ }
+
+ if let Some(Sourced { value: version, .. }) = &self.yarn {
+ Yarn::new(version.clone()).ensure_fetched(session)?;
+ }
+
+ Ok(Image {
+ node: self.node,
+ npm: self.npm,
+ pnpm: self.pnpm,
+ yarn: self.yarn,
+ })
+ }
+}
+
+fn build_path_error() -> ErrorKind {
+ ErrorKind::BuildPathError
+}
+
use std::ffi::OsString;
+
+use super::build_path_error;
+use crate::error::{Context, Fallible};
+use crate::layout::env_paths;
+
+/// A lightweight namespace type representing the system environment, i.e. the environment
+/// with Volta removed.
+pub struct System;
+
+impl System {
+ /// Produces a modified version of the current `PATH` environment variable that
+ /// removes the Volta shims and binaries, to use for running system node and
+ /// executables.
+ pub fn path() -> Fallible<OsString> {
+ let old_path = envoy::path().unwrap_or_else(|| envoy::Var::from(""));
+ let mut new_path = old_path.split();
+
+ for remove_path in env_paths()? {
+ new_path = new_path.remove(remove_path);
+ }
+
+ new_path.join().with_context(build_path_error)
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +
//! Provides the `Project` type, which represents a Node project tree in
+//! the filesystem.
+
+use std::env;
+use std::ffi::OsStr;
+use std::iter::once;
+use std::path::{Path, PathBuf};
+
+use node_semver::Version;
+use once_cell::unsync::OnceCell;
+
+use crate::error::{Context, ErrorKind, Fallible, VoltaError};
+use crate::layout::volta_home;
+use crate::platform::PlatformSpec;
+use crate::tool::BinConfig;
+use chain_map::ChainMap;
+use indexmap::IndexSet;
+
+mod serial;
+#[cfg(test)]
+mod tests;
+
+use serial::{update_manifest, Manifest, ManifestKey};
+
+/// A lazily loaded Project
+pub struct LazyProject {
+ project: OnceCell<Option<Project>>,
+}
+
+impl LazyProject {
+ pub fn init() -> Self {
+ LazyProject {
+ project: OnceCell::new(),
+ }
+ }
+
+ pub fn get(&self) -> Fallible<Option<&Project>> {
+ let project = self.project.get_or_try_init(Project::for_current_dir)?;
+ Ok(project.as_ref())
+ }
+
+ pub fn get_mut(&mut self) -> Fallible<Option<&mut Project>> {
+ let _ = self.project.get_or_try_init(Project::for_current_dir)?;
+ Ok(self.project.get_mut().unwrap().as_mut())
+ }
+}
+
+/// A Node project workspace in the filesystem
+#[cfg_attr(test, derive(Debug))]
+pub struct Project {
+ manifest_file: PathBuf,
+ workspace_manifests: IndexSet<PathBuf>,
+ dependencies: ChainMap<String, String>,
+ platform: Option<PlatformSpec>,
+}
+
+impl Project {
+ /// Creates an optional Project instance from the current directory
+ fn for_current_dir() -> Fallible<Option<Self>> {
+ let current_dir = env::current_dir().with_context(|| ErrorKind::CurrentDirError)?;
+ Self::for_dir(current_dir)
+ }
+
+ /// Creates an optional Project instance from the specified directory
+ ///
+ /// Will search ancestors to find a `package.json` and use that as the root of the project
+ fn for_dir(base_dir: PathBuf) -> Fallible<Option<Self>> {
+ match find_closest_root(base_dir) {
+ Some(mut project) => {
+ project.push("package.json");
+ Self::from_file(project).map(Some)
+ }
+ None => Ok(None),
+ }
+ }
+
+ /// Creates a Project instance from the given package manifest file (`package.json`)
+ fn from_file(manifest_file: PathBuf) -> Fallible<Self> {
+ let manifest = Manifest::from_file(&manifest_file)?;
+ let mut dependencies: ChainMap<String, String> = manifest.dependency_maps.collect();
+ let mut workspace_manifests = IndexSet::new();
+ let mut platform = manifest.platform;
+ let mut extends = manifest.extends;
+
+ // Iterate the `volta.extends` chain, parsing each file in turn
+ while let Some(path) = extends {
+ // Detect cycles to prevent infinite looping
+ if path == manifest_file || workspace_manifests.contains(&path) {
+ let mut paths = vec![manifest_file];
+ paths.extend(workspace_manifests);
+
+ return Err(ErrorKind::ExtensionCycleError {
+ paths,
+ duplicate: path,
+ }
+ .into());
+ }
+
+ let manifest = Manifest::from_file(&path)?;
+ workspace_manifests.insert(path);
+ dependencies.extend(manifest.dependency_maps);
+
+ platform = match (platform, manifest.platform) {
+ (Some(base), Some(ext)) => Some(base.merge(ext)),
+ (Some(plat), None) | (None, Some(plat)) => Some(plat),
+ (None, None) => None,
+ };
+
+ extends = manifest.extends;
+ }
+
+ let platform = platform.map(TryInto::try_into).transpose()?;
+
+ Ok(Project {
+ manifest_file,
+ workspace_manifests,
+ dependencies,
+ platform,
+ })
+ }
+
+ /// Returns a reference to the manifest file for the current project
+ pub fn manifest_file(&self) -> &Path {
+ &self.manifest_file
+ }
+
+ /// Returns an iterator of paths to all of the workspace roots
+ pub fn workspace_roots(&self) -> impl Iterator<Item = &Path> {
+ // Invariant: self.manifest_file and self.extensions will only contain paths to files that we successfully loaded
+ once(&self.manifest_file)
+ .chain(self.workspace_manifests.iter())
+ .map(|file| file.parent().expect("File paths always have a parent"))
+ }
+
+ /// Returns a reference to the Project's `PlatformSpec`, if available
+ pub fn platform(&self) -> Option<&PlatformSpec> {
+ self.platform.as_ref()
+ }
+
+ /// Returns true if the project dependency map contains the specified dependency
+ pub fn has_direct_dependency(&self, dependency: &str) -> bool {
+ self.dependencies.contains_key(dependency)
+ }
+
+ /// Returns true if the input binary name is a direct dependency of the input project
+ pub fn has_direct_bin(&self, bin_name: &OsStr) -> Fallible<bool> {
+ if let Some(name) = bin_name.to_str() {
+ let config_path = volta_home()?.default_tool_bin_config(name);
+
+ return match BinConfig::from_file_if_exists(config_path)? {
+ None => Ok(false),
+ Some(config) => Ok(self.has_direct_dependency(&config.package)),
+ };
+ }
+ Ok(false)
+ }
+
+ /// Searches the project roots to find the path to a project-local binary file
+ pub fn find_bin<P: AsRef<Path>>(&self, bin_name: P) -> Option<PathBuf> {
+ self.workspace_roots().find_map(|root| {
+ let mut bin_path = root.join("node_modules");
+ bin_path.push(".bin");
+ bin_path.push(&bin_name);
+
+ if bin_path.is_file() {
+ Some(bin_path)
+ } else {
+ None
+ }
+ })
+ }
+
+ /// Yarn projects that are using PnP or pnpm linker need to use yarn run.
+ // (project uses Yarn berry if 'yarnrc.yml' exists, uses PnP if '.pnp.js' or '.pnp.cjs' exist)
+ pub fn needs_yarn_run(&self) -> bool {
+ self.platform()
+ .is_some_and(|platform| platform.yarn.is_some())
+ && self.workspace_roots().any(|x| {
+ x.join(".yarnrc.yml").exists()
+ || x.join(".pnp.cjs").exists()
+ || x.join(".pnp.js").exists()
+ })
+ }
+
+ /// Pins the Node version in this project's manifest file
+ pub fn pin_node(&mut self, version: Version) -> Fallible<()> {
+ update_manifest(&self.manifest_file, ManifestKey::Node, Some(&version))?;
+
+ if let Some(platform) = self.platform.as_mut() {
+ platform.node = version;
+ } else {
+ self.platform = Some(PlatformSpec {
+ node: version,
+ npm: None,
+ pnpm: None,
+ yarn: None,
+ });
+ }
+
+ Ok(())
+ }
+
+ /// Pins the npm version in this project's manifest file
+ pub fn pin_npm(&mut self, version: Option<Version>) -> Fallible<()> {
+ if let Some(platform) = self.platform.as_mut() {
+ update_manifest(&self.manifest_file, ManifestKey::Npm, version.as_ref())?;
+
+ platform.npm = version;
+
+ Ok(())
+ } else {
+ Err(ErrorKind::NoPinnedNodeVersion { tool: "npm".into() }.into())
+ }
+ }
+
+ /// Pins the pnpm version in this project's manifest file
+ pub fn pin_pnpm(&mut self, version: Option<Version>) -> Fallible<()> {
+ if let Some(platform) = self.platform.as_mut() {
+ update_manifest(&self.manifest_file, ManifestKey::Pnpm, version.as_ref())?;
+
+ platform.pnpm = version;
+
+ Ok(())
+ } else {
+ Err(ErrorKind::NoPinnedNodeVersion {
+ tool: "pnpm".into(),
+ }
+ .into())
+ }
+ }
+
+ /// Pins the Yarn version in this project's manifest file
+ pub fn pin_yarn(&mut self, version: Option<Version>) -> Fallible<()> {
+ if let Some(platform) = self.platform.as_mut() {
+ update_manifest(&self.manifest_file, ManifestKey::Yarn, version.as_ref())?;
+
+ platform.yarn = version;
+
+ Ok(())
+ } else {
+ Err(ErrorKind::NoPinnedNodeVersion {
+ tool: "Yarn".into(),
+ }
+ .into())
+ }
+ }
+}
+
+fn is_node_root(dir: &Path) -> bool {
+ dir.join("package.json").exists()
+}
+
+fn is_node_modules(dir: &Path) -> bool {
+ dir.file_name().is_some_and(|tail| tail == "node_modules")
+}
+
+fn is_dependency(dir: &Path) -> bool {
+ dir.parent().is_some_and(is_node_modules)
+}
+
+fn is_project_root(dir: &Path) -> bool {
+ is_node_root(dir) && !is_dependency(dir)
+}
+
+/// Starts at `base_dir` and walks up the directory tree until a package.json file is found
+pub(crate) fn find_closest_root(mut dir: PathBuf) -> Option<PathBuf> {
+ while !is_project_root(&dir) {
+ if !dir.pop() {
+ return None;
+ }
+ }
+
+ Some(dir)
+}
+
+struct PartialPlatform {
+ node: Option<Version>,
+ npm: Option<Version>,
+ pnpm: Option<Version>,
+ yarn: Option<Version>,
+}
+
+impl PartialPlatform {
+ fn merge(self, other: PartialPlatform) -> PartialPlatform {
+ PartialPlatform {
+ node: self.node.or(other.node),
+ npm: self.npm.or(other.npm),
+ pnpm: self.pnpm.or(other.pnpm),
+ yarn: self.yarn.or(other.yarn),
+ }
+ }
+}
+
+impl TryFrom<PartialPlatform> for PlatformSpec {
+ type Error = VoltaError;
+
+ fn try_from(partial: PartialPlatform) -> Fallible<PlatformSpec> {
+ let node = partial.node.ok_or(ErrorKind::NoProjectNodeInManifest)?;
+
+ Ok(PlatformSpec {
+ node,
+ npm: partial.npm,
+ pnpm: partial.pnpm,
+ yarn: partial.yarn,
+ })
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +
use std::collections::HashMap;
+use std::fmt;
+use std::fs::{read_to_string, File};
+use std::io::Write;
+use std::path::{Path, PathBuf};
+
+use super::PartialPlatform;
+use crate::error::{Context, ErrorKind, Fallible};
+use crate::version::parse_version;
+use dunce::canonicalize;
+use node_semver::Version;
+use serde::{Deserialize, Serialize};
+use serde_json::{Map, Value};
+
+pub type DependencyMapIterator = std::iter::Chain<
+ std::option::IntoIter<HashMap<String, String>>,
+ std::option::IntoIter<HashMap<String, String>>,
+>;
+
+pub(super) struct Manifest {
+ pub dependency_maps: DependencyMapIterator,
+ pub platform: Option<PartialPlatform>,
+ pub extends: Option<PathBuf>,
+}
+
+impl Manifest {
+ pub fn from_file(file: &Path) -> Fallible<Self> {
+ let raw = RawManifest::from_file(file)?;
+
+ let dependency_maps = raw.dependencies.into_iter().chain(raw.dev_dependencies);
+
+ let (platform, extends) = match raw.volta {
+ Some(toolchain) => {
+ let (partial, extends) = toolchain.parse_split()?;
+
+ let next = extends
+ .map(|path| {
+ // Invariant: Since we successfully parsed it, we know we have a path to a file
+ let unresolved = file
+ .parent()
+ .expect("File paths always have a parent")
+ .join(&path);
+ canonicalize(unresolved)
+ .with_context(|| ErrorKind::ExtensionPathError { path })
+ })
+ .transpose()?;
+ (Some(partial), next)
+ }
+ None => (None, None),
+ };
+
+ Ok(Manifest {
+ dependency_maps,
+ platform,
+ extends,
+ })
+ }
+}
+
+pub(super) enum ManifestKey {
+ Node,
+ Npm,
+ Pnpm,
+ Yarn,
+}
+
+impl fmt::Display for ManifestKey {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.write_str(match self {
+ ManifestKey::Node => "node",
+ ManifestKey::Npm => "npm",
+ ManifestKey::Pnpm => "pnpm",
+ ManifestKey::Yarn => "yarn",
+ })
+ }
+}
+
+/// Updates the `volta` hash in the specified manifest with the given key and value
+///
+/// Will create the `volta` hash if it isn't already present
+///
+/// If the value is `None`, will remove the key from the hash
+pub(super) fn update_manifest(
+ file: &Path,
+ key: ManifestKey,
+ value: Option<&Version>,
+) -> Fallible<()> {
+ let contents = read_to_string(file).with_context(|| ErrorKind::PackageReadError {
+ file: file.to_owned(),
+ })?;
+
+ let mut manifest: serde_json::Value =
+ serde_json::from_str(&contents).with_context(|| ErrorKind::PackageParseError {
+ file: file.to_owned(),
+ })?;
+
+ let root = manifest
+ .as_object_mut()
+ .ok_or_else(|| ErrorKind::PackageParseError {
+ file: file.to_owned(),
+ })?;
+
+ let key = key.to_string();
+
+ match (value, root.get_mut("volta").and_then(|v| v.as_object_mut())) {
+ (Some(v), Some(hash)) => {
+ hash.insert(key, Value::String(v.to_string()));
+ }
+ (None, Some(hash)) => {
+ hash.remove(&key);
+ }
+ (Some(v), None) => {
+ let mut map = Map::new();
+ map.insert(key, Value::String(v.to_string()));
+ root.insert("volta".into(), Value::Object(map));
+ }
+ (None, None) => {}
+ }
+
+ let indent = detect_indent::detect_indent(&contents);
+ let mut output = File::create(file).with_context(|| ErrorKind::PackageWriteError {
+ file: file.to_owned(),
+ })?;
+ let formatter = serde_json::ser::PrettyFormatter::with_indent(indent.indent().as_bytes());
+ let mut ser = serde_json::Serializer::with_formatter(&output, formatter);
+ manifest
+ .serialize(&mut ser)
+ .with_context(|| ErrorKind::PackageWriteError {
+ file: file.to_owned(),
+ })?;
+
+ if contents.ends_with('\n') {
+ writeln!(output).with_context(|| ErrorKind::PackageWriteError {
+ file: file.to_owned(),
+ })?;
+ }
+
+ Ok(())
+}
+
+#[derive(Deserialize)]
+struct RawManifest {
+ dependencies: Option<HashMap<String, String>>,
+
+ #[serde(rename = "devDependencies")]
+ dev_dependencies: Option<HashMap<String, String>>,
+
+ volta: Option<ToolchainSpec>,
+}
+
+impl RawManifest {
+ fn from_file(package: &Path) -> Fallible<Self> {
+ let file = File::open(package).with_context(|| ErrorKind::PackageReadError {
+ file: package.to_owned(),
+ })?;
+
+ serde_json::de::from_reader(file).with_context(|| ErrorKind::PackageParseError {
+ file: package.to_owned(),
+ })
+ }
+}
+
+#[derive(Default, Deserialize, Serialize)]
+struct ToolchainSpec {
+ #[serde(skip_serializing_if = "Option::is_none")]
+ node: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ npm: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pnpm: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ yarn: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ extends: Option<PathBuf>,
+}
+
+impl ToolchainSpec {
+ /// Moves the tool versions into a `PartialPlatform` and returns that along with the `extends` value
+ fn parse_split(self) -> Fallible<(PartialPlatform, Option<PathBuf>)> {
+ let node = self.node.map(parse_version).transpose()?;
+ let npm = self.npm.map(parse_version).transpose()?;
+ let pnpm = self.pnpm.map(parse_version).transpose()?;
+ let yarn = self.yarn.map(parse_version).transpose()?;
+
+ let platform = PartialPlatform {
+ node,
+ npm,
+ pnpm,
+ yarn,
+ };
+
+ Ok((platform, self.extends))
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +
use std::env;
+use std::ffi::{OsStr, OsString};
+use std::path::PathBuf;
+
+use super::executor::{Executor, ToolCommand, ToolKind};
+use super::{debug_active_image, debug_no_platform};
+use crate::error::{Context, ErrorKind, Fallible};
+use crate::layout::volta_home;
+use crate::platform::{Platform, Sourced, System};
+use crate::session::Session;
+use crate::tool::package::BinConfig;
+use log::debug;
+
+/// Determine the correct command to run for a 3rd-party binary
+///
+/// Will detect if we should delegate to the project-local version or use the default version
+pub(super) fn command(exe: &OsStr, args: &[OsString], session: &mut Session) -> Fallible<Executor> {
+ let bin = exe.to_string_lossy().to_string();
+ // First try to use the project toolchain
+ if let Some(project) = session.project()? {
+ // Check if the executable is a direct dependency
+ if project.has_direct_bin(exe)? {
+ match project.find_bin(exe) {
+ Some(path_to_bin) => {
+ debug!("Found {} in project at '{}'", bin, path_to_bin.display());
+
+ let platform = Platform::current(session)?;
+ return Ok(ToolCommand::new(
+ path_to_bin,
+ args,
+ platform,
+ ToolKind::ProjectLocalBinary(bin),
+ )
+ .into());
+ }
+ None => {
+ if project.needs_yarn_run() {
+ debug!(
+ "Project needs to use yarn to run command, calling {} with 'yarn'",
+ bin
+ );
+ let platform = Platform::current(session)?;
+ let mut exe_and_args = vec![exe.to_os_string()];
+ exe_and_args.extend_from_slice(args);
+ return Ok(ToolCommand::new(
+ "yarn",
+ exe_and_args,
+ platform,
+ ToolKind::Yarn,
+ )
+ .into());
+ } else {
+ return Err(ErrorKind::ProjectLocalBinaryNotFound {
+ command: exe.to_string_lossy().to_string(),
+ }
+ .into());
+ }
+ }
+ }
+ }
+ }
+
+ // Try to use the default toolchain
+ if let Some(default_tool) = DefaultBinary::from_name(exe, session)? {
+ debug!(
+ "Found default {} in '{}'",
+ bin,
+ default_tool.bin_path.display()
+ );
+
+ let mut command = ToolCommand::new(
+ default_tool.bin_path,
+ args,
+ Some(default_tool.platform),
+ ToolKind::DefaultBinary(bin),
+ );
+ command.env("NODE_PATH", shared_module_path()?);
+
+ return Ok(command.into());
+ }
+
+ // At this point, the binary is not known to Volta, so we have no platform to use to execute it
+ // This should be rare, as anything we have a shim for should have a config file to load
+ Ok(ToolCommand::new(exe, args, None, ToolKind::DefaultBinary(bin)).into())
+}
+
+/// Determine the execution context (PATH and failure error message) for a project-local binary
+pub(super) fn local_execution_context(
+ tool: String,
+ platform: Option<Platform>,
+ session: &mut Session,
+) -> Fallible<(OsString, ErrorKind)> {
+ match platform {
+ Some(plat) => {
+ let image = plat.checkout(session)?;
+ let path = image.path()?;
+ debug_active_image(&image);
+
+ Ok((
+ path,
+ ErrorKind::ProjectLocalBinaryExecError { command: tool },
+ ))
+ }
+ None => {
+ let path = System::path()?;
+ debug_no_platform();
+
+ Ok((path, ErrorKind::NoPlatform))
+ }
+ }
+}
+
+/// Determine the execution context (PATH and failure error message) for a default binary
+pub(super) fn default_execution_context(
+ tool: String,
+ platform: Option<Platform>,
+ session: &mut Session,
+) -> Fallible<(OsString, ErrorKind)> {
+ match platform {
+ Some(plat) => {
+ let image = plat.checkout(session)?;
+ let path = image.path()?;
+ debug_active_image(&image);
+
+ Ok((path, ErrorKind::BinaryExecError))
+ }
+ None => {
+ let path = System::path()?;
+ debug_no_platform();
+
+ Ok((path, ErrorKind::BinaryNotFound { name: tool }))
+ }
+ }
+}
+
+/// Information about the location and execution context of default binaries
+///
+/// Fetched from the config files in the Volta directory, represents the binary that is executed
+/// when the user is outside of a project that has the given bin as a dependency.
+pub struct DefaultBinary {
+ pub bin_path: PathBuf,
+ pub platform: Platform,
+}
+
+impl DefaultBinary {
+ pub fn from_config(bin_config: BinConfig, session: &mut Session) -> Fallible<Self> {
+ let package_dir = volta_home()?.package_image_dir(&bin_config.package);
+ let mut bin_path = bin_config.manager.binary_dir(package_dir);
+ bin_path.push(&bin_config.name);
+
+ // If the user does not have yarn set in the platform for this binary, use the default
+ // This is necessary because some tools (e.g. ember-cli with the `--yarn` option) invoke `yarn`
+ let yarn = match bin_config.platform.yarn {
+ Some(yarn) => Some(yarn),
+ None => session
+ .default_platform()?
+ .and_then(|plat| plat.yarn.clone()),
+ };
+ let platform = Platform {
+ node: Sourced::with_binary(bin_config.platform.node),
+ npm: bin_config.platform.npm.map(Sourced::with_binary),
+ pnpm: bin_config.platform.pnpm.map(Sourced::with_binary),
+ yarn: yarn.map(Sourced::with_binary),
+ };
+
+ Ok(DefaultBinary { bin_path, platform })
+ }
+
+ /// Load information about a default binary by name, if available
+ ///
+ /// A `None` response here means that the tool information couldn't be found. Either the tool
+ /// name is not a valid UTF-8 string, or the tool config doesn't exist.
+ pub fn from_name(tool_name: &OsStr, session: &mut Session) -> Fallible<Option<Self>> {
+ let bin_config_file = match tool_name.to_str() {
+ Some(name) => volta_home()?.default_tool_bin_config(name),
+ None => return Ok(None),
+ };
+
+ match BinConfig::from_file_if_exists(bin_config_file)? {
+ Some(config) => DefaultBinary::from_config(config, session).map(Some),
+ None => Ok(None),
+ }
+ }
+}
+
+/// Determine the value for NODE_PATH, with the shared lib directory prepended
+///
+/// This will ensure that global bins can `require` other global libs
+fn shared_module_path() -> Fallible<OsString> {
+ let node_path = match env::var("NODE_PATH") {
+ Ok(path) => envoy::Var::from(path),
+ Err(_) => envoy::Var::from(""),
+ };
+
+ node_path
+ .split()
+ .prefix_entry(volta_home()?.shared_lib_root())
+ .join()
+ .with_context(|| ErrorKind::BuildPathError)
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402 +403 +404 +405 +406 +407 +408 +409 +410 +411 +412 +413 +414 +415 +416 +417 +418 +419 +420 +421 +422 +423 +424 +425 +426 +427 +428 +429 +430 +431 +432 +433 +434 +435 +436 +437 +438 +439 +440 +441 +442 +443 +444 +445 +446 +447 +448 +449 +450 +451 +452 +453 +454 +455 +456 +457 +458 +459 +460 +461 +462 +463 +464 +465 +466 +467 +468 +469 +470 +471 +472 +473 +474 +475 +476 +477 +478 +479 +480 +481 +482 +483 +484 +485 +486 +487 +488 +489 +490 +491 +492 +493 +494 +495 +496 +497 +498 +499 +500 +501 +502 +503 +504 +505 +506 +507 +508 +509 +510 +511 +512 +513 +514 +515 +516 +517 +518 +519 +520 +521 +522 +523 +524 +525 +526 +527 +528 +529 +530 +531 +532 +533 +534 +535 +536 +537 +538 +539 +540 +541 +542 +543 +544 +545 +546 +547 +548 +549 +550 +551 +552 +553 +554 +555 +556 +557 +558 +559 +560 +561 +562 +563 +
use std::collections::HashMap;
+use std::ffi::OsStr;
+#[cfg(unix)]
+use std::os::unix::process::ExitStatusExt;
+#[cfg(windows)]
+use std::os::windows::process::ExitStatusExt;
+use std::process::{Command, ExitStatus};
+
+use super::RECURSION_ENV_VAR;
+use crate::command::create_command;
+use crate::error::{Context, ErrorKind, Fallible};
+use crate::layout::volta_home;
+use crate::platform::{CliPlatform, Platform, System};
+use crate::session::Session;
+use crate::signal::pass_control_to_shim;
+use crate::style::{note_prefix, tool_version};
+use crate::sync::VoltaLock;
+use crate::tool::package::{DirectInstall, InPlaceUpgrade, PackageConfig, PackageManager};
+use crate::tool::Spec;
+use log::{info, warn};
+
+pub enum Executor {
+ Tool(Box<ToolCommand>),
+ PackageInstall(Box<PackageInstallCommand>),
+ PackageLink(Box<PackageLinkCommand>),
+ PackageUpgrade(Box<PackageUpgradeCommand>),
+ InternalInstall(Box<InternalInstallCommand>),
+ Uninstall(Box<UninstallCommand>),
+ Multiple(Vec<Executor>),
+}
+
+impl Executor {
+ pub fn envs<K, V, S>(&mut self, envs: &HashMap<K, V, S>)
+ where
+ K: AsRef<OsStr>,
+ V: AsRef<OsStr>,
+ {
+ match self {
+ Executor::Tool(cmd) => cmd.envs(envs),
+ Executor::PackageInstall(cmd) => cmd.envs(envs),
+ Executor::PackageLink(cmd) => cmd.envs(envs),
+ Executor::PackageUpgrade(cmd) => cmd.envs(envs),
+ // Internal installs use Volta's logic and don't rely on the environment variables
+ Executor::InternalInstall(_) => {}
+ // Uninstalls use Volta's logic and don't rely on environment variables
+ Executor::Uninstall(_) => {}
+ Executor::Multiple(executors) => {
+ for exe in executors {
+ exe.envs(envs);
+ }
+ }
+ }
+ }
+
+ pub fn cli_platform(&mut self, cli: CliPlatform) {
+ match self {
+ Executor::Tool(cmd) => cmd.cli_platform(cli),
+ Executor::PackageInstall(cmd) => cmd.cli_platform(cli),
+ Executor::PackageLink(cmd) => cmd.cli_platform(cli),
+ Executor::PackageUpgrade(cmd) => cmd.cli_platform(cli),
+ // Internal installs use Volta's logic and don't rely on the Node platform
+ Executor::InternalInstall(_) => {}
+ // Uninstall use Volta's logic and don't rely on the Node platform
+ Executor::Uninstall(_) => {}
+ Executor::Multiple(executors) => {
+ for exe in executors {
+ exe.cli_platform(cli.clone());
+ }
+ }
+ }
+ }
+
+ pub fn execute(self, session: &mut Session) -> Fallible<ExitStatus> {
+ match self {
+ Executor::Tool(cmd) => cmd.execute(session),
+ Executor::PackageInstall(cmd) => cmd.execute(session),
+ Executor::PackageLink(cmd) => cmd.execute(session),
+ Executor::PackageUpgrade(cmd) => cmd.execute(session),
+ Executor::InternalInstall(cmd) => cmd.execute(session),
+ Executor::Uninstall(cmd) => cmd.execute(),
+ Executor::Multiple(executors) => {
+ info!(
+ "{} Volta is processing each package separately",
+ note_prefix()
+ );
+ for exe in executors {
+ let status = exe.execute(session)?;
+ // If any of the sub-commands fail, then we should stop installing and return
+ // that failure.
+ if !status.success() {
+ return Ok(status);
+ }
+ }
+ // If we get here, then all of the sub-commands succeeded, so we should report success
+ Ok(ExitStatus::from_raw(0))
+ }
+ }
+ }
+}
+
+impl From<Vec<Executor>> for Executor {
+ fn from(mut executors: Vec<Executor>) -> Self {
+ if executors.len() == 1 {
+ executors.pop().unwrap()
+ } else {
+ Executor::Multiple(executors)
+ }
+ }
+}
+
+/// Process builder for launching a Volta-managed tool
+///
+/// Tracks the Platform as well as what kind of tool is being executed, to allow individual tools
+/// to customize the behavior before execution.
+pub struct ToolCommand {
+ command: Command,
+ platform: Option<Platform>,
+ kind: ToolKind,
+}
+
+/// The kind of tool being executed, used to determine the correct execution context
+pub enum ToolKind {
+ Node,
+ Npm,
+ Npx,
+ Pnpm,
+ Yarn,
+ ProjectLocalBinary(String),
+ DefaultBinary(String),
+ Bypass(String),
+}
+
+impl ToolCommand {
+ pub fn new<E, A, S>(exe: E, args: A, platform: Option<Platform>, kind: ToolKind) -> Self
+ where
+ E: AsRef<OsStr>,
+ A: IntoIterator<Item = S>,
+ S: AsRef<OsStr>,
+ {
+ let mut command = create_command(exe);
+ command.args(args);
+
+ Self {
+ command,
+ platform,
+ kind,
+ }
+ }
+
+ /// Adds or updates environment variables that the command will use
+ pub fn envs<E, K, V>(&mut self, envs: E)
+ where
+ E: IntoIterator<Item = (K, V)>,
+ K: AsRef<OsStr>,
+ V: AsRef<OsStr>,
+ {
+ self.command.envs(envs);
+ }
+
+ /// Adds or updates a single environment variable that the command will use
+ pub fn env<K, V>(&mut self, key: K, value: V)
+ where
+ K: AsRef<OsStr>,
+ V: AsRef<OsStr>,
+ {
+ self.command.env(key, value);
+ }
+
+ /// Updates the Platform for the command to include values from the command-line
+ pub fn cli_platform(&mut self, cli: CliPlatform) {
+ self.platform = match self.platform.take() {
+ Some(base) => Some(cli.merge(base)),
+ None => cli.into(),
+ };
+ }
+
+ /// Runs the command, returning the `ExitStatus` if it successfully launches
+ pub fn execute(mut self, session: &mut Session) -> Fallible<ExitStatus> {
+ let (path, on_failure) = match self.kind {
+ ToolKind::Node => super::node::execution_context(self.platform, session)?,
+ ToolKind::Npm => super::npm::execution_context(self.platform, session)?,
+ ToolKind::Npx => super::npx::execution_context(self.platform, session)?,
+ ToolKind::Pnpm => super::pnpm::execution_context(self.platform, session)?,
+ ToolKind::Yarn => super::yarn::execution_context(self.platform, session)?,
+ ToolKind::DefaultBinary(bin) => {
+ super::binary::default_execution_context(bin, self.platform, session)?
+ }
+ ToolKind::ProjectLocalBinary(bin) => {
+ super::binary::local_execution_context(bin, self.platform, session)?
+ }
+ ToolKind::Bypass(command) => (System::path()?, ErrorKind::BypassError { command }),
+ };
+
+ self.command.env(RECURSION_ENV_VAR, "1");
+ self.command.env("PATH", path);
+
+ pass_control_to_shim();
+ self.command.status().with_context(|| on_failure)
+ }
+}
+
+impl From<ToolCommand> for Executor {
+ fn from(cmd: ToolCommand) -> Self {
+ Executor::Tool(Box::new(cmd))
+ }
+}
+
+/// Process builder for launching a package install command (e.g. `npm install --global`)
+///
+/// This will use a `DirectInstall` instance to modify the command before running to point it to
+/// the Volta directory. It will also complete the install, writing config files and shims
+pub struct PackageInstallCommand {
+ /// The command that will ultimately be executed
+ command: Command,
+ /// The installer that modifies the command as necessary and provides the completion method
+ installer: DirectInstall,
+ /// The platform to use when running the command.
+ platform: Platform,
+}
+
+impl PackageInstallCommand {
+ pub fn new<A, S>(args: A, platform: Platform, manager: PackageManager) -> Fallible<Self>
+ where
+ A: IntoIterator<Item = S>,
+ S: AsRef<OsStr>,
+ {
+ let installer = DirectInstall::new(manager)?;
+
+ let mut command = match manager {
+ PackageManager::Npm => create_command("npm"),
+ PackageManager::Pnpm => create_command("pnpm"),
+ PackageManager::Yarn => create_command("yarn"),
+ };
+ command.args(args);
+
+ Ok(PackageInstallCommand {
+ command,
+ installer,
+ platform,
+ })
+ }
+
+ pub fn for_npm_link<A, S>(args: A, platform: Platform, name: String) -> Fallible<Self>
+ where
+ A: IntoIterator<Item = S>,
+ S: AsRef<OsStr>,
+ {
+ let installer = DirectInstall::with_name(PackageManager::Npm, name)?;
+
+ let mut command = create_command("npm");
+ command.args(args);
+
+ Ok(PackageInstallCommand {
+ command,
+ installer,
+ platform,
+ })
+ }
+
+ /// Adds or updates environment variables that the command will use
+ pub fn envs<E, K, V>(&mut self, envs: E)
+ where
+ E: IntoIterator<Item = (K, V)>,
+ K: AsRef<OsStr>,
+ V: AsRef<OsStr>,
+ {
+ self.command.envs(envs);
+ }
+
+ /// Updates the Platform for the command to include values from the command-line
+ pub fn cli_platform(&mut self, cli: CliPlatform) {
+ self.platform = cli.merge(self.platform.clone());
+ }
+
+ /// Runs the install command, applying the necessary modifications to install into the Volta
+ /// data directory
+ pub fn execute(mut self, session: &mut Session) -> Fallible<ExitStatus> {
+ let _lock = VoltaLock::acquire();
+ let image = self.platform.checkout(session)?;
+ let path = image.path()?;
+
+ self.command.env(RECURSION_ENV_VAR, "1");
+ self.command.env("PATH", path);
+ self.installer.setup_command(&mut self.command);
+
+ let status = self
+ .command
+ .status()
+ .with_context(|| ErrorKind::BinaryExecError)?;
+
+ if status.success() {
+ self.installer.complete_install(&image)?;
+ }
+
+ Ok(status)
+ }
+}
+
+impl From<PackageInstallCommand> for Executor {
+ fn from(cmd: PackageInstallCommand) -> Self {
+ Executor::PackageInstall(Box::new(cmd))
+ }
+}
+
+/// Process builder for launching a `npm link <package>` command
+///
+/// This will set the appropriate environment variables to ensure that the linked package can be
+/// found.
+pub struct PackageLinkCommand {
+ /// The command that will ultimately be executed
+ command: Command,
+ /// The tool the user wants to link
+ tool: String,
+ /// The platform to use when running the command
+ platform: Platform,
+}
+
+impl PackageLinkCommand {
+ pub fn new<A, S>(args: A, platform: Platform, tool: String) -> Self
+ where
+ A: IntoIterator<Item = S>,
+ S: AsRef<OsStr>,
+ {
+ let mut command = create_command("npm");
+ command.args(args);
+
+ PackageLinkCommand {
+ command,
+ tool,
+ platform,
+ }
+ }
+
+ /// Adds or updates environment variables that the command will use
+ pub fn envs<E, K, V>(&mut self, envs: E)
+ where
+ E: IntoIterator<Item = (K, V)>,
+ K: AsRef<OsStr>,
+ V: AsRef<OsStr>,
+ {
+ self.command.envs(envs);
+ }
+
+ /// Updates the Platform for the command to include values from the command-line
+ pub fn cli_platform(&mut self, cli: CliPlatform) {
+ self.platform = cli.merge(self.platform.clone());
+ }
+
+ /// Runs the link command, applying the necessary modifications to pull from the Volta data
+ /// directory.
+ ///
+ /// This will also check for some common failure cases and alert the user
+ pub fn execute(mut self, session: &mut Session) -> Fallible<ExitStatus> {
+ self.check_linked_package(session)?;
+
+ let image = self.platform.checkout(session)?;
+ let path = image.path()?;
+
+ self.command.env(RECURSION_ENV_VAR, "1");
+ self.command.env("PATH", path);
+ let package_root = volta_home()?.package_image_dir(&self.tool);
+ PackageManager::Npm.setup_global_command(&mut self.command, package_root);
+
+ self.command
+ .status()
+ .with_context(|| ErrorKind::BinaryExecError)
+ }
+
+ /// Check for possible failure cases with the linked package:
+ /// - The package is not found as a global
+ /// - The package exists, but was linked using a different package manager
+ /// - The package is using a different version of Node than the current project (warning)
+ fn check_linked_package(&self, session: &mut Session) -> Fallible<()> {
+ let config =
+ PackageConfig::from_file(volta_home()?.default_package_config_file(&self.tool))
+ .with_context(|| ErrorKind::NpmLinkMissingPackage {
+ package: self.tool.clone(),
+ })?;
+
+ if config.manager != PackageManager::Npm {
+ return Err(ErrorKind::NpmLinkWrongManager {
+ package: self.tool.clone(),
+ }
+ .into());
+ }
+
+ if let Some(platform) = session.project_platform()? {
+ if platform.node.major != config.platform.node.major {
+ warn!(
+ "the current project is using {}, but package '{}' was linked using {}. These might not interact correctly.",
+ tool_version("node", &platform.node),
+ self.tool,
+ tool_version("node", &config.platform.node)
+ );
+ }
+ }
+
+ Ok(())
+ }
+}
+
+impl From<PackageLinkCommand> for Executor {
+ fn from(cmd: PackageLinkCommand) -> Self {
+ Executor::PackageLink(Box::new(cmd))
+ }
+}
+
+/// Process builder for launching a global package upgrade command (e.g. `npm update -g`)
+///
+/// This will use an `InPlaceUpgrade` instance to modify the command and point at the appropriate
+/// image directory. It will also complete the install, writing any updated configs and shims
+pub struct PackageUpgradeCommand {
+ /// The command that will ultimately be executed
+ command: Command,
+ /// Helper utility to modify the command and provide the completion method
+ upgrader: InPlaceUpgrade,
+ /// The platform to run the command under
+ platform: Platform,
+}
+
+impl PackageUpgradeCommand {
+ pub fn new<A, S>(
+ args: A,
+ package: String,
+ platform: Platform,
+ manager: PackageManager,
+ ) -> Fallible<Self>
+ where
+ A: IntoIterator<Item = S>,
+ S: AsRef<OsStr>,
+ {
+ let upgrader = InPlaceUpgrade::new(package, manager)?;
+
+ let mut command = match manager {
+ PackageManager::Npm => create_command("npm"),
+ PackageManager::Pnpm => create_command("pnpm"),
+ PackageManager::Yarn => create_command("yarn"),
+ };
+ command.args(args);
+
+ Ok(PackageUpgradeCommand {
+ command,
+ upgrader,
+ platform,
+ })
+ }
+
+ /// Adds or updates environment variables that the command will use
+ pub fn envs<E, K, V>(&mut self, envs: E)
+ where
+ E: IntoIterator<Item = (K, V)>,
+ K: AsRef<OsStr>,
+ V: AsRef<OsStr>,
+ {
+ self.command.envs(envs);
+ }
+
+ /// Updates the Platform for the command to include values from the command-line
+ pub fn cli_platform(&mut self, cli: CliPlatform) {
+ self.platform = cli.merge(self.platform.clone());
+ }
+
+ /// Runs the upgrade command, applying the necessary modifications to point at the Volta image
+ /// directory
+ ///
+ /// Will also check for common failure cases, such as non-existant package or wrong package
+ /// manager
+ pub fn execute(mut self, session: &mut Session) -> Fallible<ExitStatus> {
+ self.upgrader.check_upgraded_package()?;
+
+ let _lock = VoltaLock::acquire();
+ let image = self.platform.checkout(session)?;
+ let path = image.path()?;
+
+ self.command.env(RECURSION_ENV_VAR, "1");
+ self.command.env("PATH", path);
+ self.upgrader.setup_command(&mut self.command);
+
+ let status = self
+ .command
+ .status()
+ .with_context(|| ErrorKind::BinaryExecError)?;
+
+ if status.success() {
+ self.upgrader.complete_upgrade(&image)?;
+ }
+
+ Ok(status)
+ }
+}
+
+impl From<PackageUpgradeCommand> for Executor {
+ fn from(cmd: PackageUpgradeCommand) -> Self {
+ Executor::PackageUpgrade(Box::new(cmd))
+ }
+}
+
+/// Executor for running an internal install (installing Node, npm, pnpm or Yarn using the `volta
+/// install` logic)
+///
+/// Note: This is not intended to be used for Package installs. Those should go through the
+/// `PackageInstallCommand` above, to more seamlessly integrate with the package manager
+pub struct InternalInstallCommand {
+ tool: Spec,
+}
+
+impl InternalInstallCommand {
+ pub fn new(tool: Spec) -> Self {
+ InternalInstallCommand { tool }
+ }
+
+ /// Runs the install, using Volta's internal install logic for the appropriate tool
+ fn execute(self, session: &mut Session) -> Fallible<ExitStatus> {
+ info!(
+ "{} using Volta to install {}",
+ note_prefix(),
+ self.tool.name()
+ );
+
+ self.tool.resolve(session)?.install(session)?;
+
+ Ok(ExitStatus::from_raw(0))
+ }
+}
+
+impl From<InternalInstallCommand> for Executor {
+ fn from(cmd: InternalInstallCommand) -> Self {
+ Executor::InternalInstall(Box::new(cmd))
+ }
+}
+
+/// Executor for running a tool uninstall command.
+///
+/// This will use the `volta uninstall` logic to correctly ensure that the package is fully
+/// uninstalled
+pub struct UninstallCommand {
+ tool: Spec,
+}
+
+impl UninstallCommand {
+ pub fn new(tool: Spec) -> Self {
+ UninstallCommand { tool }
+ }
+
+ /// Runs the uninstall with Volta's internal uninstall logic
+ fn execute(self) -> Fallible<ExitStatus> {
+ info!(
+ "{} using Volta to uninstall {}",
+ note_prefix(),
+ self.tool.name()
+ );
+
+ self.tool.uninstall()?;
+
+ Ok(ExitStatus::from_raw(0))
+ }
+}
+
+impl From<UninstallCommand> for Executor {
+ fn from(cmd: UninstallCommand) -> Self {
+ Executor::Uninstall(Box::new(cmd))
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +
use std::collections::HashMap;
+use std::env::{self, ArgsOs};
+use std::ffi::{OsStr, OsString};
+use std::path::Path;
+use std::process::ExitStatus;
+
+use crate::error::{ErrorKind, Fallible};
+use crate::platform::{CliPlatform, Image, Sourced};
+use crate::session::Session;
+use crate::VOLTA_FEATURE_PNPM;
+use log::debug;
+use node_semver::Version;
+
+pub mod binary;
+mod executor;
+mod node;
+mod npm;
+mod npx;
+mod parser;
+mod pnpm;
+mod yarn;
+
+/// Environment variable set internally when a shim has been executed and the context evaluated
+///
+/// This is set when executing a shim command. If this is already, then the built-in shims (Node,
+/// npm, npx, pnpm and Yarn) will assume that the context has already been evaluated & the PATH has
+/// already been modified, so they will use the pass-through behavior.
+///
+/// Shims should only be called recursively when the environment is misconfigured, so this will
+/// prevent infinite recursion as the pass-through logic removes the shim directory from the PATH.
+///
+/// Note: This is explicitly _removed_ when calling a command through `volta run`, as that will
+/// never happen due to the Volta environment.
+const RECURSION_ENV_VAR: &str = "_VOLTA_TOOL_RECURSION";
+const VOLTA_BYPASS: &str = "VOLTA_BYPASS";
+
+/// Execute a shim command, based on the command-line arguments to the current process
+pub fn execute_shim(session: &mut Session) -> Fallible<ExitStatus> {
+ let mut native_args = env::args_os();
+ let exe = get_tool_name(&mut native_args)?;
+ let args: Vec<_> = native_args.collect();
+
+ get_executor(&exe, &args, session)?.execute(session)
+}
+
+/// Execute a tool with the provided arguments
+pub fn execute_tool<K, V, S>(
+ exe: &OsStr,
+ args: &[OsString],
+ envs: &HashMap<K, V, S>,
+ cli: CliPlatform,
+ session: &mut Session,
+) -> Fallible<ExitStatus>
+where
+ K: AsRef<OsStr>,
+ V: AsRef<OsStr>,
+{
+ // Remove the recursion environment variable so that the context is correctly re-evaluated
+ // when calling `volta run` (even when called from a Node script)
+ env::remove_var(RECURSION_ENV_VAR);
+
+ let mut runner = get_executor(exe, args, session)?;
+ runner.cli_platform(cli);
+ runner.envs(envs);
+
+ runner.execute(session)
+}
+
+/// Get the appropriate Tool command, based on the requested executable and arguments
+fn get_executor(
+ exe: &OsStr,
+ args: &[OsString],
+ session: &mut Session,
+) -> Fallible<executor::Executor> {
+ if env::var_os(VOLTA_BYPASS).is_some() {
+ Ok(executor::ToolCommand::new(
+ exe,
+ args,
+ None,
+ executor::ToolKind::Bypass(exe.to_string_lossy().to_string()),
+ )
+ .into())
+ } else {
+ match exe.to_str() {
+ Some("volta-shim") => Err(ErrorKind::RunShimDirectly.into()),
+ Some("node") => node::command(args, session),
+ Some("npm") => npm::command(args, session),
+ Some("npx") => npx::command(args, session),
+ Some("pnpm") => {
+ // If the pnpm feature flag variable is set, delegate to the pnpm handler
+ // If not, use the binary handler as a fallback (prior to pnpm support, installing
+ // pnpm would be handled the same as any other global binary)
+ if env::var_os(VOLTA_FEATURE_PNPM).is_some() {
+ pnpm::command(args, session)
+ } else {
+ binary::command(exe, args, session)
+ }
+ }
+ Some("yarn") | Some("yarnpkg") => yarn::command(args, session),
+ _ => binary::command(exe, args, session),
+ }
+ }
+}
+
+/// Determine the name of the command to run by inspecting the first argument to the active process
+fn get_tool_name(args: &mut ArgsOs) -> Fallible<OsString> {
+ args.next()
+ .and_then(|arg0| Path::new(&arg0).file_name().map(tool_name_from_file_name))
+ .ok_or_else(|| ErrorKind::CouldNotDetermineTool.into())
+}
+
+#[cfg(unix)]
+fn tool_name_from_file_name(file_name: &OsStr) -> OsString {
+ file_name.to_os_string()
+}
+
+#[cfg(windows)]
+fn tool_name_from_file_name(file_name: &OsStr) -> OsString {
+ // On Windows PowerShell, the file name includes the .exe suffix,
+ // and the Windows file system is case-insensitive
+ // We need to remove that to get the raw tool name
+ match file_name.to_str() {
+ Some(file) => OsString::from(file.to_ascii_lowercase().trim_end_matches(".exe")),
+ None => OsString::from(file_name),
+ }
+}
+
+/// Write a debug message that there is no platform available
+#[inline]
+fn debug_no_platform() {
+ debug!("Could not find Volta-managed platform, delegating to system");
+}
+
+/// Write a debug message with the full image that will be used to execute a command
+#[inline]
+fn debug_active_image(image: &Image) {
+ debug!(
+ "Active Image:
+ Node: {}
+ npm: {}
+ pnpm: {}
+ Yarn: {}",
+ format_tool_version(&image.node),
+ image
+ .resolve_npm()
+ .ok()
+ .as_ref()
+ .map(format_tool_version)
+ .unwrap_or_else(|| "Bundled with Node".into()),
+ image
+ .pnpm
+ .as_ref()
+ .map(format_tool_version)
+ .unwrap_or_else(|| "None".into()),
+ image
+ .yarn
+ .as_ref()
+ .map(format_tool_version)
+ .unwrap_or_else(|| "None".into()),
+ )
+}
+
+fn format_tool_version(version: &Sourced<Version>) -> String {
+ format!("{} from {} configuration", version.value, version.source)
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +
use std::env;
+use std::ffi::OsString;
+
+use super::executor::{Executor, ToolCommand, ToolKind};
+use super::{debug_active_image, debug_no_platform, RECURSION_ENV_VAR};
+use crate::error::{ErrorKind, Fallible};
+use crate::platform::{Platform, System};
+use crate::session::{ActivityKind, Session};
+
+/// Build a `ToolCommand` for Node
+pub(super) fn command(args: &[OsString], session: &mut Session) -> Fallible<Executor> {
+ session.add_event_start(ActivityKind::Node);
+ // Don't re-evaluate the platform if this is a recursive call
+ let platform = match env::var_os(RECURSION_ENV_VAR) {
+ Some(_) => None,
+ None => Platform::current(session)?,
+ };
+
+ Ok(ToolCommand::new("node", args, platform, ToolKind::Node).into())
+}
+
+/// Determine the execution context (PATH and failure error message) for Node
+pub(super) fn execution_context(
+ platform: Option<Platform>,
+ session: &mut Session,
+) -> Fallible<(OsString, ErrorKind)> {
+ match platform {
+ Some(plat) => {
+ let image = plat.checkout(session)?;
+ let path = image.path()?;
+ debug_active_image(&image);
+
+ Ok((path, ErrorKind::BinaryExecError))
+ }
+ None => {
+ let path = System::path()?;
+ debug_no_platform();
+ Ok((path, ErrorKind::NoPlatform))
+ }
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +
use std::env;
+use std::ffi::OsString;
+use std::fs::File;
+
+use super::executor::{Executor, ToolCommand, ToolKind, UninstallCommand};
+use super::parser::{CommandArg, InterceptedCommand};
+use super::{debug_active_image, debug_no_platform, RECURSION_ENV_VAR};
+use crate::error::{ErrorKind, Fallible};
+use crate::platform::{Platform, System};
+use crate::session::{ActivityKind, Session};
+use crate::tool::{PackageManifest, Spec};
+use crate::version::VersionSpec;
+
+/// Build an `Executor` for npm
+///
+/// If the command is a global install or uninstall and we have a default platform available, then
+/// we will use custom logic to ensure that the package is correctly installed / uninstalled in the
+/// Volta directory.
+///
+/// If the command is _not_ a global install / uninstall or we don't have a default platform, then
+/// we will allow npm to execute the command as usual.
+pub(super) fn command(args: &[OsString], session: &mut Session) -> Fallible<Executor> {
+ session.add_event_start(ActivityKind::Npm);
+ // Don't re-evaluate the context or global install interception if this is a recursive call
+ let platform = match env::var_os(RECURSION_ENV_VAR) {
+ Some(_) => None,
+ None => {
+ match CommandArg::for_npm(args) {
+ CommandArg::Global(cmd) => {
+ // For globals, only intercept if the default platform exists
+ if let Some(default_platform) = session.default_platform()? {
+ return cmd.executor(default_platform);
+ }
+ }
+ CommandArg::Intercepted(InterceptedCommand::Link(link)) => {
+ // For link commands, only intercept if a platform exists
+ if let Some(platform) = Platform::current(session)? {
+ return link.executor(platform, current_project_name(session));
+ }
+ }
+ CommandArg::Intercepted(InterceptedCommand::Unlink) => {
+ // For unlink, attempt to find the current project name. If successful, treat
+ // this as a global uninstall of the current project.
+ if let Some(name) = current_project_name(session) {
+ // Same as for link, only intercept if a platform exists
+ if Platform::current(session)?.is_some() {
+ return Ok(UninstallCommand::new(Spec::Package(
+ name,
+ VersionSpec::None,
+ ))
+ .into());
+ }
+ }
+ }
+ _ => {}
+ }
+
+ Platform::current(session)?
+ }
+ };
+
+ Ok(ToolCommand::new("npm", args, platform, ToolKind::Npm).into())
+}
+
+/// Determine the execution context (PATH and failure error message) for npm
+pub(super) fn execution_context(
+ platform: Option<Platform>,
+ session: &mut Session,
+) -> Fallible<(OsString, ErrorKind)> {
+ match platform {
+ Some(plat) => {
+ let image = plat.checkout(session)?;
+ let path = image.path()?;
+ debug_active_image(&image);
+
+ Ok((path, ErrorKind::BinaryExecError))
+ }
+ None => {
+ let path = System::path()?;
+ debug_no_platform();
+ Ok((path, ErrorKind::NoPlatform))
+ }
+ }
+}
+
+/// Determine the name of the current project, if possible
+fn current_project_name(session: &mut Session) -> Option<String> {
+ let project = session.project().ok()??;
+ let manifest_file = File::open(project.manifest_file()).ok()?;
+ let manifest: PackageManifest = serde_json::de::from_reader(manifest_file).ok()?;
+
+ Some(manifest.name)
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +
use std::env;
+use std::ffi::OsString;
+
+use super::executor::{Executor, ToolCommand, ToolKind};
+use super::{debug_active_image, debug_no_platform, RECURSION_ENV_VAR};
+use crate::error::{ErrorKind, Fallible};
+use crate::platform::{Platform, System};
+use crate::session::{ActivityKind, Session};
+use node_semver::Version;
+use once_cell::sync::Lazy;
+
+static REQUIRED_NPM_VERSION: Lazy<Version> = Lazy::new(|| Version {
+ major: 5,
+ minor: 2,
+ patch: 0,
+ build: vec![],
+ pre_release: vec![],
+});
+
+/// Build a `ToolCommand` for npx
+pub(super) fn command(args: &[OsString], session: &mut Session) -> Fallible<Executor> {
+ session.add_event_start(ActivityKind::Npx);
+ // Don't re-evaluate the context if this is a recursive call
+ let platform = match env::var_os(RECURSION_ENV_VAR) {
+ Some(_) => None,
+ None => Platform::current(session)?,
+ };
+
+ Ok(ToolCommand::new("npx", args, platform, ToolKind::Npx).into())
+}
+
+/// Determine the execution context (PATH and failure error message) for npx
+pub(super) fn execution_context(
+ platform: Option<Platform>,
+ session: &mut Session,
+) -> Fallible<(OsString, ErrorKind)> {
+ match platform {
+ Some(plat) => {
+ let image = plat.checkout(session)?;
+
+ // If the npm version is lower than the minimum required, we can show a helpful error
+ // message instead of a 'command not found' error.
+ let active_npm = image.resolve_npm()?;
+ if active_npm.value < *REQUIRED_NPM_VERSION {
+ return Err(ErrorKind::NpxNotAvailable {
+ version: active_npm.value.to_string(),
+ }
+ .into());
+ }
+
+ let path = image.path()?;
+ debug_active_image(&image);
+
+ Ok((path, ErrorKind::BinaryExecError))
+ }
+ None => {
+ let path = System::path()?;
+ debug_no_platform();
+ Ok((path, ErrorKind::NoPlatform))
+ }
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402 +403 +404 +405 +406 +407 +408 +409 +410 +411 +412 +413 +414 +415 +416 +417 +418 +419 +420 +421 +422 +423 +424 +425 +426 +427 +428 +429 +430 +431 +432 +433 +434 +435 +436 +437 +438 +439 +440 +441 +442 +443 +444 +445 +446 +447 +448 +449 +450 +451 +452 +453 +454 +455 +456 +457 +458 +459 +460 +461 +462 +463 +464 +465 +466 +467 +468 +469 +470 +471 +472 +473 +474 +475 +476 +477 +478 +479 +480 +481 +482 +483 +484 +485 +486 +487 +488 +489 +490 +491 +492 +493 +494 +495 +496 +497 +498 +499 +500 +501 +502 +503 +504 +505 +506 +507 +508 +509 +510 +511 +512 +513 +514 +515 +516 +517 +518 +519 +520 +521 +522 +523 +524 +525 +526 +527 +528 +529 +530 +531 +532 +533 +534 +535 +536 +537 +538 +539 +540 +541 +542 +543 +544 +545 +546 +547 +548 +549 +550 +551 +552 +553 +554 +555 +556 +557 +558 +559 +560 +561 +562 +563 +564 +565 +566 +567 +568 +569 +570 +571 +572 +573 +574 +575 +576 +577 +578 +579 +580 +581 +582 +583 +584 +585 +586 +587 +588 +589 +590 +591 +592 +593 +594 +595 +596 +597 +598 +599 +600 +601 +602 +603 +604 +605 +606 +607 +608 +609 +610 +611 +612 +613 +614 +615 +616 +617 +618 +619 +620 +621 +622 +623 +624 +625 +626 +627 +628 +629 +630 +631 +632 +633 +634 +635 +636 +637 +638 +639 +640 +641 +642 +643 +644 +645 +646 +647 +648 +649 +650 +651 +652 +653 +654 +655 +656 +657 +658 +659 +660 +661 +662 +663 +664 +665 +666 +667 +668 +669 +670 +671 +672 +673 +674 +675 +676 +677 +678 +679 +680 +681 +682 +683 +684 +685 +686 +687 +688 +689 +690 +691 +692 +693 +694 +695 +696 +697 +698 +699 +700 +701 +702 +703 +704 +705 +706 +707 +708 +709 +710 +711 +712 +713 +714 +715 +716 +717 +718 +719 +720 +721 +722 +723 +724 +725 +726 +727 +728 +729 +730 +731 +732 +733 +734 +735 +736 +737 +738 +739 +740 +741 +742 +743 +744 +745 +746 +747 +748 +749 +750 +751 +752 +753 +754 +755 +756 +757 +758 +759 +760 +761 +762 +763 +764 +765 +766 +767 +768 +769 +770 +771 +772 +773 +774 +775 +776 +777 +778 +779 +780 +781 +782 +783 +784 +785 +786 +787 +788 +789 +790 +791 +792 +793 +794 +795 +796 +797 +798 +799 +800 +801 +802 +803 +804 +805 +806 +807 +808 +809 +810 +811 +812 +813 +814 +815 +816 +817 +818 +819 +820 +821 +822 +823 +824 +825 +826 +827 +828 +829 +830 +831 +832 +833 +834 +835 +836 +837 +838 +839 +840 +841 +842 +843 +844 +845 +846 +847 +848 +849 +850 +851 +852 +853 +854 +855 +856 +857 +858 +859 +860 +861 +862 +863 +864 +865 +866 +867 +868 +869 +870 +871 +872 +873 +874 +875 +876 +877 +878 +879 +880 +881 +882 +883 +884 +885 +886 +887 +888 +889 +890 +891 +892 +893 +894 +895 +896 +897 +898 +899 +900 +901 +902 +903 +904 +905 +906 +907 +908 +909 +910 +911 +912 +913 +914 +915 +916 +917 +918 +919 +920 +921 +922 +923 +924 +925 +926 +927 +928 +929 +930 +931 +932 +933 +934 +935 +936 +937 +938 +939 +940 +941 +942 +943 +944 +945 +946 +947 +948 +949 +950 +951 +952 +953 +954 +955 +956 +
use std::env;
+use std::ffi::OsStr;
+use std::iter::once;
+
+use super::executor::{
+ Executor, InternalInstallCommand, PackageInstallCommand, PackageLinkCommand,
+ PackageUpgradeCommand, UninstallCommand,
+};
+use crate::error::{ErrorKind, Fallible};
+use crate::inventory::package_configs;
+use crate::platform::{Platform, PlatformSpec};
+use crate::tool::package::PackageManager;
+use crate::tool::Spec;
+use log::debug;
+
+const UNSAFE_GLOBAL: &str = "VOLTA_UNSAFE_GLOBAL";
+/// Aliases that npm supports for the 'install' command
+const NPM_INSTALL_ALIASES: [&str; 12] = [
+ "i", "in", "ins", "inst", "insta", "instal", "install", "isnt", "isnta", "isntal", "isntall",
+ "add",
+];
+/// Aliases that npm supports for the 'uninstall' command
+const NPM_UNINSTALL_ALIASES: [&str; 5] = ["un", "uninstall", "remove", "rm", "r"];
+/// Aliases that npm supports for the 'link' command
+const NPM_LINK_ALIASES: [&str; 2] = ["link", "ln"];
+/// Aliases that npm supports for the `update` command
+const NPM_UPDATE_ALIASES: [&str; 4] = ["update", "udpate", "upgrade", "up"];
+/// Aliases that pnpm supports for the 'remove' command,
+/// see: https://pnpm.io/cli/remove
+const PNPM_UNINSTALL_ALIASES: [&str; 4] = ["remove", "uninstall", "rm", "un"];
+/// Aliases that pnpm supports for the 'update' command,
+/// see: https://pnpm.io/cli/update
+const PNPM_UPDATE_ALIASES: [&str; 3] = ["update", "upgrade", "up"];
+/// Aliases that pnpm supports for the 'link' command
+/// see: https://pnpm.io/cli/link
+const PNPM_LINK_ALIASES: [&str; 2] = ["link", "ln"];
+
+pub enum CommandArg<'a> {
+ Global(GlobalCommand<'a>),
+ Intercepted(InterceptedCommand<'a>),
+ Standard,
+}
+
+impl<'a> CommandArg<'a> {
+ /// Parse the given set of arguments to see if they correspond to an intercepted npm command
+ pub fn for_npm<S>(args: &'a [S]) -> Self
+ where
+ S: AsRef<OsStr>,
+ {
+ // If VOLTA_UNSAFE_GLOBAL is set, then we always skip any interception parsing
+ if env::var_os(UNSAFE_GLOBAL).is_some() {
+ return CommandArg::Standard;
+ }
+
+ let mut positionals = args.iter().filter(is_positional).map(AsRef::as_ref);
+
+ // The first positional argument will always be the command, however npm supports multiple
+ // aliases for commands (see https://github.com/npm/cli/blob/latest/lib/utils/cmd-list.js)
+ // Additionally, if we have a global install or uninstall, all of the remaining positional
+ // arguments will be the tools to install or uninstall. If there are _no_ other arguments,
+ // then we treat the command not a global and allow npm to handle any error messages.
+ match positionals.next() {
+ Some(cmd) if NPM_INSTALL_ALIASES.iter().any(|a| a == &cmd) => {
+ if has_global_without_prefix(args) {
+ let tools: Vec<_> = positionals.collect();
+
+ if tools.is_empty() {
+ CommandArg::Standard
+ } else {
+ // The common args for an install should be the command combined with any flags
+ let mut common_args = vec![cmd];
+ common_args.extend(args.iter().filter(is_flag).map(AsRef::as_ref));
+
+ CommandArg::Global(GlobalCommand::Install(InstallArgs {
+ manager: PackageManager::Npm,
+ common_args,
+ tools,
+ }))
+ }
+ } else {
+ CommandArg::Standard
+ }
+ }
+ Some(cmd) if NPM_UNINSTALL_ALIASES.iter().any(|a| a == &cmd) => {
+ if has_global_without_prefix(args) {
+ let tools: Vec<_> = positionals.collect();
+
+ if tools.is_empty() {
+ CommandArg::Standard
+ } else {
+ CommandArg::Global(GlobalCommand::Uninstall(UninstallArgs { tools }))
+ }
+ } else {
+ CommandArg::Standard
+ }
+ }
+ Some(cmd) if cmd == "unlink" => {
+ let tools: Vec<_> = positionals.collect();
+
+ if tools.is_empty() {
+ // `npm unlink` without any arguments is used to unlink the current project
+ CommandArg::Intercepted(InterceptedCommand::Unlink)
+ } else if has_global_without_prefix(args) {
+ // With arguments, `npm unlink` is an alias of `npm remove`
+ CommandArg::Global(GlobalCommand::Uninstall(UninstallArgs { tools }))
+ } else {
+ CommandArg::Standard
+ }
+ }
+ Some(cmd) if NPM_LINK_ALIASES.iter().any(|a| a == &cmd) => {
+ // Much like install, the common args for a link are the command combined with any flags
+ let mut common_args = vec![cmd];
+ common_args.extend(args.iter().filter(is_flag).map(AsRef::as_ref));
+ let tools: Vec<_> = positionals.collect();
+
+ CommandArg::Intercepted(InterceptedCommand::Link(LinkArgs { common_args, tools }))
+ }
+ Some(cmd) if NPM_UPDATE_ALIASES.iter().any(|a| a == &cmd) => {
+ if has_global_without_prefix(args) {
+ // Once again, the common args are the command combined with any flags
+ let mut common_args = vec![cmd];
+ common_args.extend(args.iter().filter(is_flag).map(AsRef::as_ref));
+ let tools: Vec<_> = positionals.collect();
+
+ CommandArg::Global(GlobalCommand::Upgrade(UpgradeArgs {
+ common_args,
+ tools,
+ manager: PackageManager::Npm,
+ }))
+ } else {
+ CommandArg::Standard
+ }
+ }
+ _ => CommandArg::Standard,
+ }
+ }
+
+ /// Parse the given set of arguments to see if they correspond to an intercepted pnpm command
+ #[allow(dead_code)]
+ pub fn for_pnpm<S>(args: &'a [S]) -> CommandArg<'a>
+ where
+ S: AsRef<OsStr>,
+ {
+ // If VOLTA_UNSAFE_GLOBAL is set, then we always skip any global parsing
+ if env::var_os(UNSAFE_GLOBAL).is_some() {
+ return CommandArg::Standard;
+ }
+
+ let (flags, positionals): (Vec<&OsStr>, Vec<&OsStr>) =
+ args.iter().map(AsRef::<OsStr>::as_ref).partition(is_flag);
+
+ // The first positional argument will always be the subcommand for pnpm
+ match positionals.split_first() {
+ None => CommandArg::Standard,
+ Some((&subcommand, tools)) => {
+ let is_global = flags.iter().any(|&f| f == "--global" || f == "-g");
+ // Do not intercept if a custom global dir is explicitly specified
+ // See: https://pnpm.io/npmrc#global-dir
+ let prefixed = flags.iter().any(|&f| f == "--global-dir");
+
+ // pnpm subcommands that support the `global` flag:
+ // `add`, `update`, `remove`, `link`, `list`, `outdated`,
+ // `why`, `env`, `root`, `bin`.
+ match is_global && !prefixed {
+ false => CommandArg::Standard,
+ true => match subcommand.to_str() {
+ // `add`
+ Some("add") => {
+ let manager = PackageManager::Pnpm;
+ let mut common_args = vec![subcommand];
+ common_args.extend(flags);
+
+ CommandArg::Global(GlobalCommand::Install(InstallArgs {
+ manager,
+ common_args,
+ tools: tools.to_vec(),
+ }))
+ }
+ // `update`
+ Some(cmd) if PNPM_UPDATE_ALIASES.iter().any(|&a| a == cmd) => {
+ let manager = PackageManager::Pnpm;
+ let mut common_args = vec![subcommand];
+ common_args.extend(flags);
+ CommandArg::Global(GlobalCommand::Upgrade(UpgradeArgs {
+ manager,
+ common_args,
+ tools: tools.to_vec(),
+ }))
+ }
+ // `remove`
+ Some(cmd) if PNPM_UNINSTALL_ALIASES.iter().any(|&a| a == cmd) => {
+ CommandArg::Global(GlobalCommand::Uninstall(UninstallArgs {
+ tools: tools.to_vec(),
+ }))
+ }
+ // `link`
+ Some(cmd) if PNPM_LINK_ALIASES.iter().any(|&a| a == cmd) => {
+ let mut common_args = vec![subcommand];
+ common_args.extend(flags);
+ CommandArg::Intercepted(InterceptedCommand::Link(LinkArgs {
+ common_args,
+ tools: tools.to_vec(),
+ }))
+ }
+ _ => CommandArg::Standard,
+ },
+ }
+ }
+ }
+ }
+
+ /// Parse the given set of arguments to see if they correspond to an intercepted Yarn command
+ pub fn for_yarn<S>(args: &'a [S]) -> Self
+ where
+ S: AsRef<OsStr>,
+ {
+ // If VOLTA_UNSAFE_GLOBAL is set, then we always skip any global parsing
+ if env::var_os(UNSAFE_GLOBAL).is_some() {
+ return CommandArg::Standard;
+ }
+
+ let mut positionals = args.iter().filter(is_positional).map(AsRef::as_ref);
+
+ // Yarn globals must always start with `global <command>`
+ // If we have a global add, remove, or upgrade, then all of the remaining positional
+ // arguments will be the tools to modify. As with npm, if there are no arguments then we
+ // can treat it as if it's not a global command and allow Yarn to show any errors.
+ match (positionals.next(), positionals.next()) {
+ (Some(global), Some(add)) if global == "global" && add == "add" => {
+ let tools: Vec<_> = positionals.collect();
+
+ if tools.is_empty() {
+ CommandArg::Standard
+ } else {
+ // The common args for an install should be `global add` and any flags
+ let mut common_args = vec![global, add];
+ common_args.extend(args.iter().filter(is_flag).map(AsRef::as_ref));
+
+ CommandArg::Global(GlobalCommand::Install(InstallArgs {
+ manager: PackageManager::Yarn,
+ common_args,
+ tools,
+ }))
+ }
+ }
+ (Some(global), Some(remove)) if global == "global" && remove == "remove" => {
+ let tools: Vec<_> = positionals.collect();
+
+ if tools.is_empty() {
+ CommandArg::Standard
+ } else {
+ CommandArg::Global(GlobalCommand::Uninstall(UninstallArgs { tools }))
+ }
+ }
+ (Some(global), Some(upgrade)) if global == "global" && upgrade == "upgrade" => {
+ // The common args for an upgrade should be `global upgrade` and any flags
+ let mut common_args = vec![global, upgrade];
+ common_args.extend(args.iter().filter(is_flag).map(AsRef::as_ref));
+
+ CommandArg::Global(GlobalCommand::Upgrade(UpgradeArgs {
+ common_args,
+ tools: positionals.collect(),
+ manager: PackageManager::Yarn,
+ }))
+ }
+ _ => CommandArg::Standard,
+ }
+ }
+}
+
+pub enum GlobalCommand<'a> {
+ Install(InstallArgs<'a>),
+ Uninstall(UninstallArgs<'a>),
+ Upgrade(UpgradeArgs<'a>),
+}
+
+impl<'a> GlobalCommand<'a> {
+ pub fn executor(self, platform: &PlatformSpec) -> Fallible<Executor> {
+ match self {
+ GlobalCommand::Install(cmd) => cmd.executor(platform),
+ GlobalCommand::Uninstall(cmd) => cmd.executor(),
+ GlobalCommand::Upgrade(cmd) => cmd.executor(platform),
+ }
+ }
+}
+
+/// The arguments passed to a global install command
+pub struct InstallArgs<'a> {
+ /// The package manager being used
+ manager: PackageManager,
+ /// Common arguments that apply to each tool (e.g. flags)
+ common_args: Vec<&'a OsStr>,
+ /// The individual tool arguments
+ tools: Vec<&'a OsStr>,
+}
+
+impl<'a> InstallArgs<'a> {
+ /// Convert these global install arguments into an executor for the command
+ ///
+ /// If there are multiple packages specified to install, then they will be broken out into
+ /// individual commands and run separately. That allows us to keep Volta's sandboxing for each
+ /// package while still supporting the ability to install multiple packages at once.
+ pub fn executor(self, platform_spec: &PlatformSpec) -> Fallible<Executor> {
+ let mut executors = Vec::with_capacity(self.tools.len());
+
+ for tool in self.tools {
+ // External tool installs may be in a form that doesn't match a `Spec` (such as a git
+ // URL or path to a tarball). If parsing into a `Spec` fails, we assume that it's a
+ // 3rd-party Tool and attempt to install anyway.
+ match Spec::try_from_str(&tool.to_string_lossy()) {
+ Ok(Spec::Package(_, _)) | Err(_) => {
+ let platform = platform_spec.as_default();
+ // The args for an individual install command are the common args combined
+ // with the name of the tool.
+ let args = self.common_args.iter().chain(once(&tool));
+ let command = PackageInstallCommand::new(args, platform, self.manager)?;
+ executors.push(command.into());
+ }
+ Ok(internal) => executors.push(InternalInstallCommand::new(internal).into()),
+ }
+ }
+
+ Ok(executors.into())
+ }
+}
+
+/// The list of tools passed to an uninstall command
+pub struct UninstallArgs<'a> {
+ tools: Vec<&'a OsStr>,
+}
+
+impl<'a> UninstallArgs<'a> {
+ /// Convert the tools into an executor for the uninstall command
+ ///
+ /// Since the packages are sandboxed, each needs to be uninstalled separately
+ pub fn executor(self) -> Fallible<Executor> {
+ let mut executors = Vec::with_capacity(self.tools.len());
+
+ for tool_name in self.tools {
+ let tool = Spec::try_from_str(&tool_name.to_string_lossy())?;
+ executors.push(UninstallCommand::new(tool).into());
+ }
+
+ Ok(executors.into())
+ }
+}
+
+/// The list of tools passed to an upgrade command
+pub struct UpgradeArgs<'a> {
+ /// The package manager being used
+ manager: PackageManager,
+ /// Common arguments that apply to each tool (e.g. flags)
+ common_args: Vec<&'a OsStr>,
+ /// The individual tool arguments
+ tools: Vec<&'a OsStr>,
+}
+
+impl<'a> UpgradeArgs<'a> {
+ /// Convert these global upgrade arguments into an executor for the command
+ ///
+ /// If there are multiple packages specified to upgrade, then they will be broken out into
+ /// individual commands and run separately. If no packages are specified, then we will upgrade
+ /// _all_ installed packages that were installed with the same package manager.
+ pub fn executor(self, platform_spec: &PlatformSpec) -> Fallible<Executor> {
+ if self.tools.is_empty() {
+ return self.executor_all_packages(platform_spec);
+ }
+
+ let mut executors = Vec::with_capacity(self.tools.len());
+
+ for tool in self.tools {
+ match Spec::try_from_str(&tool.to_string_lossy()) {
+ Ok(Spec::Package(package, _)) => {
+ let platform = platform_spec.as_default();
+ let args = self.common_args.iter().chain(once(&tool));
+ executors.push(
+ PackageUpgradeCommand::new(args, package, platform, self.manager)?.into(),
+ );
+ }
+ Ok(internal) => {
+ executors.push(UninstallCommand::new(internal).into());
+ }
+ Err(_) => {
+ return Err(ErrorKind::UpgradePackageNotFound {
+ package: tool.to_string_lossy().to_string(),
+ manager: self.manager,
+ }
+ .into())
+ }
+ }
+ }
+
+ Ok(executors.into())
+ }
+
+ /// Build an executor to upgrade _all_ global packages that were installed with the same
+ /// package manager as we are currently running.
+ fn executor_all_packages(self, platform_spec: &PlatformSpec) -> Fallible<Executor> {
+ package_configs()?
+ .into_iter()
+ .filter(|config| config.manager == self.manager)
+ .map(|config| {
+ let platform = platform_spec.as_default();
+ let package_name = config.name.as_ref();
+ let args = self.common_args.iter().chain(once(&package_name));
+
+ let executor =
+ PackageUpgradeCommand::new(args, config.name.clone(), platform, self.manager)?
+ .into();
+ Ok(executor)
+ })
+ .collect::<Fallible<Vec<_>>>()
+ .map(Into::into)
+ }
+}
+
+/// An intercepted local command
+pub enum InterceptedCommand<'a> {
+ Link(LinkArgs<'a>),
+ Unlink,
+}
+
+/// The arguments passed to an `npm link` command
+pub struct LinkArgs<'a> {
+ /// The common arguments that apply to each tool
+ common_args: Vec<&'a OsStr>,
+ /// The list of tools to link (if any)
+ tools: Vec<&'a OsStr>,
+}
+
+impl<'a> LinkArgs<'a> {
+ pub fn executor(self, platform: Platform, project_name: Option<String>) -> Fallible<Executor> {
+ if self.tools.is_empty() {
+ // If no tools are specified, then this is a bare link command, linking the current
+ // project as a global package. We treat this like a global install except we look up
+ // the name from the current directory first.
+ match project_name {
+ Some(name) => PackageInstallCommand::for_npm_link(self.common_args, platform, name),
+ None => PackageInstallCommand::new(self.common_args, platform, PackageManager::Npm),
+ }
+ .map(Into::into)
+ } else {
+ // If there are tools specified, then this represents a command to link a global
+ // package into the current project. We handle each tool separately to support Volta's
+ // package sandboxing.
+ let common_args = self.common_args;
+
+ Ok(self
+ .tools
+ .into_iter()
+ .map(|tool| {
+ let args = common_args.iter().chain(once(&tool));
+ PackageLinkCommand::new(
+ args,
+ platform.clone(),
+ tool.to_string_lossy().to_string(),
+ )
+ .into()
+ })
+ .collect::<Vec<_>>()
+ .into())
+ }
+ }
+}
+
+/// Check if the provided argument list includes a global flag and _doesn't_ have a prefix setting
+///
+/// For our interception, we only want to intercept global commands. Additionally, if the user
+/// passes a prefix setting, that will override the logic we use to redirect the install, so our
+/// process won't work and will cause an error. We should avoid intercepting in those cases since
+/// a command with an explicit prefix is something beyond the "standard" global install anyway.
+fn has_global_without_prefix<A>(args: &[A]) -> bool
+where
+ A: AsRef<OsStr>,
+{
+ let (has_global, has_prefix) = args.iter().fold((false, false), |(global, prefix), arg| {
+ match arg.as_ref().to_str() {
+ Some("-g") | Some("--global") => (true, prefix),
+ Some(pre) if pre.starts_with("--prefix") => (global, true),
+ _ => (global, prefix),
+ }
+ });
+
+ if has_global && has_prefix {
+ debug!("Skipping global interception due to prefix argument");
+ }
+
+ has_global && !has_prefix
+}
+
+fn is_flag<A>(arg: &A) -> bool
+where
+ A: AsRef<OsStr>,
+{
+ match arg.as_ref().to_str() {
+ Some(a) => a.starts_with('-'),
+ None => false,
+ }
+}
+
+fn is_positional<A>(arg: &A) -> bool
+where
+ A: AsRef<OsStr>,
+{
+ !is_flag(arg)
+}
+
+#[cfg(test)]
+mod tests {
+ use std::ffi::{OsStr, OsString};
+
+ fn arg_list<A, S>(args: A) -> Vec<OsString>
+ where
+ A: IntoIterator<Item = S>,
+ S: AsRef<OsStr>,
+ {
+ args.into_iter().map(|a| a.as_ref().to_owned()).collect()
+ }
+
+ mod npm {
+ use super::super::*;
+ use super::arg_list;
+
+ #[test]
+ fn handles_global_install() {
+ match CommandArg::for_npm(&arg_list(["install", "--global", "typescript@3"])) {
+ CommandArg::Global(GlobalCommand::Install(install)) => {
+ assert_eq!(install.manager, PackageManager::Npm);
+ assert_eq!(install.common_args, vec!["install", "--global"]);
+ assert_eq!(install.tools, vec!["typescript@3"]);
+ }
+ _ => panic!("Doesn't parse global install as a global"),
+ };
+ }
+
+ #[test]
+ fn handles_local_install() {
+ match CommandArg::for_npm(&arg_list(["install", "--save-dev", "typescript"])) {
+ CommandArg::Standard => (),
+ _ => panic!("Parses local install as global"),
+ };
+ }
+
+ #[test]
+ fn handles_global_uninstall() {
+ match CommandArg::for_npm(&arg_list(["uninstall", "--global", "typescript"])) {
+ CommandArg::Global(GlobalCommand::Uninstall(uninstall)) => {
+ assert_eq!(uninstall.tools, vec!["typescript"]);
+ }
+ _ => panic!("Doesn't parse global uninstall as a global"),
+ };
+ }
+
+ #[test]
+ fn handles_local_uninstall() {
+ match CommandArg::for_npm(&arg_list(["uninstall", "--save-dev", "typescript"])) {
+ CommandArg::Standard => (),
+ _ => panic!("Parses local uninstall as global"),
+ };
+ }
+
+ #[test]
+ fn handles_multiple_install() {
+ match CommandArg::for_npm(&arg_list([
+ "install",
+ "--global",
+ "typescript@3",
+ "cowsay@1",
+ "ember-cli@2",
+ ])) {
+ CommandArg::Global(GlobalCommand::Install(install)) => {
+ assert_eq!(install.manager, PackageManager::Npm);
+ assert_eq!(install.common_args, vec!["install", "--global"]);
+ assert_eq!(
+ install.tools,
+ vec!["typescript@3", "cowsay@1", "ember-cli@2"]
+ );
+ }
+ _ => panic!("Doesn't parse global install as a global"),
+ };
+ }
+
+ #[test]
+ fn handles_multiple_uninstall() {
+ match CommandArg::for_npm(&arg_list([
+ "uninstall",
+ "--global",
+ "typescript",
+ "cowsay",
+ "ember-cli",
+ ])) {
+ CommandArg::Global(GlobalCommand::Uninstall(uninstall)) => {
+ assert_eq!(uninstall.tools, vec!["typescript", "cowsay", "ember-cli"]);
+ }
+ _ => panic!("Doesn't parse global uninstall as a global"),
+ };
+ }
+
+ #[test]
+ fn handles_bare_link() {
+ match CommandArg::for_npm(&arg_list(["link"])) {
+ CommandArg::Intercepted(InterceptedCommand::Link(_)) => (),
+ _ => panic!("Doesn't parse bare link command ('npm link' with no packages"),
+ };
+ }
+
+ #[test]
+ fn handles_multiple_link() {
+ match CommandArg::for_npm(&arg_list(["link", "typescript", "react"])) {
+ CommandArg::Intercepted(InterceptedCommand::Link(link)) => {
+ assert_eq!(link.tools, vec!["typescript", "react"]);
+ }
+ _ => panic!("Doesn't parse link command with packages"),
+ };
+ }
+
+ #[test]
+ fn handles_bare_unlink() {
+ match CommandArg::for_npm(&arg_list(["unlink"])) {
+ CommandArg::Intercepted(InterceptedCommand::Unlink) => (),
+ _ => panic!("Doesn't parse bare unlink command ('npm unlink' with no packages"),
+ };
+ }
+
+ #[test]
+ fn handles_local_unlink() {
+ match CommandArg::for_npm(&arg_list(["unlink", "@angular/cli"])) {
+ CommandArg::Standard => (),
+ _ => panic!("Doesn't pass through local 'unlink' command"),
+ }
+ }
+
+ #[test]
+ fn handles_global_aliases() {
+ match CommandArg::for_npm(&arg_list(["install", "--global", "typescript"])) {
+ CommandArg::Global(GlobalCommand::Install(_)) => (),
+ _ => panic!("Doesn't parse long form (--global)"),
+ };
+
+ match CommandArg::for_npm(&arg_list(["install", "-g", "typescript"])) {
+ CommandArg::Global(GlobalCommand::Install(_)) => (),
+ _ => panic!("Doesn't parse short form (-g)"),
+ };
+ }
+
+ #[test]
+ fn handles_install_aliases() {
+ match CommandArg::for_npm(&arg_list(["i", "--global", "typescript"])) {
+ CommandArg::Global(GlobalCommand::Install(_)) => (),
+ _ => panic!("Doesn't parse short form (i)"),
+ };
+
+ match CommandArg::for_npm(&arg_list(["in", "--global", "typescript"])) {
+ CommandArg::Global(GlobalCommand::Install(_)) => (),
+ _ => panic!("Doesn't parse short form (in)"),
+ };
+
+ match CommandArg::for_npm(&arg_list(["ins", "--global", "typescript"])) {
+ CommandArg::Global(GlobalCommand::Install(_)) => (),
+ _ => panic!("Doesn't parse short form (ins)"),
+ };
+
+ match CommandArg::for_npm(&arg_list(["inst", "--global", "typescript"])) {
+ CommandArg::Global(GlobalCommand::Install(_)) => (),
+ _ => panic!("Doesn't parse short form (inst)"),
+ };
+
+ match CommandArg::for_npm(&arg_list(["insta", "--global", "typescript"])) {
+ CommandArg::Global(GlobalCommand::Install(_)) => (),
+ _ => panic!("Doesn't parse short form (insta)"),
+ };
+
+ match CommandArg::for_npm(&arg_list(["instal", "--global", "typescript"])) {
+ CommandArg::Global(GlobalCommand::Install(_)) => (),
+ _ => panic!("Doesn't parse short form (instal)"),
+ };
+
+ match CommandArg::for_npm(&arg_list(["install", "--global", "typescript"])) {
+ CommandArg::Global(GlobalCommand::Install(_)) => (),
+ _ => panic!("Doesn't parse exact command (install)"),
+ };
+
+ match CommandArg::for_npm(&arg_list(["isnt", "--global", "typescript"])) {
+ CommandArg::Global(GlobalCommand::Install(_)) => (),
+ _ => panic!("Doesn't parse short form misspelling (isnt)"),
+ };
+
+ match CommandArg::for_npm(&arg_list(["isnta", "--global", "typescript"])) {
+ CommandArg::Global(GlobalCommand::Install(_)) => (),
+ _ => panic!("Doesn't parse short form misspelling (isnta)"),
+ };
+
+ match CommandArg::for_npm(&arg_list(["isntal", "--global", "typescript"])) {
+ CommandArg::Global(GlobalCommand::Install(_)) => (),
+ _ => panic!("Doesn't parse short form misspelling (isntal)"),
+ };
+
+ match CommandArg::for_npm(&arg_list(["isntall", "--global", "typescript"])) {
+ CommandArg::Global(GlobalCommand::Install(_)) => (),
+ _ => panic!("Doesn't parse misspelling (isntall)"),
+ };
+
+ match CommandArg::for_npm(&arg_list(["add", "--global", "typescript"])) {
+ CommandArg::Global(GlobalCommand::Install(_)) => (),
+ _ => panic!("Doesn't parse 'add' alias"),
+ };
+ }
+
+ #[test]
+ fn handles_uninstall_aliases() {
+ match CommandArg::for_npm(&arg_list(["uninstall", "--global", "typescript"])) {
+ CommandArg::Global(GlobalCommand::Uninstall(_)) => (),
+ _ => panic!("Doesn't parse long form (uninstall)"),
+ };
+
+ match CommandArg::for_npm(&arg_list(["unlink", "--global", "typescript"])) {
+ CommandArg::Global(GlobalCommand::Uninstall(_)) => (),
+ _ => panic!("Doesn't parse 'unlink'"),
+ };
+
+ match CommandArg::for_npm(&arg_list(["remove", "--global", "typescript"])) {
+ CommandArg::Global(GlobalCommand::Uninstall(_)) => (),
+ _ => panic!("Doesn't parse 'remove'"),
+ };
+
+ match CommandArg::for_npm(&arg_list(["un", "--global", "typescript"])) {
+ CommandArg::Global(GlobalCommand::Uninstall(_)) => (),
+ _ => panic!("Doesn't parse short form (un)"),
+ };
+
+ match CommandArg::for_npm(&arg_list(["rm", "--global", "typescript"])) {
+ CommandArg::Global(GlobalCommand::Uninstall(_)) => (),
+ _ => panic!("Doesn't parse short form (rm)"),
+ };
+
+ match CommandArg::for_npm(&arg_list(["r", "--global", "typescript"])) {
+ CommandArg::Global(GlobalCommand::Uninstall(_)) => (),
+ _ => panic!("Doesn't parse short form (r)"),
+ };
+ }
+
+ #[test]
+ fn handles_link_aliases() {
+ match CommandArg::for_npm(&arg_list(["link"])) {
+ CommandArg::Intercepted(InterceptedCommand::Link(_)) => (),
+ _ => panic!("Doesn't parse long form (link)"),
+ };
+
+ match CommandArg::for_npm(&arg_list(["ln"])) {
+ CommandArg::Intercepted(InterceptedCommand::Link(_)) => (),
+ _ => panic!("Doesn't parse short form (ln)"),
+ };
+ }
+
+ #[test]
+ fn processes_flags() {
+ match CommandArg::for_npm(&arg_list([
+ "--global",
+ "install",
+ "typescript",
+ "--no-audit",
+ "cowsay",
+ "--no-update-notifier",
+ ])) {
+ CommandArg::Global(GlobalCommand::Install(install)) => {
+ // The command gets moved to the front of common_args
+ assert_eq!(
+ install.common_args,
+ vec!["install", "--global", "--no-audit", "--no-update-notifier"]
+ );
+ assert_eq!(install.tools, vec!["typescript", "cowsay"]);
+ }
+ _ => panic!("Doesn't parse install with extra flags as a global"),
+ };
+
+ match CommandArg::for_npm(&arg_list([
+ "uninstall",
+ "--silent",
+ "typescript",
+ "-g",
+ "cowsay",
+ ])) {
+ CommandArg::Global(GlobalCommand::Uninstall(uninstall)) => {
+ assert_eq!(uninstall.tools, vec!["typescript", "cowsay"]);
+ }
+ _ => panic!("Doesn't parse uninstall with extra flags as a global"),
+ }
+ }
+
+ #[test]
+ fn skips_commands_with_prefix() {
+ match CommandArg::for_npm(&arg_list(["install", "-g", "--prefix", "~/", "ember"])) {
+ CommandArg::Standard => {}
+ _ => panic!("Parsed command with prefix as a global"),
+ }
+
+ match CommandArg::for_npm(&arg_list(["install", "-g", "--prefix=~/", "ember"])) {
+ CommandArg::Standard => {}
+ _ => panic!("Parsed command with prefix as a global"),
+ }
+
+ match CommandArg::for_npm(&arg_list(["uninstall", "-g", "--prefix", "~/", "ember"])) {
+ CommandArg::Standard => {}
+ _ => panic!("Parsed command with prefix as a global"),
+ }
+
+ match CommandArg::for_npm(&arg_list(["uninstall", "-g", "--prefix=~/", "ember"])) {
+ CommandArg::Standard => {}
+ _ => panic!("Parsed command with prefix as a global"),
+ }
+
+ match CommandArg::for_npm(&arg_list(["unlink", "-g", "--prefix", "~/", "ember"])) {
+ CommandArg::Standard => {}
+ _ => panic!("Parsed command with prefix as a global"),
+ }
+
+ match CommandArg::for_npm(&arg_list(["unlink", "-g", "--prefix=~/", "ember"])) {
+ CommandArg::Standard => {}
+ _ => panic!("Parsed command with prefix as a global"),
+ }
+
+ match CommandArg::for_npm(&arg_list(["update", "-g", "--prefix", "~/"])) {
+ CommandArg::Standard => {}
+ _ => panic!("Parsed command with prefix as a global"),
+ }
+
+ match CommandArg::for_npm(&arg_list(["update", "-g", "--prefix=~/"])) {
+ CommandArg::Standard => {}
+ _ => panic!("Parsed command with prefix as a global"),
+ }
+ }
+ }
+
+ mod yarn {
+ use super::super::*;
+ use super::*;
+
+ #[test]
+ fn handles_global_add() {
+ match CommandArg::for_yarn(&arg_list(["global", "add", "typescript"])) {
+ CommandArg::Global(GlobalCommand::Install(install)) => {
+ assert_eq!(install.manager, PackageManager::Yarn);
+ assert_eq!(install.common_args, vec!["global", "add"]);
+ assert_eq!(install.tools, vec!["typescript"]);
+ }
+ _ => panic!("Doesn't parse global add as a global"),
+ };
+ }
+
+ #[test]
+ fn handles_local_add() {
+ match CommandArg::for_yarn(&arg_list(["add", "typescript"])) {
+ CommandArg::Standard => (),
+ _ => panic!("Parses local add as a global"),
+ };
+
+ match CommandArg::for_yarn(&arg_list(["add", "global"])) {
+ CommandArg::Standard => (),
+ _ => panic!("Incorrectly handles bad order"),
+ };
+ }
+
+ #[test]
+ fn handles_global_remove() {
+ match CommandArg::for_yarn(&arg_list(["global", "remove", "typescript"])) {
+ CommandArg::Global(GlobalCommand::Uninstall(uninstall)) => {
+ assert_eq!(uninstall.tools, vec!["typescript"]);
+ }
+ _ => panic!("Doesn't parse global remove as a global"),
+ };
+ }
+
+ #[test]
+ fn handles_local_remove() {
+ match CommandArg::for_yarn(&arg_list(["remove", "typescript"])) {
+ CommandArg::Standard => (),
+ _ => panic!("Parses local remove as a global"),
+ };
+
+ match CommandArg::for_yarn(&arg_list(["remove", "global"])) {
+ CommandArg::Standard => (),
+ _ => panic!("Incorrectly handles bad order"),
+ };
+ }
+
+ #[test]
+ fn handles_multiple_add() {
+ match CommandArg::for_yarn(&arg_list([
+ "global",
+ "add",
+ "typescript",
+ "cowsay",
+ "ember-cli",
+ ])) {
+ CommandArg::Global(GlobalCommand::Install(install)) => {
+ assert_eq!(install.manager, PackageManager::Yarn);
+ assert_eq!(install.common_args, vec!["global", "add"]);
+ assert_eq!(install.tools, vec!["typescript", "cowsay", "ember-cli"]);
+ }
+ _ => panic!("Doesn't parse global add as a global"),
+ };
+ }
+
+ #[test]
+ fn handles_multiple_remove() {
+ match CommandArg::for_yarn(&arg_list([
+ "global",
+ "remove",
+ "typescript",
+ "cowsay",
+ "ember-cli",
+ ])) {
+ CommandArg::Global(GlobalCommand::Uninstall(uninstall)) => {
+ assert_eq!(uninstall.tools, vec!["typescript", "cowsay", "ember-cli"]);
+ }
+ _ => panic!("Doesn't parse global remove as a global"),
+ };
+ }
+
+ #[test]
+ fn processes_flags() {
+ match CommandArg::for_yarn(&arg_list([
+ "global",
+ "--silent",
+ "add",
+ "ember-cli",
+ "--prefix=~/",
+ "typescript",
+ ])) {
+ CommandArg::Global(GlobalCommand::Install(install)) => {
+ // The commands get moved to the front of common_args
+ assert_eq!(
+ install.common_args,
+ vec!["global", "add", "--silent", "--prefix=~/"]
+ );
+ assert_eq!(install.tools, vec!["ember-cli", "typescript"]);
+ }
+ _ => panic!("Doesn't parse global add as a global"),
+ };
+
+ match CommandArg::for_yarn(&arg_list([
+ "global",
+ "--silent",
+ "remove",
+ "ember-cli",
+ "--prefix=~/",
+ "typescript",
+ ])) {
+ CommandArg::Global(GlobalCommand::Uninstall(uninstall)) => {
+ assert_eq!(uninstall.tools, vec!["ember-cli", "typescript"]);
+ }
+ _ => panic!("Doesn't parse global add as a global"),
+ };
+ }
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +
use std::env;
+use std::ffi::OsString;
+
+use super::executor::{Executor, ToolCommand, ToolKind};
+use super::{debug_active_image, debug_no_platform, RECURSION_ENV_VAR};
+use crate::error::{ErrorKind, Fallible};
+use crate::platform::{Platform, Source, System};
+use crate::session::{ActivityKind, Session};
+
+pub(super) fn command(args: &[OsString], session: &mut Session) -> Fallible<Executor> {
+ session.add_event_start(ActivityKind::Pnpm);
+ // Don't re-evaluate the context or global install interception if this is a recursive call
+ let platform = match env::var_os(RECURSION_ENV_VAR) {
+ Some(_) => None,
+ None => {
+ // FIXME: Figure out how to intercept pnpm global commands properly.
+ // This guard prevents all global commands from running, it should
+ // be removed when we fully implement global command interception.
+ let is_global = args.iter().any(|f| f == "--global" || f == "-g");
+ if is_global {
+ return Err(ErrorKind::Unimplemented {
+ feature: "pnpm global commands".into(),
+ }
+ .into());
+ }
+
+ Platform::current(session)?
+ }
+ };
+
+ Ok(ToolCommand::new("pnpm", args, platform, ToolKind::Pnpm).into())
+}
+
+/// Determine the execution context (PATH and failure error message) for pnpm
+pub(super) fn execution_context(
+ platform: Option<Platform>,
+ session: &mut Session,
+) -> Fallible<(OsString, ErrorKind)> {
+ match platform {
+ Some(plat) => {
+ validate_platform_pnpm(&plat)?;
+
+ let image = plat.checkout(session)?;
+ let path = image.path()?;
+ debug_active_image(&image);
+
+ Ok((path, ErrorKind::BinaryExecError))
+ }
+ None => {
+ let path = System::path()?;
+ debug_no_platform();
+ Ok((path, ErrorKind::NoPlatform))
+ }
+ }
+}
+
+fn validate_platform_pnpm(platform: &Platform) -> Fallible<()> {
+ match &platform.pnpm {
+ Some(_) => Ok(()),
+ None => match platform.node.source {
+ Source::Project => Err(ErrorKind::NoProjectPnpm.into()),
+ Source::Default | Source::Binary => Err(ErrorKind::NoDefaultPnpm.into()),
+ Source::CommandLine => Err(ErrorKind::NoCommandLinePnpm.into()),
+ },
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +
use std::env;
+use std::ffi::OsString;
+
+use super::executor::{Executor, ToolCommand, ToolKind};
+use super::parser::CommandArg;
+use super::{debug_active_image, debug_no_platform, RECURSION_ENV_VAR};
+use crate::error::{ErrorKind, Fallible};
+use crate::platform::{Platform, Source, System};
+use crate::session::{ActivityKind, Session};
+
+/// Build an `Executor` for Yarn
+///
+/// If the command is a global add or remove and we have a default platform available, then we will
+/// use custom logic to ensure that the package is correctly installed / uninstalled in the Volta
+/// directory.
+///
+/// If the command is _not_ a global add / remove or we don't have a default platform, then
+/// we will allow Yarn to execute the command as usual.
+pub(super) fn command(args: &[OsString], session: &mut Session) -> Fallible<Executor> {
+ session.add_event_start(ActivityKind::Yarn);
+ // Don't re-evaluate the context or global install interception if this is a recursive call
+ let platform = match env::var_os(RECURSION_ENV_VAR) {
+ Some(_) => None,
+ None => {
+ if let CommandArg::Global(cmd) = CommandArg::for_yarn(args) {
+ // For globals, only intercept if the default platform exists
+ if let Some(default_platform) = session.default_platform()? {
+ return cmd.executor(default_platform);
+ }
+ }
+
+ Platform::current(session)?
+ }
+ };
+
+ Ok(ToolCommand::new("yarn", args, platform, ToolKind::Yarn).into())
+}
+
+/// Determine the execution context (PATH and failure error message) for Yarn
+pub(super) fn execution_context(
+ platform: Option<Platform>,
+ session: &mut Session,
+) -> Fallible<(OsString, ErrorKind)> {
+ match platform {
+ Some(plat) => {
+ validate_platform_yarn(&plat)?;
+
+ let image = plat.checkout(session)?;
+ let path = image.path()?;
+ debug_active_image(&image);
+
+ Ok((path, ErrorKind::BinaryExecError))
+ }
+ None => {
+ let path = System::path()?;
+ debug_no_platform();
+ Ok((path, ErrorKind::NoPlatform))
+ }
+ }
+}
+
+fn validate_platform_yarn(platform: &Platform) -> Fallible<()> {
+ match &platform.yarn {
+ Some(_) => Ok(()),
+ None => match platform.node.source {
+ Source::Project => Err(ErrorKind::NoProjectYarn.into()),
+ Source::Default | Source::Binary => Err(ErrorKind::NoDefaultYarn.into()),
+ Source::CommandLine => Err(ErrorKind::NoCommandLineYarn.into()),
+ },
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +
//! Provides the `Session` type, which represents the user's state during an
+//! execution of a Volta tool, including their current directory, Volta
+//! hook configuration, and the state of the local inventory.
+
+use std::fmt::{self, Display, Formatter};
+use std::process::exit;
+
+use crate::error::{ExitCode, Fallible, VoltaError};
+use crate::event::EventLog;
+use crate::hook::{HookConfig, LazyHookConfig};
+use crate::platform::PlatformSpec;
+use crate::project::{LazyProject, Project};
+use crate::toolchain::{LazyToolchain, Toolchain};
+use log::debug;
+
+#[derive(Eq, PartialEq, Ord, PartialOrd, Clone, Copy)]
+pub enum ActivityKind {
+ Fetch,
+ Install,
+ Uninstall,
+ List,
+ Current,
+ Default,
+ Pin,
+ Node,
+ Npm,
+ Npx,
+ Pnpm,
+ Yarn,
+ Volta,
+ Tool,
+ Help,
+ Version,
+ Binary,
+ Shim,
+ Completions,
+ Which,
+ Setup,
+ Run,
+ Args,
+}
+
+impl Display for ActivityKind {
+ fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {
+ let s = match self {
+ ActivityKind::Fetch => "fetch",
+ ActivityKind::Install => "install",
+ ActivityKind::Uninstall => "uninstall",
+ ActivityKind::List => "list",
+ ActivityKind::Current => "current",
+ ActivityKind::Default => "default",
+ ActivityKind::Pin => "pin",
+ ActivityKind::Node => "node",
+ ActivityKind::Npm => "npm",
+ ActivityKind::Npx => "npx",
+ ActivityKind::Pnpm => "pnpm",
+ ActivityKind::Yarn => "yarn",
+ ActivityKind::Volta => "volta",
+ ActivityKind::Tool => "tool",
+ ActivityKind::Help => "help",
+ ActivityKind::Version => "version",
+ ActivityKind::Binary => "binary",
+ ActivityKind::Setup => "setup",
+ ActivityKind::Shim => "shim",
+ ActivityKind::Completions => "completions",
+ ActivityKind::Which => "which",
+ ActivityKind::Run => "run",
+ ActivityKind::Args => "args",
+ };
+ f.write_str(s)
+ }
+}
+
+/// Represents the user's state during an execution of a Volta tool. The session
+/// encapsulates a number of aspects of the environment in which the tool was
+/// invoked, including:
+///
+/// - the current directory
+/// - the Node project tree that contains the current directory (if any)
+/// - the Volta hook configuration
+/// - the inventory of locally-fetched Volta tools
+pub struct Session {
+ hooks: LazyHookConfig,
+ toolchain: LazyToolchain,
+ project: LazyProject,
+ event_log: EventLog,
+}
+
+impl Session {
+ /// Constructs a new `Session`.
+ pub fn init() -> Session {
+ Session {
+ hooks: LazyHookConfig::init(),
+ toolchain: LazyToolchain::init(),
+ project: LazyProject::init(),
+ event_log: EventLog::init(),
+ }
+ }
+
+ /// Produces a reference to the current Node project, if any.
+ pub fn project(&self) -> Fallible<Option<&Project>> {
+ self.project.get()
+ }
+
+ /// Produces a mutable reference to the current Node project, if any.
+ pub fn project_mut(&mut self) -> Fallible<Option<&mut Project>> {
+ self.project.get_mut()
+ }
+
+ /// Returns the user's default platform, if any
+ pub fn default_platform(&self) -> Fallible<Option<&PlatformSpec>> {
+ self.toolchain.get().map(Toolchain::platform)
+ }
+
+ /// Returns the current project's pinned platform image, if any.
+ pub fn project_platform(&self) -> Fallible<Option<&PlatformSpec>> {
+ if let Some(project) = self.project()? {
+ return Ok(project.platform());
+ }
+ Ok(None)
+ }
+
+ /// Produces a reference to the current toolchain (default platform specification)
+ pub fn toolchain(&self) -> Fallible<&Toolchain> {
+ self.toolchain.get()
+ }
+
+ /// Produces a mutable reference to the current toolchain
+ pub fn toolchain_mut(&mut self) -> Fallible<&mut Toolchain> {
+ self.toolchain.get_mut()
+ }
+
+ /// Produces a reference to the hook configuration
+ pub fn hooks(&self) -> Fallible<&HookConfig> {
+ self.hooks.get(self.project()?)
+ }
+
+ pub fn add_event_start(&mut self, activity_kind: ActivityKind) {
+ self.event_log.add_event_start(activity_kind)
+ }
+ pub fn add_event_end(&mut self, activity_kind: ActivityKind, exit_code: ExitCode) {
+ self.event_log.add_event_end(activity_kind, exit_code)
+ }
+ pub fn add_event_tool_end(&mut self, activity_kind: ActivityKind, exit_code: i32) {
+ self.event_log.add_event_tool_end(activity_kind, exit_code)
+ }
+ pub fn add_event_error(&mut self, activity_kind: ActivityKind, error: &VoltaError) {
+ self.event_log.add_event_error(activity_kind, error)
+ }
+
+ fn publish_to_event_log(self) {
+ let Self {
+ project,
+ hooks,
+ mut event_log,
+ ..
+ } = self;
+ let plugin_res = project
+ .get()
+ .and_then(|p| hooks.get(p))
+ .map(|hooks| hooks.events().and_then(|e| e.publish.as_ref()));
+ match plugin_res {
+ Ok(plugin) => {
+ event_log.add_event_args();
+ event_log.publish(plugin);
+ }
+ Err(e) => {
+ debug!("Unable to publish event log.\n{}", e);
+ }
+ }
+ }
+
+ pub fn exit(self, code: ExitCode) -> ! {
+ self.publish_to_event_log();
+ code.exit();
+ }
+
+ pub fn exit_tool(self, code: i32) -> ! {
+ self.publish_to_event_log();
+ exit(code);
+ }
+}
+
+#[cfg(test)]
+pub mod tests {
+
+ use crate::session::Session;
+ use std::env;
+ use std::path::PathBuf;
+
+ fn fixture_path(fixture_dir: &str) -> PathBuf {
+ let mut cargo_manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
+ cargo_manifest_dir.push("fixtures");
+ cargo_manifest_dir.push(fixture_dir);
+ cargo_manifest_dir
+ }
+
+ #[test]
+ fn test_in_pinned_project() {
+ let project_pinned = fixture_path("basic");
+ env::set_current_dir(project_pinned).expect("Could not set current directory");
+ let pinned_session = Session::init();
+ let pinned_platform = pinned_session
+ .project_platform()
+ .expect("Couldn't create Project");
+ assert!(pinned_platform.is_some());
+
+ let project_unpinned = fixture_path("no_toolchain");
+ env::set_current_dir(project_unpinned).expect("Could not set current directory");
+ let unpinned_session = Session::init();
+ let unpinned_platform = unpinned_session
+ .project_platform()
+ .expect("Couldn't create Project");
+ assert!(unpinned_platform.is_none());
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +
//! Provides utilities for modifying shims for 3rd-party executables
+
+use std::collections::HashSet;
+use std::fs;
+use std::io;
+use std::path::Path;
+
+use crate::error::{Context, ErrorKind, Fallible, VoltaError};
+use crate::fs::read_dir_eager;
+use crate::layout::volta_home;
+use crate::sync::VoltaLock;
+use log::debug;
+
+pub use platform::create;
+
+pub fn regenerate_shims_for_dir(dir: &Path) -> Fallible<()> {
+ // Acquire a lock on the Volta directory, if possible, to prevent concurrent changes
+ let _lock = VoltaLock::acquire();
+ debug!("Rebuilding shims for directory: {}", dir.display());
+ for shim_name in get_shim_list_deduped(dir)?.iter() {
+ delete(shim_name)?;
+ create(shim_name)?;
+ }
+
+ Ok(())
+}
+
+fn get_shim_list_deduped(dir: &Path) -> Fallible<HashSet<String>> {
+ let contents = read_dir_eager(dir).with_context(|| ErrorKind::ReadDirError {
+ dir: dir.to_owned(),
+ })?;
+
+ #[cfg(unix)]
+ {
+ let mut shims: HashSet<String> =
+ contents.filter_map(platform::entry_to_shim_name).collect();
+ shims.insert("node".into());
+ shims.insert("npm".into());
+ shims.insert("npx".into());
+ shims.insert("pnpm".into());
+ shims.insert("yarn".into());
+ shims.insert("yarnpkg".into());
+ Ok(shims)
+ }
+
+ #[cfg(windows)]
+ {
+ // On Windows, the default shims are installed in Program Files, so we don't need to generate them here
+ Ok(contents.filter_map(platform::entry_to_shim_name).collect())
+ }
+}
+
+#[derive(PartialEq, Eq)]
+pub enum ShimResult {
+ Created,
+ AlreadyExists,
+ Deleted,
+ DoesntExist,
+}
+
+pub fn delete(shim_name: &str) -> Fallible<ShimResult> {
+ let shim = volta_home()?.shim_file(shim_name);
+
+ #[cfg(windows)]
+ platform::delete_git_bash_script(shim_name)?;
+
+ match fs::remove_file(shim) {
+ Ok(_) => Ok(ShimResult::Deleted),
+ Err(err) => {
+ if err.kind() == io::ErrorKind::NotFound {
+ Ok(ShimResult::DoesntExist)
+ } else {
+ Err(VoltaError::from_source(
+ err,
+ ErrorKind::ShimRemoveError {
+ name: shim_name.to_string(),
+ },
+ ))
+ }
+ }
+ }
+}
+
+#[cfg(unix)]
+mod platform {
+ //! Unix-specific shim utilities
+ //!
+ //! On macOS and Linux, creating a shim involves creating a symlink to the `volta-shim`
+ //! executable. Additionally, filtering the shims from directory entries means looking
+ //! for symlinks and ignoring the actual binaries
+ use std::ffi::OsStr;
+ use std::fs::{DirEntry, Metadata};
+ use std::io;
+
+ use super::ShimResult;
+ use crate::error::{ErrorKind, Fallible, VoltaError};
+ use crate::fs::symlink_file;
+ use crate::layout::{volta_home, volta_install};
+
+ pub fn create(shim_name: &str) -> Fallible<ShimResult> {
+ let executable = volta_install()?.shim_executable();
+ let shim = volta_home()?.shim_file(shim_name);
+
+ match symlink_file(executable, shim) {
+ Ok(_) => Ok(ShimResult::Created),
+ Err(err) => {
+ if err.kind() == io::ErrorKind::AlreadyExists {
+ Ok(ShimResult::AlreadyExists)
+ } else {
+ Err(VoltaError::from_source(
+ err,
+ ErrorKind::ShimCreateError {
+ name: shim_name.to_string(),
+ },
+ ))
+ }
+ }
+ }
+ }
+
+ pub fn entry_to_shim_name((entry, metadata): (DirEntry, Metadata)) -> Option<String> {
+ if metadata.file_type().is_symlink() {
+ entry
+ .path()
+ .file_stem()
+ .and_then(OsStr::to_str)
+ .map(ToOwned::to_owned)
+ } else {
+ None
+ }
+ }
+}
+
+#[cfg(windows)]
+mod platform {
+ //! Windows-specific shim utilities
+ //!
+ //! On Windows, creating a shim involves creating a small .cmd script, rather than a symlink.
+ //! This allows us to create shims without requiring administrator privileges or developer
+ //! mode. Also, to support Git Bash, we create a similar script with bash syntax that doesn't
+ //! have a file extension. This allows Powershell and Cmd to ignore it, while Bash detects it
+ //! as an executable script.
+ //!
+ //! Finally, filtering directory entries to find the shim files involves looking for the .cmd
+ //! files.
+ use std::ffi::OsStr;
+ use std::fs::{write, DirEntry, Metadata};
+
+ use super::ShimResult;
+ use crate::error::{Context, ErrorKind, Fallible};
+ use crate::fs::remove_file_if_exists;
+ use crate::layout::volta_home;
+
+ const SHIM_SCRIPT_CONTENTS: &str = r#"@echo off
+volta run %~n0 %*
+"#;
+
+ const GIT_BASH_SCRIPT_CONTENTS: &str = r#"#!/bin/bash
+volta run "$(basename $0)" "$@""#;
+
+ pub fn create(shim_name: &str) -> Fallible<ShimResult> {
+ let shim = volta_home()?.shim_file(shim_name);
+
+ write(shim, SHIM_SCRIPT_CONTENTS).with_context(|| ErrorKind::ShimCreateError {
+ name: shim_name.to_owned(),
+ })?;
+
+ let git_bash_script = volta_home()?.shim_git_bash_script_file(shim_name);
+
+ write(git_bash_script, GIT_BASH_SCRIPT_CONTENTS).with_context(|| {
+ ErrorKind::ShimCreateError {
+ name: shim_name.to_owned(),
+ }
+ })?;
+
+ Ok(ShimResult::Created)
+ }
+
+ pub fn entry_to_shim_name((entry, _): (DirEntry, Metadata)) -> Option<String> {
+ let path = entry.path();
+
+ if path.extension().is_some_and(|ext| ext == "cmd") {
+ path.file_stem()
+ .and_then(OsStr::to_str)
+ .map(ToOwned::to_owned)
+ } else {
+ None
+ }
+ }
+
+ pub fn delete_git_bash_script(shim_name: &str) -> Fallible<()> {
+ let script_path = volta_home()?.shim_git_bash_script_file(shim_name);
+ remove_file_if_exists(script_path).with_context(|| ErrorKind::ShimRemoveError {
+ name: shim_name.to_string(),
+ })
+ }
+}
+
use std::process::exit;
+use std::sync::atomic::{AtomicBool, Ordering};
+
+use log::debug;
+
+static SHIM_HAS_CONTROL: AtomicBool = AtomicBool::new(false);
+const INTERRUPTED_EXIT_CODE: i32 = 130;
+
+pub fn pass_control_to_shim() {
+ SHIM_HAS_CONTROL.store(true, Ordering::SeqCst);
+}
+
+pub fn setup_signal_handler() {
+ let result = ctrlc::set_handler(|| {
+ if !SHIM_HAS_CONTROL.load(Ordering::SeqCst) {
+ exit(INTERRUPTED_EXIT_CODE);
+ }
+ });
+
+ if result.is_err() {
+ debug!("Unable to set Ctrl+C handler, SIGINT will not be handled correctly");
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +
//! The view layer of Volta, with utilities for styling command-line output.
+use std::borrow::Cow;
+use std::error::Error;
+use std::time::Duration;
+
+use archive::Origin;
+use cfg_if::cfg_if;
+use console::{style, StyledObject};
+use indicatif::{ProgressBar, ProgressStyle};
+use terminal_size::{terminal_size, Width};
+
+pub const MAX_WIDTH: usize = 100;
+const MAX_PROGRESS_WIDTH: usize = 40;
+
+/// Generate the styled prefix for a success message
+pub fn success_prefix() -> StyledObject<&'static str> {
+ style("success:").green().bold()
+}
+
+/// Generate the styled prefix for a note
+pub fn note_prefix() -> StyledObject<&'static str> {
+ style(" note:").magenta().bold()
+}
+
+/// Format the underlying cause of an error
+pub(crate) fn format_error_cause(inner: &dyn Error) -> String {
+ format!(
+ "{}{} {}",
+ style("Error cause").underlined().bold(),
+ style(":").bold(),
+ inner
+ )
+}
+
+/// Determines the string to display based on the Origin of the operation.
+fn action_str(origin: Origin) -> &'static str {
+ match origin {
+ Origin::Local => "Unpacking",
+ Origin::Remote => "Fetching",
+ }
+}
+
+pub fn tool_version<N, V>(name: N, version: V) -> String
+where
+ N: std::fmt::Display + Sized,
+ V: std::fmt::Display + Sized,
+{
+ format!("{:}@{:}", name, version)
+}
+
+/// Get the width of the terminal, limited to a maximum of MAX_WIDTH
+pub fn text_width() -> Option<usize> {
+ terminal_size().map(|(Width(w), _)| (w as usize).min(MAX_WIDTH))
+}
+
+/// Constructs a command-line progress bar based on the specified Origin enum
+/// (e.g., `Origin::Remote`), details string (e.g., `"v1.23.4"`), and logical
+/// length (i.e., the number of logical progress steps in the process being
+/// visualized by the progress bar).
+pub fn progress_bar(origin: Origin, details: &str, len: u64) -> ProgressBar {
+ let action = action_str(origin);
+ let action_width = action.len() + 2; // plus 2 spaces to look nice
+ let msg_width = action_width + 1 + details.len();
+
+ // Fetching node@9.11.2 [=============> ] 34%
+ // |--------| |---------| |--------------------------------------| |-|
+ // action details bar percentage
+ let bar_width = match text_width() {
+ Some(width) => MAX_PROGRESS_WIDTH.min(width - 2 - msg_width - 2 - 2 - 1 - 3 - 1),
+ None => MAX_PROGRESS_WIDTH,
+ };
+
+ let progress = ProgressBar::new(len);
+
+ progress.set_message(format!(
+ "{: >width$} {}",
+ style(action).green().bold(),
+ details,
+ width = action_width,
+ ));
+ progress.set_style(
+ ProgressStyle::default_bar()
+ .template(&format!(
+ "{{msg}} [{{bar:{}.cyan/blue}}] {{percent:>3}}%",
+ bar_width
+ ))
+ .expect("template is valid")
+ .progress_chars("=> "),
+ );
+
+ progress
+}
+
+cfg_if! {
+ if #[cfg(windows)] {
+ /// Constructs a command-line progress spinner with the specified "message"
+ /// string. The spinner is ticked by default every 100ms.
+ pub fn progress_spinner<S>(message: S) -> ProgressBar
+ where
+ S: Into<Cow<'static, str>>,
+ {
+ let spinner = ProgressBar::new_spinner();
+ // Windows CMD prompt doesn't support Unicode characters, so use a simplified spinner
+ let style = ProgressStyle::default_spinner().tick_chars(r#"-\|/-"#);
+
+ spinner.set_message(message);
+ spinner.set_style(style);
+ spinner.enable_steady_tick(Duration::from_millis(100));
+
+ spinner
+ }
+ } else {
+ /// Constructs a command-line progress spinner with the specified "message"
+ /// string. The spinner is ticked by default every 50ms.
+ pub fn progress_spinner<S>(message: S) -> ProgressBar
+ where
+ S: Into<Cow<'static, str>>,
+ {
+ // ⠋ Fetching public registry: https://nodejs.org/dist/index.json
+ let spinner = ProgressBar::new_spinner();
+
+ spinner.set_message(message);
+ spinner.set_style(ProgressStyle::default_spinner());
+ spinner.enable_steady_tick(Duration::from_millis(50));
+
+ spinner
+ }
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +
//! Inter-process locking on the Volta directory
+//!
+//! To avoid issues where multiple separate invocations of Volta modify the
+//! data directory simultaneously, we provide a locking mechanism that only
+//! allows a single process to modify the directory at a time.
+//!
+//! However, within a single process, we may attempt to lock the directory in
+//! different code paths. For example, when installing a package we require a
+//! lock, however we also may need to install Node, which requires a lock as
+//! well. To avoid deadlocks in those situations, we track the state of the
+//! lock globally:
+//!
+//! - If a lock is requested and no locks are active, then we acquire a file
+//! lock on the `volta.lock` file and initialize the state with a count of 1
+//! - If a lock already exists, then we increment the count of active locks
+//! - When a lock is no longer needed, we decrement the count of active locks
+//! - When the last lock is released, we release the file lock and clear the
+//! global lock state.
+//!
+//! This allows multiple code paths to request a lock and not worry about
+//! potential deadlocks, while still preventing multiple processes from making
+//! concurrent changes.
+
+use std::fs::{File, OpenOptions};
+use std::marker::PhantomData;
+use std::ops::Drop;
+use std::sync::Mutex;
+
+use crate::error::{Context, ErrorKind, Fallible};
+use crate::layout::volta_home;
+use crate::style::progress_spinner;
+use fs2::FileExt;
+use log::debug;
+use once_cell::sync::Lazy;
+
+static LOCK_STATE: Lazy<Mutex<Option<LockState>>> = Lazy::new(|| Mutex::new(None));
+
+/// The current state of locks for this process.
+///
+/// Note: To ensure thread safety _within_ this process, we enclose the
+/// state in a Mutex. This Mutex and it's associated locks are separate
+/// from the overall process lock and are only used to ensure the count
+/// is accurately maintained within a given process.
+struct LockState {
+ file: File,
+ count: usize,
+}
+
+const LOCK_FILE: &str = "volta.lock";
+
+/// An RAII implementation of a process lock on the Volta directory. A given Volta process can have
+/// multiple active locks, but only one process can have any locks at a time.
+///
+/// Once all of the `VoltaLock` objects go out of scope, the lock will be released to other
+/// processes.
+pub struct VoltaLock {
+ // Private field ensures that this cannot be created except for with the `acquire()` method
+ _private: PhantomData<()>,
+}
+
+impl VoltaLock {
+ pub fn acquire() -> Fallible<Self> {
+ let mut state = LOCK_STATE
+ .lock()
+ .with_context(|| ErrorKind::LockAcquireError)?;
+
+ // Check if there is an active lock for this process. If so, increment
+ // the count of active locks. If not, create a file lock and initialize
+ // the state with a count of 1
+ match &mut *state {
+ Some(inner) => {
+ inner.count += 1;
+ }
+ None => {
+ let path = volta_home()?.root().join(LOCK_FILE);
+ debug!("Acquiring lock on Volta directory: {}", path.display());
+
+ let file = OpenOptions::new()
+ .write(true)
+ .create(true)
+ .open(path)
+ .with_context(|| ErrorKind::LockAcquireError)?;
+ // First we try to lock the file without blocking. If that fails, then we show a spinner
+ // and block until the lock completes.
+ if file.try_lock_exclusive().is_err() {
+ let spinner = progress_spinner("Waiting for file lock on Volta directory");
+ // Note: Blocks until the file can be locked
+ let lock_result = file
+ .lock_exclusive()
+ .with_context(|| ErrorKind::LockAcquireError);
+ spinner.finish_and_clear();
+ lock_result?;
+ }
+
+ *state = Some(LockState { file, count: 1 });
+ }
+ }
+
+ Ok(Self {
+ _private: PhantomData,
+ })
+ }
+}
+
+impl Drop for VoltaLock {
+ fn drop(&mut self) {
+ // On drop, decrement the count of active locks. If the count is 1,
+ // then this is the last active lock, so instead unlock the file and
+ // clear out the lock state.
+ if let Ok(mut state) = LOCK_STATE.lock() {
+ match &mut *state {
+ Some(inner) => {
+ if inner.count == 1 {
+ debug!("Unlocking Volta Directory");
+ let _ = inner.file.unlock();
+ *state = None;
+ } else {
+ inner.count -= 1;
+ }
+ }
+ None => {
+ debug!("Unexpected unlock of Volta directory when it wasn't locked");
+ }
+ }
+ }
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +
use std::env;
+use std::fmt::{self, Display};
+use std::path::PathBuf;
+
+use crate::error::{ErrorKind, Fallible};
+use crate::layout::volta_home;
+use crate::session::Session;
+use crate::style::{note_prefix, success_prefix, tool_version};
+use crate::sync::VoltaLock;
+use crate::version::VersionSpec;
+use crate::VOLTA_FEATURE_PNPM;
+use cfg_if::cfg_if;
+use log::{debug, info};
+
+pub mod node;
+pub mod npm;
+pub mod package;
+pub mod pnpm;
+mod registry;
+mod serial;
+pub mod yarn;
+
+pub use node::{
+ load_default_npm_version, Node, NODE_DISTRO_ARCH, NODE_DISTRO_EXTENSION, NODE_DISTRO_OS,
+};
+pub use npm::{BundledNpm, Npm};
+pub use package::{BinConfig, Package, PackageConfig, PackageManifest};
+pub use pnpm::Pnpm;
+pub use registry::PackageDetails;
+pub use yarn::Yarn;
+
+fn debug_already_fetched<T: Display>(tool: T) {
+ debug!("{} has already been fetched, skipping download", tool);
+}
+
+fn info_installed<T: Display>(tool: T) {
+ info!("{} installed and set {tool} as default", success_prefix());
+}
+
+fn info_fetched<T: Display>(tool: T) {
+ info!("{} fetched {tool}", success_prefix());
+}
+
+fn info_pinned<T: Display>(tool: T) {
+ info!("{} pinned {tool} in package.json", success_prefix());
+}
+
+fn info_project_version<P, D>(project_version: P, default_version: D)
+where
+ P: Display,
+ D: Display,
+{
+ info!(
+ r#"{} you are using {project_version} in the current project; to
+ instead use {default_version}, run `volta pin {default_version}`"#,
+ note_prefix()
+ );
+}
+
+/// Trait representing all of the actions that can be taken with a tool
+pub trait Tool: Display {
+ /// Fetch a Tool into the local inventory
+ fn fetch(self: Box<Self>, session: &mut Session) -> Fallible<()>;
+ /// Install a tool, making it the default so it is available everywhere on the user's machine
+ fn install(self: Box<Self>, session: &mut Session) -> Fallible<()>;
+ /// Pin a tool in the local project so that it is usable within the project
+ fn pin(self: Box<Self>, session: &mut Session) -> Fallible<()>;
+}
+
+/// Specification for a tool and its associated version.
+#[derive(Debug)]
+#[cfg_attr(test, derive(PartialEq, Eq))]
+pub enum Spec {
+ Node(VersionSpec),
+ Npm(VersionSpec),
+ Pnpm(VersionSpec),
+ Yarn(VersionSpec),
+ Package(String, VersionSpec),
+}
+
+impl Spec {
+ /// Resolve a tool spec into a fully realized Tool that can be fetched
+ pub fn resolve(self, session: &mut Session) -> Fallible<Box<dyn Tool>> {
+ match self {
+ Spec::Node(version) => {
+ let version = node::resolve(version, session)?;
+ Ok(Box::new(Node::new(version)))
+ }
+ Spec::Npm(version) => match npm::resolve(version, session)? {
+ Some(version) => Ok(Box::new(Npm::new(version))),
+ None => Ok(Box::new(BundledNpm)),
+ },
+ Spec::Pnpm(version) => {
+ // If the pnpm feature flag is set, use the special-cased package manager logic
+ // to handle resolving (and ultimately fetching / installing) pnpm. If not, then
+ // fall back to the global package behavior, which was the case prior to pnpm
+ // support being added
+ if env::var_os(VOLTA_FEATURE_PNPM).is_some() {
+ let version = pnpm::resolve(version, session)?;
+ Ok(Box::new(Pnpm::new(version)))
+ } else {
+ let package = Package::new("pnpm".to_owned(), version)?;
+ Ok(Box::new(package))
+ }
+ }
+ Spec::Yarn(version) => {
+ let version = yarn::resolve(version, session)?;
+ Ok(Box::new(Yarn::new(version)))
+ }
+ // When using global package install, we allow the package manager to perform the version resolution
+ Spec::Package(name, version) => {
+ let package = Package::new(name, version)?;
+ Ok(Box::new(package))
+ }
+ }
+ }
+
+ /// Uninstall a tool, removing it from the local inventory
+ ///
+ /// This is implemented on Spec, instead of Resolved, because there is currently no need to
+ /// resolve the specific version before uninstalling a tool.
+ pub fn uninstall(self) -> Fallible<()> {
+ match self {
+ Spec::Node(_) => Err(ErrorKind::Unimplemented {
+ feature: "Uninstalling node".into(),
+ }
+ .into()),
+ Spec::Npm(_) => Err(ErrorKind::Unimplemented {
+ feature: "Uninstalling npm".into(),
+ }
+ .into()),
+ Spec::Pnpm(_) => {
+ if env::var_os(VOLTA_FEATURE_PNPM).is_some() {
+ Err(ErrorKind::Unimplemented {
+ feature: "Uninstalling pnpm".into(),
+ }
+ .into())
+ } else {
+ package::uninstall("pnpm")
+ }
+ }
+ Spec::Yarn(_) => Err(ErrorKind::Unimplemented {
+ feature: "Uninstalling yarn".into(),
+ }
+ .into()),
+ Spec::Package(name, _) => package::uninstall(&name),
+ }
+ }
+
+ /// The name of the tool, without the version, used for messaging
+ pub fn name(&self) -> &str {
+ match self {
+ Spec::Node(_) => "Node",
+ Spec::Npm(_) => "npm",
+ Spec::Pnpm(_) => "pnpm",
+ Spec::Yarn(_) => "Yarn",
+ Spec::Package(name, _) => name,
+ }
+ }
+}
+
+impl Display for Spec {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ let s = match self {
+ Spec::Node(ref version) => tool_version("node", version),
+ Spec::Npm(ref version) => tool_version("npm", version),
+ Spec::Pnpm(ref version) => tool_version("pnpm", version),
+ Spec::Yarn(ref version) => tool_version("yarn", version),
+ Spec::Package(ref name, ref version) => tool_version(name, version),
+ };
+ f.write_str(&s)
+ }
+}
+
+/// Represents the result of checking if a tool is available locally or not
+///
+/// If a fetch is required, will include an exclusive lock on the Volta directory where possible
+enum FetchStatus {
+ AlreadyFetched,
+ FetchNeeded(Option<VoltaLock>),
+}
+
+/// Uses the supplied `already_fetched` predicate to determine if a tool is available or not.
+///
+/// This uses double-checking logic, to correctly handle concurrent fetch requests:
+///
+/// - If `already_fetched` indicates that a fetch is needed, we acquire an exclusive lock on the Volta directory
+/// - Then, we check _again_, to confirm that no other process completed the fetch while we waited for the lock
+///
+/// Note: If acquiring the lock fails, we proceed anyway, since the fetch is still necessary.
+fn check_fetched<F>(already_fetched: F) -> Fallible<FetchStatus>
+where
+ F: Fn() -> Fallible<bool>,
+{
+ if !already_fetched()? {
+ let lock = match VoltaLock::acquire() {
+ Ok(l) => Some(l),
+ Err(_) => {
+ debug!("Unable to acquire lock on Volta directory!");
+ None
+ }
+ };
+
+ if !already_fetched()? {
+ Ok(FetchStatus::FetchNeeded(lock))
+ } else {
+ Ok(FetchStatus::AlreadyFetched)
+ }
+ } else {
+ Ok(FetchStatus::AlreadyFetched)
+ }
+}
+
+fn download_tool_error(tool: Spec, from_url: impl AsRef<str>) -> impl FnOnce() -> ErrorKind {
+ let from_url = from_url.as_ref().to_string();
+ || ErrorKind::DownloadToolNetworkError { tool, from_url }
+}
+
+fn registry_fetch_error(
+ tool: impl AsRef<str>,
+ from_url: impl AsRef<str>,
+) -> impl FnOnce() -> ErrorKind {
+ let tool = tool.as_ref().to_string();
+ let from_url = from_url.as_ref().to_string();
+ || ErrorKind::RegistryFetchError { tool, from_url }
+}
+
+cfg_if!(
+ if #[cfg(windows)] {
+ const PATH_VAR_NAME: &str = "Path";
+ } else {
+ const PATH_VAR_NAME: &str = "PATH";
+ }
+);
+
+/// Check if a newly-installed shim is first on the PATH. If it isn't, we want to inform the user
+/// that they'll want to move it to the start of PATH to make sure things work as expected.
+pub fn check_shim_reachable(shim_name: &str) {
+ let Some(expected_dir) = find_expected_shim_dir(shim_name) else {
+ return;
+ };
+
+ let Ok(resolved) = which::which(shim_name) else {
+ info!(
+ "{} cannot find command {}. Please ensure that {} is available on your {}.",
+ note_prefix(),
+ shim_name,
+ expected_dir.display(),
+ PATH_VAR_NAME,
+ );
+ return;
+ };
+
+ if !resolved.starts_with(&expected_dir) {
+ info!(
+ "{} {} is shadowed by another binary of the same name at {}. To ensure your commands work as expected, please move {} to the start of your {}.",
+ note_prefix(),
+ shim_name,
+ resolved.display(),
+ expected_dir.display(),
+ PATH_VAR_NAME
+ );
+ }
+}
+
+/// Locate the base directory for the relevant shim in the Volta directories.
+///
+/// On Unix, all of the shims, including the default ones, are installed in `VoltaHome::shim_dir`
+#[cfg(unix)]
+fn find_expected_shim_dir(_shim_name: &str) -> Option<PathBuf> {
+ volta_home().ok().map(|home| home.shim_dir().to_owned())
+}
+
+/// Locate the base directory for the relevant shim in the Volta directories.
+///
+/// On Windows, the default shims (node, npm, yarn, etc.) are installed in `Program Files`
+/// alongside the Volta binaries. To determine where we should be checking, we first look for the
+/// relevant shim inside of `VoltaHome::shim_dir`. If it's there, we use that directory. If it
+/// isn't, we assume it must be a default shim and return `VoltaInstall::root`, which is where
+/// Volta itself is installed.
+#[cfg(windows)]
+fn find_expected_shim_dir(shim_name: &str) -> Option<PathBuf> {
+ use crate::layout::volta_install;
+
+ let home = volta_home().ok()?;
+
+ if home.shim_file(shim_name).exists() {
+ Some(home.shim_dir().to_owned())
+ } else {
+ volta_install()
+ .ok()
+ .map(|install| install.root().to_owned())
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +
//! Provides fetcher for Node distributions
+
+use std::fs::{read_to_string, write, File};
+use std::path::{Path, PathBuf};
+
+use super::NodeVersion;
+use crate::error::{Context, ErrorKind, Fallible};
+use crate::fs::{create_staging_dir, create_staging_file, rename};
+use crate::hook::ToolHooks;
+use crate::layout::volta_home;
+use crate::style::{progress_bar, tool_version};
+use crate::tool::{self, download_tool_error, Node};
+use crate::version::{parse_version, VersionSpec};
+use archive::{self, Archive};
+use cfg_if::cfg_if;
+use fs_utils::ensure_containing_dir_exists;
+use log::debug;
+use node_semver::Version;
+use serde::Deserialize;
+
+cfg_if! {
+ if #[cfg(feature = "mock-network")] {
+ // TODO: We need to reconsider our mocking strategy in light of mockito deprecating the
+ // SERVER_URL constant: Since our acceptance tests run the binary in a separate process,
+ // we can't use `mockito::server_url()`, which relies on shared memory.
+ fn public_node_server_root() -> String {
+ #[allow(deprecated)]
+ mockito::SERVER_URL.to_string()
+ }
+ } else {
+ fn public_node_server_root() -> String {
+ "https://nodejs.org/dist".to_string()
+ }
+ }
+}
+
+fn npm_manifest_path(version: &Version) -> PathBuf {
+ let mut manifest = PathBuf::from(Node::archive_basename(version));
+
+ #[cfg(unix)]
+ manifest.push("lib");
+
+ manifest.push("node_modules");
+ manifest.push("npm");
+ manifest.push("package.json");
+
+ manifest
+}
+
+pub fn fetch(version: &Version, hooks: Option<&ToolHooks<Node>>) -> Fallible<NodeVersion> {
+ let home = volta_home()?;
+ let node_dir = home.node_inventory_dir();
+ let cache_file = node_dir.join(Node::archive_filename(version));
+
+ let (archive, staging) = match load_cached_distro(&cache_file) {
+ Some(archive) => {
+ debug!(
+ "Loading {} from cached archive at '{}'",
+ tool_version("node", version),
+ cache_file.display()
+ );
+ (archive, None)
+ }
+ None => {
+ let staging = create_staging_file()?;
+ let remote_url = determine_remote_url(version, hooks)?;
+ let archive = fetch_remote_distro(version, &remote_url, staging.path())?;
+ (archive, Some(staging))
+ }
+ };
+
+ let node_version = unpack_archive(archive, version)?;
+
+ if let Some(staging_file) = staging {
+ ensure_containing_dir_exists(&cache_file).with_context(|| {
+ ErrorKind::ContainingDirError {
+ path: cache_file.clone(),
+ }
+ })?;
+ staging_file
+ .persist(cache_file)
+ .with_context(|| ErrorKind::PersistInventoryError {
+ tool: "Node".into(),
+ })?;
+ }
+
+ Ok(node_version)
+}
+
+/// Unpack the node archive into the image directory so that it is ready for use
+fn unpack_archive(archive: Box<dyn Archive>, version: &Version) -> Fallible<NodeVersion> {
+ let temp = create_staging_dir()?;
+ debug!("Unpacking node into '{}'", temp.path().display());
+
+ let progress = progress_bar(
+ archive.origin(),
+ &tool_version("node", version),
+ archive.compressed_size(),
+ );
+ let version_string = version.to_string();
+
+ archive
+ .unpack(temp.path(), &mut |_, read| {
+ progress.inc(read as u64);
+ })
+ .with_context(|| ErrorKind::UnpackArchiveError {
+ tool: "Node".into(),
+ version: version_string.clone(),
+ })?;
+
+ // Save the npm version number in the npm version file for this distro
+ let npm_package_json = temp.path().join(npm_manifest_path(version));
+ let npm = Manifest::version(&npm_package_json)?;
+ save_default_npm_version(version, &npm)?;
+
+ let dest = volta_home()?.node_image_dir(&version_string);
+ ensure_containing_dir_exists(&dest)
+ .with_context(|| ErrorKind::ContainingDirError { path: dest.clone() })?;
+
+ rename(temp.path().join(Node::archive_basename(version)), &dest).with_context(|| {
+ ErrorKind::SetupToolImageError {
+ tool: "Node".into(),
+ version: version_string,
+ dir: dest.clone(),
+ }
+ })?;
+
+ progress.finish_and_clear();
+
+ // Note: We write these after the progress bar is finished to avoid display bugs with re-renders of the progress
+ debug!("Saving bundled npm version ({})", npm);
+ debug!("Installing node in '{}'", dest.display());
+
+ Ok(NodeVersion {
+ runtime: version.clone(),
+ npm,
+ })
+}
+
+/// Return the archive if it is valid. It may have been corrupted or interrupted in the middle of
+/// downloading.
+// ISSUE(#134) - verify checksum
+fn load_cached_distro(file: &Path) -> Option<Box<dyn Archive>> {
+ if file.is_file() {
+ let file = File::open(file).ok()?;
+ archive::load_native(file).ok()
+ } else {
+ None
+ }
+}
+
+/// Determine the remote URL to download from, using the hooks if available
+fn determine_remote_url(version: &Version, hooks: Option<&ToolHooks<Node>>) -> Fallible<String> {
+ let distro_file_name = Node::archive_filename(version);
+ match hooks {
+ Some(&ToolHooks {
+ distro: Some(ref hook),
+ ..
+ }) => {
+ debug!("Using node.distro hook to determine download URL");
+ hook.resolve(version, &distro_file_name)
+ }
+ _ => Ok(format!(
+ "{}/v{}/{}",
+ public_node_server_root(),
+ version,
+ distro_file_name
+ )),
+ }
+}
+
+/// Fetch the distro archive from the internet
+fn fetch_remote_distro(
+ version: &Version,
+ url: &str,
+ staging_path: &Path,
+) -> Fallible<Box<dyn Archive>> {
+ debug!("Downloading {} from {}", tool_version("node", version), url);
+ archive::fetch_native(url, staging_path).with_context(download_tool_error(
+ tool::Spec::Node(VersionSpec::Exact(version.clone())),
+ url,
+ ))
+}
+
+/// The portion of npm's `package.json` file that we care about
+#[derive(Deserialize)]
+struct Manifest {
+ version: String,
+}
+
+impl Manifest {
+ /// Parse the version out of a package.json file
+ fn version(path: &Path) -> Fallible<Version> {
+ let file = File::open(path).with_context(|| ErrorKind::ReadNpmManifestError)?;
+ let manifest: Manifest =
+ serde_json::de::from_reader(file).with_context(|| ErrorKind::ParseNpmManifestError)?;
+ parse_version(manifest.version)
+ }
+}
+
+/// Load the local npm version file to determine the default npm version for a given version of Node
+pub fn load_default_npm_version(node: &Version) -> Fallible<Version> {
+ let npm_version_file_path = volta_home()?.node_npm_version_file(&node.to_string());
+ let npm_version =
+ read_to_string(&npm_version_file_path).with_context(|| ErrorKind::ReadDefaultNpmError {
+ file: npm_version_file_path,
+ })?;
+ parse_version(npm_version)
+}
+
+/// Save the default npm version to the filesystem for a given version of Node
+fn save_default_npm_version(node: &Version, npm: &Version) -> Fallible<()> {
+ let npm_version_file_path = volta_home()?.node_npm_version_file(&node.to_string());
+ write(&npm_version_file_path, npm.to_string().as_bytes()).with_context(|| {
+ ErrorKind::WriteDefaultNpmError {
+ file: npm_version_file_path,
+ }
+ })
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +
use std::collections::HashSet;
+
+use super::NODE_DISTRO_IDENTIFIER;
+#[cfg(any(
+ all(target_os = "macos", target_arch = "aarch64"),
+ all(target_os = "windows", target_arch = "aarch64")
+))]
+use super::NODE_DISTRO_IDENTIFIER_FALLBACK;
+use crate::version::{option_version_serde, version_serde};
+use node_semver::Version;
+use serde::{Deserialize, Deserializer};
+
+/// The index of the public Node server.
+pub struct NodeIndex {
+ pub(super) entries: Vec<NodeEntry>,
+}
+
+#[derive(Debug)]
+pub struct NodeEntry {
+ pub version: Version,
+ pub lts: bool,
+}
+
+#[derive(Deserialize)]
+pub struct RawNodeIndex(Vec<RawNodeEntry>);
+
+#[derive(Deserialize)]
+pub struct RawNodeEntry {
+ #[serde(with = "version_serde")]
+ version: Version,
+ #[serde(default)] // handles Option
+ #[serde(with = "option_version_serde")]
+ npm: Option<Version>,
+ files: HashSet<String>,
+ #[serde(deserialize_with = "lts_version_serde")]
+ lts: bool,
+}
+
+impl From<RawNodeIndex> for NodeIndex {
+ fn from(raw: RawNodeIndex) -> NodeIndex {
+ let entries = raw
+ .0
+ .into_iter()
+ .filter_map(|entry| {
+ #[cfg(not(any(
+ all(target_os = "macos", target_arch = "aarch64"),
+ all(target_os = "windows", target_arch = "aarch64")
+ )))]
+ if entry.npm.is_some() && entry.files.contains(NODE_DISTRO_IDENTIFIER) {
+ Some(NodeEntry {
+ version: entry.version,
+ lts: entry.lts,
+ })
+ } else {
+ None
+ }
+
+ #[cfg(any(
+ all(target_os = "macos", target_arch = "aarch64"),
+ all(target_os = "windows", target_arch = "aarch64")
+ ))]
+ if entry.npm.is_some()
+ && (entry.files.contains(NODE_DISTRO_IDENTIFIER)
+ || entry.files.contains(NODE_DISTRO_IDENTIFIER_FALLBACK))
+ {
+ Some(NodeEntry {
+ version: entry.version,
+ lts: entry.lts,
+ })
+ } else {
+ None
+ }
+ })
+ .collect();
+
+ NodeIndex { entries }
+ }
+}
+
+#[allow(clippy::unnecessary_wraps)] // Needs to match the API expected by Serde
+fn lts_version_serde<'de, D>(deserializer: D) -> Result<bool, D::Error>
+where
+ D: Deserializer<'de>,
+{
+ match String::deserialize(deserializer) {
+ Ok(_) => Ok(true),
+ Err(_) => Ok(false),
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +
use std::fmt::{self, Display};
+
+use super::{
+ check_fetched, check_shim_reachable, debug_already_fetched, info_fetched, info_installed,
+ info_pinned, info_project_version, FetchStatus, Tool,
+};
+use crate::error::{ErrorKind, Fallible};
+use crate::inventory::node_available;
+use crate::session::Session;
+use crate::style::{note_prefix, tool_version};
+use crate::sync::VoltaLock;
+use cfg_if::cfg_if;
+use log::info;
+use node_semver::Version;
+
+mod fetch;
+mod metadata;
+mod resolve;
+
+pub use fetch::load_default_npm_version;
+pub use resolve::resolve;
+
+cfg_if! {
+ if #[cfg(all(target_os = "windows", target_arch = "x86"))] {
+ /// The OS component of a Node distro filename
+ pub const NODE_DISTRO_OS: &str = "win";
+ /// The architecture component of a Node distro filename
+ pub const NODE_DISTRO_ARCH: &str = "x86";
+ /// The extension for Node distro files
+ pub const NODE_DISTRO_EXTENSION: &str = "zip";
+ /// The file identifier in the Node index `files` array
+ pub const NODE_DISTRO_IDENTIFIER: &str = "win-x86-zip";
+ } else if #[cfg(all(target_os = "windows", target_arch = "x86_64"))] {
+ /// The OS component of a Node distro filename
+ pub const NODE_DISTRO_OS: &str = "win";
+ /// The architecture component of a Node distro filename
+ pub const NODE_DISTRO_ARCH: &str = "x64";
+ /// The extension for Node distro files
+ pub const NODE_DISTRO_EXTENSION: &str = "zip";
+ /// The file identifier in the Node index `files` array
+ pub const NODE_DISTRO_IDENTIFIER: &str = "win-x64-zip";
+ } else if #[cfg(all(target_os = "windows", target_arch = "aarch64"))] {
+ /// The OS component of a Node distro filename
+ pub const NODE_DISTRO_OS: &str = "win";
+ /// The architecture component of a Node distro filename
+ pub const NODE_DISTRO_ARCH: &str = "arm64";
+ /// The extension for Node distro files
+ pub const NODE_DISTRO_EXTENSION: &str = "zip";
+ /// The file identifier in the Node index `files` array
+ pub const NODE_DISTRO_IDENTIFIER: &str = "win-arm64-zip";
+
+ // NOTE: Node support for pre-built ARM64 binaries on Windows was added in major version 20
+ // For versions prior to that, we need to fall back on the x64 binaries via emulator
+
+ /// The fallback architecture component of a Node distro filename
+ pub const NODE_DISTRO_ARCH_FALLBACK: &str = "x64";
+ /// The fallback file identifier in the Node index `files` array
+ pub const NODE_DISTRO_IDENTIFIER_FALLBACK: &str = "win-x64-zip";
+ } else if #[cfg(all(target_os = "macos", target_arch = "x86_64"))] {
+ /// The OS component of a Node distro filename
+ pub const NODE_DISTRO_OS: &str = "darwin";
+ /// The architecture component of a Node distro filename
+ pub const NODE_DISTRO_ARCH: &str = "x64";
+ /// The extension for Node distro files
+ pub const NODE_DISTRO_EXTENSION: &str = "tar.gz";
+ /// The file identifier in the Node index `files` array
+ pub const NODE_DISTRO_IDENTIFIER: &str = "osx-x64-tar";
+ } else if #[cfg(all(target_os = "macos", target_arch = "aarch64"))] {
+ /// The OS component of a Node distro filename
+ pub const NODE_DISTRO_OS: &str = "darwin";
+ /// The architecture component of a Node distro filename
+ pub const NODE_DISTRO_ARCH: &str = "arm64";
+ /// The extension for Node distro files
+ pub const NODE_DISTRO_EXTENSION: &str = "tar.gz";
+ /// The file identifier in the Node index `files` array
+ pub const NODE_DISTRO_IDENTIFIER: &str = "osx-arm64-tar";
+
+ // NOTE: Node support for pre-built Apple Silicon binaries was added in major version 16
+ // For versions prior to that, we need to fall back on the x64 binaries via Rosetta 2
+
+ /// The fallback architecture component of a Node distro filename
+ pub const NODE_DISTRO_ARCH_FALLBACK: &str = "x64";
+ /// The fallback file identifier in the Node index `files` array
+ pub const NODE_DISTRO_IDENTIFIER_FALLBACK: &str = "osx-x64-tar";
+ } else if #[cfg(all(target_os = "linux", target_arch = "x86_64"))] {
+ /// The OS component of a Node distro filename
+ pub const NODE_DISTRO_OS: &str = "linux";
+ /// The architecture component of a Node distro filename
+ pub const NODE_DISTRO_ARCH: &str = "x64";
+ /// The extension for Node distro files
+ pub const NODE_DISTRO_EXTENSION: &str = "tar.gz";
+ /// The file identifier in the Node index `files` array
+ pub const NODE_DISTRO_IDENTIFIER: &str = "linux-x64";
+ } else if #[cfg(all(target_os = "linux", target_arch = "aarch64"))] {
+ /// The OS component of a Node distro filename
+ pub const NODE_DISTRO_OS: &str = "linux";
+ /// The architecture component of a Node distro filename
+ pub const NODE_DISTRO_ARCH: &str = "arm64";
+ /// The extension for Node distro files
+ pub const NODE_DISTRO_EXTENSION: &str = "tar.gz";
+ /// The file identifier in the Node index `files` array
+ pub const NODE_DISTRO_IDENTIFIER: &str = "linux-arm64";
+ } else if #[cfg(all(target_os = "linux", target_arch = "arm"))] {
+ /// The OS component of a Node distro filename
+ pub const NODE_DISTRO_OS: &str = "linux";
+ /// The architecture component of a Node distro filename
+ pub const NODE_DISTRO_ARCH: &str = "armv7l";
+ /// The extension for Node distro files
+ pub const NODE_DISTRO_EXTENSION: &str = "tar.gz";
+ /// The file identifier in the Node index `files` array
+ pub const NODE_DISTRO_IDENTIFIER: &str = "linux-armv7l";
+ } else {
+ compile_error!("Unsuppored operating system + architecture combination");
+ }
+}
+
+/// A full Node version including not just the version of Node itself
+/// but also the specific version of npm installed globally with that
+/// Node installation.
+#[derive(Clone, Debug)]
+pub struct NodeVersion {
+ /// The version of Node itself.
+ pub runtime: Version,
+ /// The npm version globally installed with the Node distro.
+ pub npm: Version,
+}
+
+impl Display for NodeVersion {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(
+ f,
+ "{} (with {})",
+ tool_version("node", &self.runtime),
+ tool_version("npm", &self.npm)
+ )
+ }
+}
+
+/// The Tool implementation for fetching and installing Node
+pub struct Node {
+ pub(super) version: Version,
+}
+
+impl Node {
+ pub fn new(version: Version) -> Self {
+ Node { version }
+ }
+
+ #[cfg(not(any(
+ all(target_os = "macos", target_arch = "aarch64"),
+ all(target_os = "windows", target_arch = "aarch64")
+ )))]
+ pub fn archive_basename(version: &Version) -> String {
+ format!("node-v{}-{}-{}", version, NODE_DISTRO_OS, NODE_DISTRO_ARCH)
+ }
+
+ #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
+ pub fn archive_basename(version: &Version) -> String {
+ // Note: Node began shipping pre-built binaries for Apple Silicon with Major version 16
+ // Prior to that, we need to fall back on the x64 binaries
+ format!(
+ "node-v{}-{}-{}",
+ version,
+ NODE_DISTRO_OS,
+ if version.major >= 16 {
+ NODE_DISTRO_ARCH
+ } else {
+ NODE_DISTRO_ARCH_FALLBACK
+ }
+ )
+ }
+
+ #[cfg(all(target_os = "windows", target_arch = "aarch64"))]
+ pub fn archive_basename(version: &Version) -> String {
+ // Note: Node began shipping pre-built binaries for Windows ARM with Major version 20
+ // Prior to that, we need to fall back on the x64 binaries
+ format!(
+ "node-v{}-{}-{}",
+ version,
+ NODE_DISTRO_OS,
+ if version.major >= 20 {
+ NODE_DISTRO_ARCH
+ } else {
+ NODE_DISTRO_ARCH_FALLBACK
+ }
+ )
+ }
+
+ pub fn archive_filename(version: &Version) -> String {
+ format!(
+ "{}.{}",
+ Node::archive_basename(version),
+ NODE_DISTRO_EXTENSION
+ )
+ }
+
+ pub(crate) fn ensure_fetched(&self, session: &mut Session) -> Fallible<NodeVersion> {
+ match check_fetched(|| node_available(&self.version))? {
+ FetchStatus::AlreadyFetched => {
+ debug_already_fetched(self);
+ let npm = fetch::load_default_npm_version(&self.version)?;
+
+ Ok(NodeVersion {
+ runtime: self.version.clone(),
+ npm,
+ })
+ }
+ FetchStatus::FetchNeeded(_lock) => fetch::fetch(&self.version, session.hooks()?.node()),
+ }
+ }
+}
+
+impl Tool for Node {
+ fn fetch(self: Box<Self>, session: &mut Session) -> Fallible<()> {
+ let node_version = self.ensure_fetched(session)?;
+
+ info_fetched(node_version);
+ Ok(())
+ }
+ fn install(self: Box<Self>, session: &mut Session) -> Fallible<()> {
+ // Acquire a lock on the Volta directory, if possible, to prevent concurrent changes
+ let _lock = VoltaLock::acquire();
+ let node_version = self.ensure_fetched(session)?;
+
+ let default_toolchain = session.toolchain_mut()?;
+ default_toolchain.set_active_node(&self.version)?;
+
+ // If the user has a default version of `npm`, we shouldn't show the "(with npm@X.Y.ZZZ)" text in the success message
+ // Instead we should check if the bundled version is higher than the default and inform the user
+ // Note: The previous line ensures that there will be a default platform
+ if let Some(default_npm) = &default_toolchain.platform().unwrap().npm {
+ info_installed(&self); // includes node version
+
+ if node_version.npm > *default_npm {
+ info!("{} this version of Node includes {}, which is higher than your default version ({}).
+ To use the version included with Node, run `volta install npm@bundled`",
+ note_prefix(),
+ tool_version("npm", node_version.npm),
+ default_npm.to_string()
+ );
+ }
+ } else {
+ info_installed(node_version); // includes node and npm version
+ }
+
+ check_shim_reachable("node");
+
+ if let Ok(Some(project)) = session.project_platform() {
+ info_project_version(tool_version("node", &project.node), &self);
+ }
+
+ Ok(())
+ }
+ fn pin(self: Box<Self>, session: &mut Session) -> Fallible<()> {
+ if session.project()?.is_some() {
+ let node_version = self.ensure_fetched(session)?;
+
+ // Note: We know this will succeed, since we checked above
+ let project = session.project_mut()?.unwrap();
+ project.pin_node(self.version.clone())?;
+
+ // If the user has a pinned version of `npm`, we shouldn't show the "(with npm@X.Y.ZZZ)" text in the success message
+ // Instead we should check if the bundled version is higher than the pinned and inform the user
+ // Note: The pin operation guarantees there will be a platform
+ if let Some(pinned_npm) = &project.platform().unwrap().npm {
+ info_pinned(self); // includes node version
+
+ if node_version.npm > *pinned_npm {
+ info!("{} this version of Node includes {}, which is higher than your pinned version ({}).
+ To use the version included with Node, run `volta pin npm@bundled`",
+ note_prefix(),
+ tool_version("npm", node_version.npm),
+ pinned_npm.to_string()
+ );
+ }
+ } else {
+ info_pinned(node_version); // includes node and npm version
+ }
+
+ Ok(())
+ } else {
+ Err(ErrorKind::NotInPackage.into())
+ }
+ }
+}
+
+impl Display for Node {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.write_str(&tool_version("node", &self.version))
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_node_archive_basename() {
+ assert_eq!(
+ Node::archive_basename(&Version::parse("20.2.3").unwrap()),
+ format!("node-v20.2.3-{}-{}", NODE_DISTRO_OS, NODE_DISTRO_ARCH)
+ );
+ }
+
+ #[test]
+ fn test_node_archive_filename() {
+ assert_eq!(
+ Node::archive_filename(&Version::parse("20.2.3").unwrap()),
+ format!(
+ "node-v20.2.3-{}-{}.{}",
+ NODE_DISTRO_OS, NODE_DISTRO_ARCH, NODE_DISTRO_EXTENSION
+ )
+ );
+ }
+
+ #[test]
+ #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
+ fn test_fallback_node_archive_basename() {
+ assert_eq!(
+ Node::archive_basename(&Version::parse("15.2.3").unwrap()),
+ format!(
+ "node-v15.2.3-{}-{}",
+ NODE_DISTRO_OS, NODE_DISTRO_ARCH_FALLBACK
+ )
+ );
+ }
+
+ #[test]
+ #[cfg(all(target_os = "windows", target_arch = "aarch64"))]
+ fn test_fallback_node_archive_basename() {
+ assert_eq!(
+ Node::archive_basename(&Version::parse("19.2.3").unwrap()),
+ format!(
+ "node-v19.2.3-{}-{}",
+ NODE_DISTRO_OS, NODE_DISTRO_ARCH_FALLBACK
+ )
+ );
+ }
+
+ #[test]
+ #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
+ fn test_fallback_node_archive_filename() {
+ assert_eq!(
+ Node::archive_filename(&Version::parse("15.2.3").unwrap()),
+ format!(
+ "node-v15.2.3-{}-{}.{}",
+ NODE_DISTRO_OS, NODE_DISTRO_ARCH_FALLBACK, NODE_DISTRO_EXTENSION
+ )
+ );
+ }
+
+ #[test]
+ #[cfg(all(target_os = "windows", target_arch = "aarch64"))]
+ fn test_fallback_node_archive_filename() {
+ assert_eq!(
+ Node::archive_filename(&Version::parse("19.2.3").unwrap()),
+ format!(
+ "node-v19.2.3-{}-{}.{}",
+ NODE_DISTRO_OS, NODE_DISTRO_ARCH_FALLBACK, NODE_DISTRO_EXTENSION
+ )
+ );
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +
//! Provides resolution of Node requirements into specific versions, using the NodeJS index
+
+use std::fs::File;
+use std::io::Write;
+use std::time::{Duration, SystemTime};
+
+use super::super::registry_fetch_error;
+use super::metadata::{NodeEntry, NodeIndex, RawNodeIndex};
+use crate::error::{Context, ErrorKind, Fallible};
+use crate::fs::{create_staging_file, read_file};
+use crate::hook::ToolHooks;
+use crate::layout::volta_home;
+use crate::session::Session;
+use crate::style::progress_spinner;
+use crate::tool::Node;
+use crate::version::{VersionSpec, VersionTag};
+use attohttpc::header::HeaderMap;
+use attohttpc::Response;
+use cfg_if::cfg_if;
+use fs_utils::ensure_containing_dir_exists;
+use headers::{CacheControl, Expires, HeaderMapExt};
+use log::debug;
+use node_semver::{Range, Version};
+
+// ISSUE (#86): Move public repository URLs to config file
+cfg_if! {
+ if #[cfg(feature = "mock-network")] {
+ // TODO: We need to reconsider our mocking strategy in light of mockito deprecating the
+ // SERVER_URL constant: Since our acceptance tests run the binary in a separate process,
+ // we can't use `mockito::server_url()`, which relies on shared memory.
+ #[allow(deprecated)]
+ const SERVER_URL: &str = mockito::SERVER_URL;
+ fn public_node_version_index() -> String {
+ format!("{}/node-dist/index.json", SERVER_URL)
+ }
+ } else {
+ /// Returns the URL of the index of available Node versions on the public Node server.
+ fn public_node_version_index() -> String {
+ "https://nodejs.org/dist/index.json".to_string()
+ }
+ }
+}
+
+pub fn resolve(matching: VersionSpec, session: &mut Session) -> Fallible<Version> {
+ let hooks = session.hooks()?.node();
+ match matching {
+ VersionSpec::Semver(requirement) => resolve_semver(requirement, hooks),
+ VersionSpec::Exact(version) => Ok(version),
+ VersionSpec::None | VersionSpec::Tag(VersionTag::Lts) => resolve_lts(hooks),
+ VersionSpec::Tag(VersionTag::Latest) => resolve_latest(hooks),
+ // Node doesn't have "tagged" versions (apart from 'latest' and 'lts'), so custom tags will always be an error
+ VersionSpec::Tag(VersionTag::Custom(tag)) => {
+ Err(ErrorKind::NodeVersionNotFound { matching: tag }.into())
+ }
+ }
+}
+
+fn resolve_latest(hooks: Option<&ToolHooks<Node>>) -> Fallible<Version> {
+ // NOTE: This assumes the registry always produces a list in sorted order
+ // from newest to oldest. This should be specified as a requirement
+ // when we document the plugin API.
+ let url = match hooks {
+ Some(&ToolHooks {
+ latest: Some(ref hook),
+ ..
+ }) => {
+ debug!("Using node.latest hook to determine node index URL");
+ hook.resolve("index.json")?
+ }
+ _ => public_node_version_index(),
+ };
+ let version_opt = match_node_version(&url, |_| true)?;
+
+ match version_opt {
+ Some(version) => {
+ debug!("Found latest node version ({}) from {}", version, url);
+ Ok(version)
+ }
+ None => Err(ErrorKind::NodeVersionNotFound {
+ matching: "latest".into(),
+ }
+ .into()),
+ }
+}
+
+fn resolve_lts(hooks: Option<&ToolHooks<Node>>) -> Fallible<Version> {
+ let url = match hooks {
+ Some(&ToolHooks {
+ index: Some(ref hook),
+ ..
+ }) => {
+ debug!("Using node.index hook to determine node index URL");
+ hook.resolve("index.json")?
+ }
+ _ => public_node_version_index(),
+ };
+ let version_opt = match_node_version(&url, |&NodeEntry { lts, .. }| lts)?;
+
+ match version_opt {
+ Some(version) => {
+ debug!("Found newest LTS node version ({}) from {}", version, url);
+ Ok(version)
+ }
+ None => Err(ErrorKind::NodeVersionNotFound {
+ matching: "lts".into(),
+ }
+ .into()),
+ }
+}
+
+fn resolve_semver(matching: Range, hooks: Option<&ToolHooks<Node>>) -> Fallible<Version> {
+ let url = match hooks {
+ Some(&ToolHooks {
+ index: Some(ref hook),
+ ..
+ }) => {
+ debug!("Using node.index hook to determine node index URL");
+ hook.resolve("index.json")?
+ }
+ _ => public_node_version_index(),
+ };
+ let version_opt = match_node_version(&url, |NodeEntry { version, .. }| {
+ matching.satisfies(version)
+ })?;
+
+ match version_opt {
+ Some(version) => {
+ debug!(
+ "Found node@{} matching requirement '{}' from {}",
+ version, matching, url
+ );
+ Ok(version)
+ }
+ None => Err(ErrorKind::NodeVersionNotFound {
+ matching: matching.to_string(),
+ }
+ .into()),
+ }
+}
+
+fn match_node_version(
+ url: &str,
+ predicate: impl Fn(&NodeEntry) -> bool,
+) -> Fallible<Option<Version>> {
+ let index: NodeIndex = resolve_node_versions(url)?.into();
+ let mut entries = index.entries.into_iter();
+ Ok(entries
+ .find(predicate)
+ .map(|NodeEntry { version, .. }| version))
+}
+
+/// Reads a public index from the Node cache, if it exists and hasn't expired.
+fn read_cached_opt(url: &str) -> Fallible<Option<RawNodeIndex>> {
+ let expiry_file = volta_home()?.node_index_expiry_file();
+ let expiry = read_file(expiry_file).with_context(|| ErrorKind::ReadNodeIndexExpiryError {
+ file: expiry_file.to_owned(),
+ })?;
+
+ if !expiry
+ .map(|date| httpdate::parse_http_date(&date))
+ .transpose()
+ .with_context(|| ErrorKind::ParseNodeIndexExpiryError)?
+ .is_some_and(|expiry_date| SystemTime::now() < expiry_date)
+ {
+ return Ok(None);
+ };
+
+ let index_file = volta_home()?.node_index_file();
+ let cached = read_file(index_file).with_context(|| ErrorKind::ReadNodeIndexCacheError {
+ file: index_file.to_owned(),
+ })?;
+
+ let Some(json) = cached
+ .as_ref()
+ .and_then(|content| content.strip_prefix(url))
+ else {
+ return Ok(None);
+ };
+
+ serde_json::de::from_str(json).with_context(|| ErrorKind::ParseNodeIndexCacheError)
+}
+
+/// Get the cache max-age of an HTTP response.
+fn max_age(headers: &HeaderMap) -> Duration {
+ const FOUR_HOURS: Duration = Duration::from_secs(4 * 60 * 60);
+ headers
+ .typed_get::<CacheControl>()
+ .and_then(|cache_control| cache_control.max_age())
+ .unwrap_or(FOUR_HOURS)
+}
+
+fn resolve_node_versions(url: &str) -> Fallible<RawNodeIndex> {
+ match read_cached_opt(url)? {
+ Some(serial) => {
+ debug!("Found valid cache of Node version index");
+ Ok(serial)
+ }
+ None => {
+ debug!("Node index cache was not found or was invalid");
+ let spinner = progress_spinner(format!("Fetching public registry: {}", url));
+
+ let (_, headers, response) = attohttpc::get(url)
+ .send()
+ .and_then(Response::error_for_status)
+ .with_context(registry_fetch_error("Node", url))?
+ .split();
+
+ let expires = headers
+ .typed_get::<Expires>()
+ .map(SystemTime::from)
+ .unwrap_or_else(|| SystemTime::now() + max_age(&headers));
+
+ let response_text = response
+ .text()
+ .with_context(registry_fetch_error("Node", url))?;
+
+ let index: RawNodeIndex =
+ serde_json::de::from_str(&response_text).with_context(|| {
+ ErrorKind::ParseNodeIndexError {
+ from_url: url.to_string(),
+ }
+ })?;
+
+ let cached = create_staging_file()?;
+
+ let mut cached_file: &File = cached.as_file();
+ writeln!(cached_file, "{}", url)
+ .and_then(|_| cached_file.write(response_text.as_bytes()))
+ .with_context(|| ErrorKind::WriteNodeIndexCacheError {
+ file: cached.path().to_path_buf(),
+ })?;
+
+ let index_cache_file = volta_home()?.node_index_file();
+ ensure_containing_dir_exists(&index_cache_file).with_context(|| {
+ ErrorKind::ContainingDirError {
+ path: index_cache_file.to_owned(),
+ }
+ })?;
+ cached.persist(index_cache_file).with_context(|| {
+ ErrorKind::WriteNodeIndexCacheError {
+ file: index_cache_file.to_owned(),
+ }
+ })?;
+
+ let expiry = create_staging_file()?;
+ let mut expiry_file: &File = expiry.as_file();
+
+ write!(expiry_file, "{}", httpdate::fmt_http_date(expires)).with_context(|| {
+ ErrorKind::WriteNodeIndexExpiryError {
+ file: expiry.path().to_path_buf(),
+ }
+ })?;
+
+ let index_expiry_file = volta_home()?.node_index_expiry_file();
+ ensure_containing_dir_exists(&index_expiry_file).with_context(|| {
+ ErrorKind::ContainingDirError {
+ path: index_expiry_file.to_owned(),
+ }
+ })?;
+ expiry.persist(index_expiry_file).with_context(|| {
+ ErrorKind::WriteNodeIndexExpiryError {
+ file: index_expiry_file.to_owned(),
+ }
+ })?;
+
+ spinner.finish_and_clear();
+ Ok(index)
+ }
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +
//! Provides fetcher for npm distributions
+
+use std::fs::{write, File};
+use std::path::Path;
+
+use super::super::download_tool_error;
+use super::super::registry::public_registry_package;
+use crate::error::{Context, ErrorKind, Fallible};
+use crate::fs::{create_staging_dir, create_staging_file, rename, set_executable};
+use crate::hook::ToolHooks;
+use crate::layout::volta_home;
+use crate::style::{progress_bar, tool_version};
+use crate::tool::{self, Npm};
+use crate::version::VersionSpec;
+use archive::{Archive, Tarball};
+use fs_utils::ensure_containing_dir_exists;
+use log::debug;
+use node_semver::Version;
+
+pub fn fetch(version: &Version, hooks: Option<&ToolHooks<Npm>>) -> Fallible<()> {
+ let npm_dir = volta_home()?.npm_inventory_dir();
+ let cache_file = npm_dir.join(Npm::archive_filename(&version.to_string()));
+
+ let (archive, staging) = match load_cached_distro(&cache_file) {
+ Some(archive) => {
+ debug!(
+ "Loading {} from cached archive at '{}'",
+ tool_version("npm", version),
+ cache_file.display()
+ );
+ (archive, None)
+ }
+ None => {
+ let staging = create_staging_file()?;
+ let remote_url = determine_remote_url(version, hooks)?;
+ let archive = fetch_remote_distro(version, &remote_url, staging.path())?;
+ (archive, Some(staging))
+ }
+ };
+
+ unpack_archive(archive, version)?;
+
+ if let Some(staging_file) = staging {
+ ensure_containing_dir_exists(&cache_file).with_context(|| {
+ ErrorKind::ContainingDirError {
+ path: cache_file.clone(),
+ }
+ })?;
+ staging_file
+ .persist(cache_file)
+ .with_context(|| ErrorKind::PersistInventoryError { tool: "npm".into() })?;
+ }
+
+ Ok(())
+}
+
+/// Unpack the npm archive into the image directory so that it is ready for use
+fn unpack_archive(archive: Box<dyn Archive>, version: &Version) -> Fallible<()> {
+ let temp = create_staging_dir()?;
+ debug!("Unpacking npm into '{}'", temp.path().display());
+
+ let progress = progress_bar(
+ archive.origin(),
+ &tool_version("npm", version),
+ archive.compressed_size(),
+ );
+ let version_string = version.to_string();
+
+ archive
+ .unpack(temp.path(), &mut |_, read| {
+ progress.inc(read as u64);
+ })
+ .with_context(|| ErrorKind::UnpackArchiveError {
+ tool: "npm".into(),
+ version: version_string.clone(),
+ })?;
+
+ let bin_path = temp.path().join("package").join("bin");
+ overwrite_launcher(&bin_path, "npm")?;
+ overwrite_launcher(&bin_path, "npx")?;
+
+ #[cfg(windows)]
+ {
+ overwrite_cmd_launcher(&bin_path, "npm")?;
+ overwrite_cmd_launcher(&bin_path, "npx")?;
+ }
+
+ let dest = volta_home()?.npm_image_dir(&version_string);
+ ensure_containing_dir_exists(&dest)
+ .with_context(|| ErrorKind::ContainingDirError { path: dest.clone() })?;
+
+ rename(temp.path().join("package"), &dest).with_context(|| ErrorKind::SetupToolImageError {
+ tool: "npm".into(),
+ version: version_string.clone(),
+ dir: dest.clone(),
+ })?;
+
+ progress.finish_and_clear();
+
+ // Note: We write this after the progress bar is finished to avoid display bugs with re-renders of the progress
+ debug!("Installing npm in '{}'", dest.display());
+
+ Ok(())
+}
+
+/// Return the archive if it is valid. It may have been corrupted or interrupted in the middle of
+/// downloading.
+/// ISSUE(#134) - verify checksum
+fn load_cached_distro(file: &Path) -> Option<Box<dyn Archive>> {
+ if file.is_file() {
+ let file = File::open(file).ok()?;
+ Tarball::load(file).ok()
+ } else {
+ None
+ }
+}
+
+/// Determine the remote URL to download from, using the hooks if avaialble
+fn determine_remote_url(version: &Version, hooks: Option<&ToolHooks<Npm>>) -> Fallible<String> {
+ let version_str = version.to_string();
+ match hooks {
+ Some(&ToolHooks {
+ distro: Some(ref hook),
+ ..
+ }) => {
+ debug!("Using npm.distro hook to determine download URL");
+ let distro_file_name = Npm::archive_filename(&version_str);
+ hook.resolve(version, &distro_file_name)
+ }
+ _ => Ok(public_registry_package("npm", &version_str)),
+ }
+}
+
+/// Fetch the distro archive from the internet
+fn fetch_remote_distro(
+ version: &Version,
+ url: &str,
+ staging_path: &Path,
+) -> Fallible<Box<dyn Archive>> {
+ debug!("Downloading {} from {}", tool_version("npm", version), url);
+ Tarball::fetch(url, staging_path).with_context(download_tool_error(
+ tool::Spec::Npm(VersionSpec::Exact(version.clone())),
+ url,
+ ))
+}
+
+/// Overwrite the launcher script
+fn overwrite_launcher(base_path: &Path, tool: &str) -> Fallible<()> {
+ let path = base_path.join(tool);
+ write(
+ &path,
+ // Note: Adapted from the existing npm/npx launcher, without unnecessary detection of Node location
+ format!(
+ r#"#!/bin/sh
+(set -o igncr) 2>/dev/null && set -o igncr; # cygwin encoding fix
+
+basedir=`dirname "$0"`
+
+case `uname` in
+ *CYGWIN*) basedir=`cygpath -w "$basedir"`;;
+esac
+
+node "$basedir/{}-cli.js" "$@"
+"#,
+ tool
+ ),
+ )
+ .and_then(|_| set_executable(&path))
+ .with_context(|| ErrorKind::WriteLauncherError { tool: tool.into() })
+}
+
+/// Overwrite the CMD launcher
+#[cfg(windows)]
+fn overwrite_cmd_launcher(base_path: &Path, tool: &str) -> Fallible<()> {
+ write(
+ base_path.join(format!("{}.cmd", tool)),
+ // Note: Adapted from the existing npm/npx cmd launcher, without unnecessary detection of Node location
+ format!(
+ r#"@ECHO OFF
+
+node "%~dp0\{}-cli.js" %*
+"#,
+ tool
+ ),
+ )
+ .with_context(|| ErrorKind::WriteLauncherError { tool: tool.into() })
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +
use std::fmt::{self, Display};
+
+use super::node::load_default_npm_version;
+use super::{
+ check_fetched, check_shim_reachable, debug_already_fetched, info_fetched, info_installed,
+ info_pinned, info_project_version, FetchStatus, Tool,
+};
+use crate::error::{Context, ErrorKind, Fallible};
+use crate::inventory::npm_available;
+use crate::session::Session;
+use crate::style::{success_prefix, tool_version};
+use crate::sync::VoltaLock;
+use log::info;
+use node_semver::Version;
+
+mod fetch;
+mod resolve;
+
+pub use resolve::resolve;
+
+/// The Tool implementation for fetching and installing npm
+pub struct Npm {
+ pub(super) version: Version,
+}
+
+impl Npm {
+ pub fn new(version: Version) -> Self {
+ Npm { version }
+ }
+
+ pub fn archive_basename(version: &str) -> String {
+ format!("npm-{}", version)
+ }
+
+ pub fn archive_filename(version: &str) -> String {
+ format!("{}.tgz", Npm::archive_basename(version))
+ }
+
+ pub(crate) fn ensure_fetched(&self, session: &mut Session) -> Fallible<()> {
+ match check_fetched(|| npm_available(&self.version))? {
+ FetchStatus::AlreadyFetched => {
+ debug_already_fetched(self);
+ Ok(())
+ }
+ FetchStatus::FetchNeeded(_lock) => fetch::fetch(&self.version, session.hooks()?.npm()),
+ }
+ }
+}
+
+impl Tool for Npm {
+ fn fetch(self: Box<Self>, session: &mut Session) -> Fallible<()> {
+ self.ensure_fetched(session)?;
+
+ info_fetched(self);
+ Ok(())
+ }
+ fn install(self: Box<Self>, session: &mut Session) -> Fallible<()> {
+ // Acquire a lock on the Volta directory, if possible, to prevent concurrent changes
+ let _lock = VoltaLock::acquire();
+ self.ensure_fetched(session)?;
+
+ session
+ .toolchain_mut()?
+ .set_active_npm(Some(self.version.clone()))?;
+
+ info_installed(&self);
+ check_shim_reachable("npm");
+
+ if let Ok(Some(project)) = session.project_platform() {
+ if let Some(npm) = &project.npm {
+ info_project_version(tool_version("npm", npm), &self);
+ }
+ }
+ Ok(())
+ }
+ fn pin(self: Box<Self>, session: &mut Session) -> Fallible<()> {
+ if session.project()?.is_some() {
+ self.ensure_fetched(session)?;
+
+ // Note: We know this will succeed, since we checked above
+ let project = session.project_mut()?.unwrap();
+ project.pin_npm(Some(self.version.clone()))?;
+
+ info_pinned(self);
+ Ok(())
+ } else {
+ Err(ErrorKind::NotInPackage.into())
+ }
+ }
+}
+
+impl Display for Npm {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.write_str(&tool_version("npm", &self.version))
+ }
+}
+
+/// The Tool implementation for setting npm to the version bundled with Node
+pub struct BundledNpm;
+
+impl Tool for BundledNpm {
+ fn fetch(self: Box<Self>, _session: &mut Session) -> Fallible<()> {
+ info!("Bundled npm is included with Node, use `volta fetch node` to fetch Node");
+ Ok(())
+ }
+
+ fn install(self: Box<Self>, session: &mut Session) -> Fallible<()> {
+ let toolchain = session.toolchain_mut()?;
+
+ toolchain.set_active_npm(None)?;
+
+ let bundled_version = match toolchain.platform() {
+ Some(platform) => {
+ let version = load_default_npm_version(&platform.node).with_context(|| {
+ ErrorKind::NoBundledNpm {
+ command: "install".into(),
+ }
+ })?;
+ version.to_string()
+ }
+ None => {
+ return Err(ErrorKind::NoBundledNpm {
+ command: "install".into(),
+ }
+ .into());
+ }
+ };
+
+ info!(
+ "{} set bundled npm (currently {}) as default",
+ success_prefix(),
+ bundled_version
+ );
+
+ Ok(())
+ }
+
+ fn pin(self: Box<Self>, session: &mut Session) -> Fallible<()> {
+ match session.project_mut()? {
+ Some(project) => {
+ project.pin_npm(None)?;
+
+ let bundled_version = match project.platform() {
+ Some(platform) => {
+ let version =
+ load_default_npm_version(&platform.node).with_context(|| {
+ ErrorKind::NoBundledNpm {
+ command: "pin".into(),
+ }
+ })?;
+ version.to_string()
+ }
+ None => {
+ return Err(ErrorKind::NoBundledNpm {
+ command: "pin".into(),
+ }
+ .into());
+ }
+ };
+
+ info!(
+ "{} set package.json to use bundled npm (currently {})",
+ success_prefix(),
+ bundled_version
+ );
+
+ Ok(())
+ }
+ None => Err(ErrorKind::NotInPackage.into()),
+ }
+ }
+}
+
+impl Display for BundledNpm {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.write_str(&tool_version("npm", "bundled"))
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_npm_archive_basename() {
+ assert_eq!(Npm::archive_basename("1.2.3"), "npm-1.2.3");
+ }
+
+ #[test]
+ fn test_npm_archive_filename() {
+ assert_eq!(Npm::archive_filename("1.2.3"), "npm-1.2.3.tgz");
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +
//! Provides resolution of npm Version requirements into specific versions
+
+use super::super::registry::{
+ fetch_npm_registry, public_registry_index, PackageDetails, PackageIndex,
+};
+use crate::error::{ErrorKind, Fallible};
+use crate::hook::ToolHooks;
+use crate::session::Session;
+use crate::tool::Npm;
+use crate::version::{VersionSpec, VersionTag};
+use log::debug;
+use node_semver::{Range, Version};
+
+pub fn resolve(matching: VersionSpec, session: &mut Session) -> Fallible<Option<Version>> {
+ let hooks = session.hooks()?.npm();
+ match matching {
+ VersionSpec::Semver(requirement) => resolve_semver(requirement, hooks).map(Some),
+ VersionSpec::Exact(version) => Ok(Some(version)),
+ VersionSpec::None | VersionSpec::Tag(VersionTag::Latest) => {
+ resolve_tag("latest", hooks).map(Some)
+ }
+ VersionSpec::Tag(VersionTag::Custom(tag)) if tag == "bundled" => Ok(None),
+ VersionSpec::Tag(tag) => resolve_tag(&tag.to_string(), hooks).map(Some),
+ }
+}
+
+fn fetch_npm_index(hooks: Option<&ToolHooks<Npm>>) -> Fallible<(String, PackageIndex)> {
+ let url = match hooks {
+ Some(&ToolHooks {
+ index: Some(ref hook),
+ ..
+ }) => {
+ debug!("Using npm.index hook to determine npm index URL");
+ hook.resolve("npm")?
+ }
+ _ => public_registry_index("npm"),
+ };
+
+ fetch_npm_registry(url, "npm")
+}
+
+fn resolve_tag(tag: &str, hooks: Option<&ToolHooks<Npm>>) -> Fallible<Version> {
+ let (url, mut index) = fetch_npm_index(hooks)?;
+
+ match index.tags.remove(tag) {
+ Some(version) => {
+ debug!("Found npm@{} matching tag '{}' from {}", version, tag, url);
+ Ok(version)
+ }
+ None => Err(ErrorKind::NpmVersionNotFound {
+ matching: tag.into(),
+ }
+ .into()),
+ }
+}
+
+fn resolve_semver(matching: Range, hooks: Option<&ToolHooks<Npm>>) -> Fallible<Version> {
+ let (url, index) = fetch_npm_index(hooks)?;
+
+ let details_opt = index
+ .entries
+ .into_iter()
+ .find(|PackageDetails { version, .. }| matching.satisfies(version));
+
+ match details_opt {
+ Some(details) => {
+ debug!(
+ "Found npm@{} matching requirement '{}' from {}",
+ details.version, matching, url
+ );
+ Ok(details.version)
+ }
+ None => Err(ErrorKind::NpmVersionNotFound {
+ matching: matching.to_string(),
+ }
+ .into()),
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +
use std::path::PathBuf;
+
+use super::manager::PackageManager;
+use super::metadata::{BinConfig, PackageConfig, PackageManifest};
+use crate::error::{ErrorKind, Fallible};
+use crate::layout::volta_home;
+use crate::platform::{Image, PlatformSpec};
+use crate::shim;
+use crate::tool::check_shim_reachable;
+
+/// Read the manifest for the package being installed
+pub(super) fn parse_manifest(
+ package_name: &str,
+ staging_dir: PathBuf,
+ manager: PackageManager,
+) -> Fallible<PackageManifest> {
+ let mut package_dir = manager.source_dir(staging_dir);
+ package_dir.push(package_name);
+
+ PackageManifest::for_dir(package_name, &package_dir)
+}
+
+/// Generate configuration files and shims for the package and each of its bins
+pub(super) fn write_config_and_shims(
+ name: &str,
+ manifest: &PackageManifest,
+ image: &Image,
+ manager: PackageManager,
+) -> Fallible<()> {
+ validate_bins(name, manifest)?;
+
+ let platform = PlatformSpec {
+ node: image.node.value.clone(),
+ npm: image.npm.clone().map(|s| s.value),
+ pnpm: image.pnpm.clone().map(|s| s.value),
+ yarn: image.yarn.clone().map(|s| s.value),
+ };
+
+ // Generate the shims and bin configs for each bin provided by the package
+ for bin_name in &manifest.bin {
+ shim::create(bin_name)?;
+ check_shim_reachable(bin_name);
+
+ BinConfig {
+ name: bin_name.clone(),
+ package: name.into(),
+ version: manifest.version.clone(),
+ platform: platform.clone(),
+ manager,
+ }
+ .write()?;
+ }
+
+ // Write the config for the package
+ PackageConfig {
+ name: name.into(),
+ version: manifest.version.clone(),
+ platform,
+ bins: manifest.bin.clone(),
+ manager,
+ }
+ .write()?;
+
+ Ok(())
+}
+
+/// Validate that we aren't attempting to install a bin that is already installed by
+/// another package.
+fn validate_bins(package_name: &str, manifest: &PackageManifest) -> Fallible<()> {
+ let home = volta_home()?;
+ for bin_name in &manifest.bin {
+ // Check for name conflicts with already-installed bins
+ // Some packages may install bins with the same name
+ if let Ok(config) = BinConfig::from_file(home.default_tool_bin_config(bin_name)) {
+ // The file exists, so there is a bin with this name
+ // That is okay iff it came from the package that is currently being installed
+ if package_name != config.package {
+ return Err(ErrorKind::BinaryAlreadyInstalled {
+ bin_name: bin_name.into(),
+ existing_package: config.package,
+ new_package: package_name.into(),
+ }
+ .into());
+ }
+ }
+ }
+
+ Ok(())
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +
use std::path::PathBuf;
+
+use super::manager::PackageManager;
+use crate::command::create_command;
+use crate::error::{Context, ErrorKind, Fallible};
+use crate::platform::Image;
+use crate::style::progress_spinner;
+use log::debug;
+
+/// Use `npm install --global` to install the package
+///
+/// Sets the environment variable `npm_config_prefix` to redirect the install to the Volta
+/// data directory, taking advantage of the standard global install behavior with a custom
+/// location
+pub(super) fn run_global_install(
+ package: String,
+ staging_dir: PathBuf,
+ platform_image: &Image,
+) -> Fallible<()> {
+ let mut command = create_command("npm");
+ command.args([
+ "install",
+ "--global",
+ "--loglevel=warn",
+ "--no-update-notifier",
+ "--no-audit",
+ ]);
+ command.arg(&package);
+ command.env("PATH", platform_image.path()?);
+ PackageManager::Npm.setup_global_command(&mut command, staging_dir);
+
+ debug!("Installing {} with command: {:?}", package, command);
+ let spinner = progress_spinner(format!("Installing {}", package));
+ let output_result = command
+ .output()
+ .with_context(|| ErrorKind::PackageInstallFailed {
+ package: package.clone(),
+ });
+ spinner.finish_and_clear();
+ let output = output_result?;
+
+ let stderr = String::from_utf8_lossy(&output.stderr);
+ debug!("[install stderr]\n{}", stderr);
+ debug!(
+ "[install stdout]\n{}",
+ String::from_utf8_lossy(&output.stdout)
+ );
+
+ if output.status.success() {
+ Ok(())
+ } else if stderr.contains("code E404") {
+ // npm outputs "code E404" as part of the error output when a package couldn't be found
+ // Detect that and show a nicer error message (since we likely know the problem in that case)
+ Err(ErrorKind::PackageNotFound { package }.into())
+ } else {
+ Err(ErrorKind::PackageInstallFailed { package }.into())
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +
use std::ffi::OsStr;
+use std::fs::File;
+use std::path::{Path, PathBuf};
+use std::process::Command;
+
+use super::metadata::GlobalYarnManifest;
+use crate::fs::read_dir_eager;
+
+/// The package manager used to install a given package
+#[derive(
+ Copy, Clone, serde::Serialize, serde::Deserialize, PartialOrd, Ord, PartialEq, Eq, Debug,
+)]
+pub enum PackageManager {
+ Npm,
+ Pnpm,
+ Yarn,
+}
+
+impl PackageManager {
+ /// Given the `package_root`, returns the directory where the source is stored for this
+ /// package manager. This will include the top-level `node_modules`, where appropriate.
+ pub fn source_dir(self, package_root: PathBuf) -> PathBuf {
+ let mut path = self.source_root(package_root);
+ path.push("node_modules");
+
+ path
+ }
+
+ /// Given the `package_root`, returns the root of the source directory. This directory will
+ /// contain the top-level `node-modules`
+ #[cfg(unix)]
+ pub fn source_root(self, package_root: PathBuf) -> PathBuf {
+ let mut path = package_root;
+ match self {
+ // On Unix, the source is always within a `lib` subdirectory, with both npm and Yarn
+ PackageManager::Npm | PackageManager::Yarn => path.push("lib"),
+ // pnpm puts the source node_modules directory in the global-dir
+ // plus a versioned subdirectory.
+ // FIXME: Here the subdirectory is hard-coded, I don't know if it's
+ // possible to retrieve it from pnpm dynamically.
+ PackageManager::Pnpm => path.push("5"),
+ }
+
+ path
+ }
+
+ /// Given the `package_root`, returns the root of the source directory. This directory will
+ /// contain the top-level `node-modules`
+ #[cfg(windows)]
+ pub fn source_root(self, package_root: PathBuf) -> PathBuf {
+ match self {
+ // On Windows, npm puts the source node_modules directory in the root of the `prefix`
+ PackageManager::Npm => package_root,
+ // On Windows, we still tell yarn to use the `lib` subdirectory
+ PackageManager::Yarn => {
+ let mut path = package_root;
+ path.push("lib");
+ path
+ }
+ // pnpm puts the source node_modules directory in the global-dir
+ // plus a versioned subdirectory.
+ // FIXME: Here the subdirectory is hard-coded, I don't know if it's
+ // possible to retrieve it from pnpm dynamically.
+ PackageManager::Pnpm => {
+ let mut path = package_root;
+ path.push("5");
+ path
+ }
+ }
+ }
+
+ /// Given the `package_root`, returns the directory where binaries are stored for this package
+ /// manager.
+ #[cfg(unix)]
+ pub fn binary_dir(self, package_root: PathBuf) -> PathBuf {
+ // On Unix, the binaries are always within a `bin` subdirectory for both npm and Yarn
+ let mut path = package_root;
+ path.push("bin");
+
+ path
+ }
+
+ /// Given the `package_root`, returns the directory where binaries are stored for this package
+ /// manager.
+ #[cfg(windows)]
+ pub fn binary_dir(self, package_root: PathBuf) -> PathBuf {
+ match self {
+ // On Windows, npm leaves the binaries at the root of the `prefix` directory
+ PackageManager::Npm => package_root,
+ // On Windows, Yarn still includes the `bin` subdirectory. pnpm by
+ // default generates binaries into the `PNPM_HOME` path
+ PackageManager::Yarn | PackageManager::Pnpm => {
+ let mut path = package_root;
+ path.push("bin");
+ path
+ }
+ }
+ }
+
+ /// Modify a given `Command` to be set up for global installs, given the package root
+ pub fn setup_global_command(self, command: &mut Command, package_root: PathBuf) {
+ command.env("npm_config_prefix", &package_root);
+
+ if let PackageManager::Yarn = self {
+ command.env("npm_config_global_folder", self.source_root(package_root));
+ } else if let PackageManager::Pnpm = self {
+ // FIXME: Find out if there is a perfect way to intercept pnpm global
+ // installs by using environment variables or whatever.
+ // Using `--global-dir` and `--global-bin-dir` flags here is not enough,
+ // because pnpm generates _absolute path_ based symlinks, and this makes
+ // impossible to simply move installed packages from the staging directory
+ // to the final `image/packages/` destination.
+
+ // Specify the staging directory to store global package,
+ // see: https://pnpm.io/npmrc#global-dir
+ command.arg("--global-dir").arg(&package_root);
+ // Specify the staging directory for the bin files of globally installed packages.
+ // See: https://pnpm.io/npmrc#global-bin-dir (>= 6.15.0)
+ // and https://github.com/volta-cli/rfcs/pull/46#discussion_r933296625
+ let global_bin_dir = self.binary_dir(package_root);
+ command.arg("--global-bin-dir").arg(&global_bin_dir);
+ // pnpm requires the `global-bin-dir` to be in PATH, otherwise it
+ // will not trigger global installs. One can also use the `PNPM_HOME`
+ // environment variable, which is only available in pnpm v7+, to
+ // pass the check.
+ // See: https://github.com/volta-cli/rfcs/pull/46#discussion_r861943740
+ let mut new_path = global_bin_dir;
+ for (name, value) in command.get_envs() {
+ if name == "PATH" {
+ if let Some(old_path) = value {
+ #[cfg(unix)]
+ let path_delimiter = OsStr::new(":");
+ #[cfg(windows)]
+ let path_delimiter = OsStr::new(";");
+ new_path =
+ PathBuf::from([new_path.as_os_str(), old_path].join(path_delimiter));
+ break;
+ }
+ }
+ }
+ command.env("PATH", new_path);
+ }
+ }
+
+ /// Determine the name of the package that was installed into the `package_root`
+ ///
+ /// If there are none or more than one package installed, then we return None
+ pub(super) fn get_installed_package(self, package_root: PathBuf) -> Option<String> {
+ match self {
+ PackageManager::Npm => get_npm_package_name(self.source_dir(package_root)),
+ PackageManager::Pnpm | PackageManager::Yarn => {
+ get_pnpm_or_yarn_package_name(self.source_root(package_root))
+ }
+ }
+ }
+}
+
+/// Determine the package name for an npm global install
+///
+/// npm doesn't hoist the packages inside of `node_modules`, so the only directory will be the
+/// globally installed package.
+fn get_npm_package_name(mut source_dir: PathBuf) -> Option<String> {
+ let possible_name = get_single_directory_name(&source_dir)?;
+
+ // If the directory starts with `@`, that represents a scoped package, so we need to step
+ // a level deeper to determine the full package name (`@scope/package`)
+ if possible_name.starts_with('@') {
+ source_dir.push(&possible_name);
+ let package = get_single_directory_name(&source_dir)?;
+ Some(format!("{}/{}", possible_name, package))
+ } else {
+ Some(possible_name)
+ }
+}
+
+/// Return the name of the single subdirectory (if any) to the given `parent_dir`
+///
+/// If there are more than one subdirectory, then this will return `None`
+fn get_single_directory_name(parent_dir: &Path) -> Option<String> {
+ let mut entries = read_dir_eager(parent_dir)
+ .ok()?
+ .filter_map(|(entry, metadata)| {
+ // If the entry is a symlink, _both_ is_dir() _and_ is_file() will be false. We want to
+ // include symlinks as well as directories in our search, since `npm link` uses
+ // symlinks internally, so we only exclude files from this search
+ if !metadata.is_file() {
+ Some(entry)
+ } else {
+ None
+ }
+ });
+
+ match (entries.next(), entries.next()) {
+ (Some(entry), None) => entry.file_name().into_string().ok(),
+ _ => None,
+ }
+}
+
+/// Determine the package name for a pnpm or Yarn global install
+///
+/// pnpm/Yarn creates a `package.json` file with the globally installed package as a dependency
+fn get_pnpm_or_yarn_package_name(source_root: PathBuf) -> Option<String> {
+ let package_file = source_root.join("package.json");
+ let file = File::open(package_file).ok()?;
+ let manifest: GlobalYarnManifest = serde_json::de::from_reader(file).ok()?;
+ let mut dependencies = manifest.dependencies.into_iter();
+
+ match (dependencies.next(), dependencies.next()) {
+ // If there is exactly one dependency, we return it
+ (Some((key, _)), None) => Some(key),
+ // Otherwise, we can't determine the package name
+ _ => None,
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +
use std::collections::HashMap;
+use std::fs::File;
+use std::io;
+use std::path::Path;
+
+use super::manager::PackageManager;
+use crate::error::{Context, ErrorKind, Fallible, VoltaError};
+use crate::layout::volta_home;
+use crate::platform::PlatformSpec;
+use crate::version::{option_version_serde, version_serde};
+use fs_utils::ensure_containing_dir_exists;
+use node_semver::Version;
+
+/// Configuration information about an installed package
+///
+/// Will be stored in `<VOLTA_HOME>/tools/user/packages/<package>.json`
+#[derive(serde::Serialize, serde::Deserialize, PartialOrd, Ord, PartialEq, Eq)]
+pub struct PackageConfig {
+ /// The package name
+ pub name: String,
+ /// The package version
+ #[serde(with = "version_serde")]
+ pub version: Version,
+ /// The platform used to install this package
+ #[serde(with = "RawPlatformSpec")]
+ pub platform: PlatformSpec,
+ /// The binaries installed by this package
+ pub bins: Vec<String>,
+ /// The package manager that was used to install this package
+ pub manager: PackageManager,
+}
+
+impl PackageConfig {
+ /// Parse a `PackageConfig` instance from a config file
+ pub fn from_file<P>(file: P) -> Fallible<Self>
+ where
+ P: AsRef<Path>,
+ {
+ let config = File::open(&file).with_context(|| ErrorKind::ReadPackageConfigError {
+ file: file.as_ref().to_owned(),
+ })?;
+ serde_json::from_reader(config).with_context(|| ErrorKind::ParsePackageConfigError)
+ }
+
+ pub fn from_file_if_exists<P>(file: P) -> Fallible<Option<Self>>
+ where
+ P: AsRef<Path>,
+ {
+ match File::open(&file) {
+ Err(error) => {
+ if error.kind() == io::ErrorKind::NotFound {
+ Ok(None)
+ } else {
+ Err(VoltaError::from_source(
+ error,
+ ErrorKind::ReadPackageConfigError {
+ file: file.as_ref().to_owned(),
+ },
+ ))
+ }
+ }
+ Ok(config) => serde_json::from_reader(config)
+ .with_context(|| ErrorKind::ParsePackageConfigError)
+ .map(Some),
+ }
+ }
+
+ /// Write this `PackageConfig` into the appropriate config file
+ pub fn write(self) -> Fallible<()> {
+ let config_file_path = volta_home()?.default_package_config_file(&self.name);
+
+ ensure_containing_dir_exists(&config_file_path).with_context(|| {
+ ErrorKind::ContainingDirError {
+ path: config_file_path.clone(),
+ }
+ })?;
+
+ let file = File::create(&config_file_path).with_context(|| {
+ ErrorKind::WritePackageConfigError {
+ file: config_file_path,
+ }
+ })?;
+ serde_json::to_writer_pretty(file, &self)
+ .with_context(|| ErrorKind::StringifyPackageConfigError)
+ }
+}
+
+/// Configuration information about a single installed binary from a package
+///
+/// Will be stored in <VOLTA_HOME>/tools/user/bins/<bin-name>.json
+#[derive(serde::Serialize, serde::Deserialize)]
+pub struct BinConfig {
+ /// The binary name
+ pub name: String,
+ /// The package that installed the binary
+ pub package: String,
+ /// The package version
+ #[serde(with = "version_serde")]
+ pub version: Version,
+ /// The platform used to install this binary
+ #[serde(with = "RawPlatformSpec")]
+ pub platform: PlatformSpec,
+ /// The package manager used to install this binary
+ pub manager: PackageManager,
+}
+
+impl BinConfig {
+ /// Parse a `BinConfig` instance from the given config file
+ pub fn from_file<P>(file: P) -> Fallible<Self>
+ where
+ P: AsRef<Path>,
+ {
+ let config = File::open(&file).with_context(|| ErrorKind::ReadBinConfigError {
+ file: file.as_ref().to_owned(),
+ })?;
+ serde_json::from_reader(config).with_context(|| ErrorKind::ParseBinConfigError)
+ }
+
+ pub fn from_file_if_exists<P>(file: P) -> Fallible<Option<Self>>
+ where
+ P: AsRef<Path>,
+ {
+ match File::open(&file) {
+ Err(error) => {
+ if error.kind() == io::ErrorKind::NotFound {
+ Ok(None)
+ } else {
+ Err(VoltaError::from_source(
+ error,
+ ErrorKind::ReadBinConfigError {
+ file: file.as_ref().to_owned(),
+ },
+ ))
+ }
+ }
+ Ok(config) => serde_json::from_reader(config)
+ .with_context(|| ErrorKind::ParseBinConfigError)
+ .map(Some),
+ }
+ }
+
+ /// Write this `BinConfig` to the appropriate config file
+ pub fn write(self) -> Fallible<()> {
+ let config_file_path = volta_home()?.default_tool_bin_config(&self.name);
+
+ ensure_containing_dir_exists(&config_file_path).with_context(|| {
+ ErrorKind::ContainingDirError {
+ path: config_file_path.clone(),
+ }
+ })?;
+
+ let file =
+ File::create(&config_file_path).with_context(|| ErrorKind::WriteBinConfigError {
+ file: config_file_path,
+ })?;
+ serde_json::to_writer_pretty(file, &self)
+ .with_context(|| ErrorKind::StringifyBinConfigError)
+ }
+}
+
+#[derive(serde::Serialize, serde::Deserialize)]
+#[serde(remote = "PlatformSpec")]
+struct RawPlatformSpec {
+ #[serde(with = "version_serde")]
+ node: Version,
+ #[serde(with = "option_version_serde")]
+ npm: Option<Version>,
+ // The magic:
+ // `serde(default)` to assign the pnpm field with a default value, this
+ // ensures a seamless migration is performed from the previous package
+ // platformspec which did not have a pnpm field despite the same layout.v3
+ #[serde(default)]
+ #[serde(with = "option_version_serde")]
+ pnpm: Option<Version>,
+ #[serde(with = "option_version_serde")]
+ yarn: Option<Version>,
+}
+
+/// The relevant information we need out of a package's `package.json` file
+///
+/// This includes the exact Version (since we can install using a range)
+/// and the list of bins provided by the package.
+#[derive(serde::Deserialize)]
+pub struct PackageManifest {
+ /// The name of the package
+ pub name: String,
+ /// The version of the package
+ #[serde(deserialize_with = "version_serde::deserialize")]
+ pub version: Version,
+ /// The `bin` section, containing a map of binary names to locations
+ #[serde(default, deserialize_with = "serde_bins::deserialize")]
+ pub bin: Vec<String>,
+}
+
+impl PackageManifest {
+ /// Parse the `package.json` for a given package directory
+ pub fn for_dir(package: &str, package_root: &Path) -> Fallible<Self> {
+ let package_file = package_root.join("package.json");
+ let file =
+ File::open(package_file).with_context(|| ErrorKind::PackageManifestReadError {
+ package: package.into(),
+ })?;
+
+ let mut manifest: Self = serde_json::de::from_reader(file).with_context(|| {
+ ErrorKind::PackageManifestParseError {
+ package: package.into(),
+ }
+ })?;
+
+ // If the bin list contains only an empty string, that means `bin` was a string value,
+ // rather than a map. In that case, to match `npm`s behavior, we use the name of the package
+ // as the bin name.
+ // Note: For a scoped package, we should remove the scope and only use the package name
+ if manifest.bin == [""] {
+ manifest.bin.pop();
+ manifest.bin.push(default_binary_name(&manifest.name));
+ }
+
+ Ok(manifest)
+ }
+}
+
+#[derive(serde::Deserialize)]
+/// Struct to read the `dependencies` out of Yarn's global manifest.
+///
+/// For global installs, yarn creates a `package.json` file in the `global-folder` and installs
+/// global packages as dependencies of that pseudo-package
+pub(super) struct GlobalYarnManifest {
+ #[serde(default)]
+ pub dependencies: HashMap<String, String>,
+}
+
+mod serde_bins {
+ use std::fmt;
+
+ use serde::de::{Deserializer, Error, MapAccess, Visitor};
+
+ pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ deserializer.deserialize_any(BinMapVisitor)
+ }
+
+ struct BinMapVisitor;
+
+ impl<'de> Visitor<'de> for BinMapVisitor {
+ type Value = Vec<String>;
+
+ fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.write_str("string or map")
+ }
+
+ // Handle String values with only the path
+ fn visit_str<E>(self, _path: &str) -> Result<Self::Value, E>
+ where
+ E: Error,
+ {
+ // Use an empty string as a placeholder for the binary name, since at this level we
+ // don't know the binary name for sure (npm uses the package name in this case)
+ Ok(vec![String::new()])
+ }
+
+ // Handle maps of Name -> Path
+ fn visit_map<M>(self, mut access: M) -> Result<Self::Value, M::Error>
+ where
+ M: MapAccess<'de>,
+ {
+ let mut bins = Vec::new();
+ while let Some((name, _)) = access.next_entry::<String, String>()? {
+ // Bin names that include path separators are invalid, as they would then point to
+ // other locations on the filesystem. To match the behavior of npm & Yarn, we
+ // filter those values out of the list of bins.
+ if !name.contains(&['/', '\\'][..]) {
+ bins.push(name);
+ }
+ }
+ Ok(bins)
+ }
+ }
+}
+
+/// Determine the default binary name from the package name
+///
+/// For non-scoped packages, this is just the package name
+/// For scoped packages, to match the behavior of the package managers, we remove the scope and use
+/// only the package part, e.g. `@microsoft/rush` would have a default name of `rush`
+fn default_binary_name(package_name: &str) -> String {
+ if package_name.starts_with('@') {
+ let mut chars = package_name.chars();
+
+ loop {
+ match chars.next() {
+ Some('/') | None => break,
+ _ => {}
+ }
+ }
+
+ let name = chars.as_str();
+ if name.is_empty() {
+ package_name.to_string()
+ } else {
+ name.to_string()
+ }
+ } else {
+ package_name.to_string()
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::default_binary_name;
+
+ #[test]
+ fn default_binary_uses_full_name_if_unscoped() {
+ assert_eq!(default_binary_name("my-package"), "my-package");
+ }
+
+ #[test]
+ fn default_binary_removes_scope() {
+ assert_eq!(default_binary_name("@scope/my-package"), "my-package");
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +
use std::fmt::{self, Display};
+use std::fs::create_dir_all;
+use std::path::{Path, PathBuf};
+use std::process::Command;
+
+use super::Tool;
+use crate::error::{Context, ErrorKind, Fallible};
+use crate::fs::{remove_dir_if_exists, rename, symlink_dir};
+use crate::layout::volta_home;
+use crate::platform::{Image, PlatformSpec};
+use crate::session::Session;
+use crate::style::{success_prefix, tool_version};
+use crate::sync::VoltaLock;
+use crate::version::VersionSpec;
+use fs_utils::ensure_containing_dir_exists;
+use log::info;
+use tempfile::{tempdir_in, TempDir};
+
+mod configure;
+mod install;
+mod manager;
+mod metadata;
+mod uninstall;
+
+pub use manager::PackageManager;
+pub use metadata::{BinConfig, PackageConfig, PackageManifest};
+pub use uninstall::uninstall;
+
+/// The Tool implementation for installing 3rd-party global packages
+pub struct Package {
+ name: String,
+ version: VersionSpec,
+ staging: TempDir,
+}
+
+impl Package {
+ pub fn new(name: String, version: VersionSpec) -> Fallible<Self> {
+ let staging = setup_staging_directory(PackageManager::Npm, NeedsScope::No)?;
+
+ Ok(Package {
+ name,
+ version,
+ staging,
+ })
+ }
+
+ pub fn run_install(&self, platform_image: &Image) -> Fallible<()> {
+ install::run_global_install(
+ self.to_string(),
+ self.staging.path().to_owned(),
+ platform_image,
+ )
+ }
+
+ pub fn complete_install(self, image: &Image) -> Fallible<PackageManifest> {
+ let manager = PackageManager::Npm;
+ let manifest =
+ configure::parse_manifest(&self.name, self.staging.path().to_owned(), manager)?;
+
+ persist_install(&self.name, &self.version, self.staging.path())?;
+ link_package_to_shared_dir(&self.name, manager)?;
+ configure::write_config_and_shims(&self.name, &manifest, image, manager)?;
+
+ Ok(manifest)
+ }
+}
+
+impl Tool for Package {
+ fn fetch(self: Box<Self>, _session: &mut Session) -> Fallible<()> {
+ Err(ErrorKind::CannotFetchPackage {
+ package: self.to_string(),
+ }
+ .into())
+ }
+
+ fn install(self: Box<Self>, session: &mut Session) -> Fallible<()> {
+ let _lock = VoltaLock::acquire();
+
+ let default_image = session
+ .default_platform()?
+ .map(PlatformSpec::as_default)
+ .ok_or(ErrorKind::NoPlatform)?
+ .checkout(session)?;
+
+ self.run_install(&default_image)?;
+ let manifest = self.complete_install(&default_image)?;
+
+ let bins = manifest.bin.join(", ");
+
+ if bins.is_empty() {
+ info!(
+ "{} installed {}",
+ success_prefix(),
+ tool_version(manifest.name, manifest.version)
+ );
+ } else {
+ info!(
+ "{} installed {} with executables: {}",
+ success_prefix(),
+ tool_version(manifest.name, manifest.version),
+ bins
+ );
+ }
+
+ Ok(())
+ }
+
+ fn pin(self: Box<Self>, _session: &mut Session) -> Fallible<()> {
+ Err(ErrorKind::CannotPinPackage { package: self.name }.into())
+ }
+}
+
+impl Display for Package {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self.version {
+ VersionSpec::None => f.write_str(&self.name),
+ _ => f.write_str(&tool_version(&self.name, &self.version)),
+ }
+ }
+}
+
+/// Helper struct for direct installs through `npm i -g` or `yarn global add`
+///
+/// Provides methods to simplify installing into a staging directory and then moving that install
+/// into the proper location after it is complete.
+///
+/// Note: We don't always know the name of the package up-front, as the install could be from a
+/// tarball or a git coordinate. If we do know ahead of time, then we can skip looking it up
+pub struct DirectInstall {
+ staging: TempDir,
+ manager: PackageManager,
+ name: Option<String>,
+}
+
+impl DirectInstall {
+ pub fn new(manager: PackageManager) -> Fallible<Self> {
+ let staging = setup_staging_directory(manager, NeedsScope::No)?;
+
+ Ok(DirectInstall {
+ staging,
+ manager,
+ name: None,
+ })
+ }
+
+ pub fn with_name(manager: PackageManager, name: String) -> Fallible<Self> {
+ let staging = setup_staging_directory(manager, name.contains('/').into())?;
+
+ Ok(DirectInstall {
+ staging,
+ manager,
+ name: Some(name),
+ })
+ }
+
+ pub fn setup_command(&self, command: &mut Command) {
+ self.manager
+ .setup_global_command(command, self.staging.path().to_owned());
+ }
+
+ pub fn complete_install(self, image: &Image) -> Fallible<()> {
+ let DirectInstall {
+ staging,
+ name,
+ manager,
+ } = self;
+
+ let name = name
+ .or_else(|| manager.get_installed_package(staging.path().to_owned()))
+ .ok_or(ErrorKind::InstalledPackageNameError)?;
+ let manifest = configure::parse_manifest(&name, staging.path().to_owned(), manager)?;
+
+ persist_install(&name, &manifest.version, staging.path())?;
+ link_package_to_shared_dir(&name, manager)?;
+ configure::write_config_and_shims(&name, &manifest, image, manager)
+ }
+}
+
+/// Helper struct for direct in-place upgrades using `npm update -g` or `yarn global upgrade`
+///
+/// Upgrades the requested package directly in the image directory
+pub struct InPlaceUpgrade {
+ package: String,
+ directory: PathBuf,
+ manager: PackageManager,
+}
+
+impl InPlaceUpgrade {
+ pub fn new(package: String, manager: PackageManager) -> Fallible<Self> {
+ let directory = volta_home()?.package_image_dir(&package);
+
+ Ok(Self {
+ package,
+ directory,
+ manager,
+ })
+ }
+
+ /// Check for possible failure cases with the package to be upgraded
+ /// - The package is not installed as a global
+ /// - The package exists, but was installed with a different package manager
+ pub fn check_upgraded_package(&self) -> Fallible<()> {
+ let config =
+ PackageConfig::from_file(volta_home()?.default_package_config_file(&self.package))
+ .with_context(|| ErrorKind::UpgradePackageNotFound {
+ package: self.package.clone(),
+ manager: self.manager,
+ })?;
+
+ if config.manager != self.manager {
+ Err(ErrorKind::UpgradePackageWrongManager {
+ package: self.package.clone(),
+ manager: config.manager,
+ }
+ .into())
+ } else {
+ Ok(())
+ }
+ }
+
+ pub fn setup_command(&self, command: &mut Command) {
+ self.manager
+ .setup_global_command(command, self.directory.clone());
+ }
+
+ pub fn complete_upgrade(self, image: &Image) -> Fallible<()> {
+ let manifest = configure::parse_manifest(&self.package, self.directory, self.manager)?;
+
+ link_package_to_shared_dir(&self.package, self.manager)?;
+ configure::write_config_and_shims(&self.package, &manifest, image, self.manager)
+ }
+}
+
+#[derive(Clone, Copy, PartialEq)]
+enum NeedsScope {
+ Yes,
+ No,
+}
+
+impl From<bool> for NeedsScope {
+ fn from(value: bool) -> Self {
+ if value {
+ NeedsScope::Yes
+ } else {
+ NeedsScope::No
+ }
+ }
+}
+
+/// Create the temporary staging directory we will use to install and ensure expected
+/// subdirectories exist within it
+fn setup_staging_directory(manager: PackageManager, needs_scope: NeedsScope) -> Fallible<TempDir> {
+ // Workaround to ensure relative symlinks continue to work.
+ // The final installed location of packages is:
+ // $VOLTA_HOME/tools/image/packages/{name}/
+ // To ensure that the temp directory has the same amount of nesting, we use:
+ // $VOLTA_HOME/tmp/image/packages/{tempdir}/
+ // This way any relative symlinks will have the same amount of nesting and will remain valid
+ // even when the directory is persisted.
+ // We also need to handle the case when the linked package has a scope, which requires another
+ // level of nesting
+ let mut staging_root = volta_home()?.tmp_dir().to_owned();
+ staging_root.push("image");
+ staging_root.push("packages");
+ if needs_scope == NeedsScope::Yes {
+ staging_root.push("scope");
+ }
+ create_dir_all(&staging_root).with_context(|| ErrorKind::ContainingDirError {
+ path: staging_root.clone(),
+ })?;
+ let staging = tempdir_in(&staging_root).with_context(|| ErrorKind::CreateTempDirError {
+ in_dir: staging_root,
+ })?;
+
+ let source_dir = manager.source_dir(staging.path().to_owned());
+ ensure_containing_dir_exists(&source_dir)
+ .with_context(|| ErrorKind::ContainingDirError { path: source_dir })?;
+
+ let binary_dir = manager.binary_dir(staging.path().to_owned());
+ ensure_containing_dir_exists(&binary_dir)
+ .with_context(|| ErrorKind::ContainingDirError { path: binary_dir })?;
+
+ Ok(staging)
+}
+
+fn persist_install<V>(package_name: &str, package_version: V, staging_dir: &Path) -> Fallible<()>
+where
+ V: Display,
+{
+ let package_dir = volta_home()?.package_image_dir(package_name);
+
+ remove_dir_if_exists(&package_dir)?;
+
+ // Handle scoped packages (@vue/cli), which have an extra directory for the scope
+ ensure_containing_dir_exists(&package_dir).with_context(|| ErrorKind::ContainingDirError {
+ path: package_dir.to_owned(),
+ })?;
+
+ rename(staging_dir, &package_dir).with_context(|| ErrorKind::SetupToolImageError {
+ tool: package_name.into(),
+ version: package_version.to_string(),
+ dir: package_dir,
+ })?;
+
+ Ok(())
+}
+
+fn link_package_to_shared_dir(package_name: &str, manager: PackageManager) -> Fallible<()> {
+ let home = volta_home()?;
+ let mut source = manager.source_dir(home.package_image_dir(package_name));
+ source.push(package_name);
+
+ let target = home.shared_lib_dir(package_name);
+
+ remove_dir_if_exists(&target)?;
+
+ // Handle scoped packages (@vue/cli), which have an extra directory for the scope
+ ensure_containing_dir_exists(&target).with_context(|| ErrorKind::ContainingDirError {
+ path: target.clone(),
+ })?;
+
+ symlink_dir(source, target).with_context(|| ErrorKind::CreateSharedLinkError {
+ name: package_name.into(),
+ })
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +
use super::metadata::{BinConfig, PackageConfig};
+use crate::error::{Context, ErrorKind, Fallible};
+use crate::fs::{
+ dir_entry_match, ok_if_not_found, read_dir_eager, remove_dir_if_exists, remove_file_if_exists,
+};
+use crate::layout::volta_home;
+use crate::shim;
+use crate::style::success_prefix;
+use crate::sync::VoltaLock;
+use log::{info, warn};
+
+/// Uninstalls the specified package.
+///
+/// This removes:
+///
+/// - The JSON configuration files for both the package and its bins
+/// - The shims for the package bins
+/// - The package directory itself
+pub fn uninstall(name: &str) -> Fallible<()> {
+ let home = volta_home()?;
+ // Acquire a lock on the Volta directory, if possible, to prevent concurrent changes
+ let _lock = VoltaLock::acquire();
+
+ // If the package config file exists, use that to remove any installed bins and shims
+ let package_config_file = home.default_package_config_file(name);
+
+ let package_found = match PackageConfig::from_file_if_exists(&package_config_file)? {
+ None => {
+ // there is no package config - check for orphaned binaries
+ let package_binary_list = binaries_from_package(name)?;
+ if !package_binary_list.is_empty() {
+ for bin_name in package_binary_list {
+ remove_config_and_shim(&bin_name, name)?;
+ }
+ true
+ } else {
+ false
+ }
+ }
+ Some(package_config) => {
+ for bin_name in package_config.bins {
+ remove_config_and_shim(&bin_name, name)?;
+ }
+
+ remove_file_if_exists(package_config_file)?;
+ true
+ }
+ };
+
+ remove_shared_link_dir(name)?;
+
+ // Remove the package directory itself
+ let package_image_dir = home.package_image_dir(name);
+ remove_dir_if_exists(package_image_dir)?;
+
+ if package_found {
+ info!("{} package '{}' uninstalled", success_prefix(), name);
+ } else {
+ warn!("No package '{}' found to uninstall", name);
+ }
+
+ Ok(())
+}
+
+/// Remove a shim and its associated configuration file
+fn remove_config_and_shim(bin_name: &str, pkg_name: &str) -> Fallible<()> {
+ shim::delete(bin_name)?;
+ let config_file = volta_home()?.default_tool_bin_config(bin_name);
+ remove_file_if_exists(config_file)?;
+ info!(
+ "Removed executable '{}' installed by '{}'",
+ bin_name, pkg_name
+ );
+ Ok(())
+}
+
+/// Reads the contents of a directory and returns a Vec containing the names of
+/// all the binaries installed by the given package.
+fn binaries_from_package(package: &str) -> Fallible<Vec<String>> {
+ let bin_config_dir = volta_home()?.default_bin_dir();
+
+ dir_entry_match(bin_config_dir, |entry| {
+ let path = entry.path();
+ if let Ok(config) = BinConfig::from_file(path) {
+ if config.package == package {
+ return Some(config.name);
+ }
+ }
+ None
+ })
+ .or_else(ok_if_not_found)
+ .with_context(|| ErrorKind::ReadBinConfigDirError {
+ dir: bin_config_dir.to_owned(),
+ })
+}
+
+/// Remove the link to the package in the shared lib directory
+///
+/// For scoped packages, if the scope directory is now empty, it will also be removed
+fn remove_shared_link_dir(name: &str) -> Fallible<()> {
+ // Remove the link in the shared package directory, if it exists
+ let mut shared_lib_dir = volta_home()?.shared_lib_dir(name);
+ remove_dir_if_exists(&shared_lib_dir)?;
+
+ // For scoped packages, clean up the scope directory if it is now empty
+ if name.starts_with('@') {
+ shared_lib_dir.pop();
+
+ if let Ok(mut entries) = read_dir_eager(&shared_lib_dir) {
+ if entries.next().is_none() {
+ remove_dir_if_exists(&shared_lib_dir)?;
+ }
+ }
+ }
+
+ Ok(())
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +
//! Provides fetcher for pnpm distributions
+
+use std::fs::{write, File};
+use std::path::Path;
+
+use archive::{Archive, Tarball};
+use fs_utils::ensure_containing_dir_exists;
+use log::debug;
+use node_semver::Version;
+
+use crate::error::{Context, ErrorKind, Fallible};
+use crate::fs::{create_staging_dir, create_staging_file, rename, set_executable};
+use crate::hook::ToolHooks;
+use crate::layout::volta_home;
+use crate::style::{progress_bar, tool_version};
+use crate::tool::registry::public_registry_package;
+use crate::tool::{self, download_tool_error, Pnpm};
+use crate::version::VersionSpec;
+
+pub fn fetch(version: &Version, hooks: Option<&ToolHooks<Pnpm>>) -> Fallible<()> {
+ let pnpm_dir = volta_home()?.pnpm_inventory_dir();
+ let cache_file = pnpm_dir.join(Pnpm::archive_filename(&version.to_string()));
+
+ let (archive, staging) = match load_cached_distro(&cache_file) {
+ Some(archive) => {
+ debug!(
+ "Loading {} from cached archive at '{}'",
+ tool_version("pnpm", version),
+ cache_file.display(),
+ );
+ (archive, None)
+ }
+ None => {
+ let staging = create_staging_file()?;
+ let remote_url = determine_remote_url(version, hooks)?;
+ let archive = fetch_remote_distro(version, &remote_url, staging.path())?;
+ (archive, Some(staging))
+ }
+ };
+
+ unpack_archive(archive, version)?;
+
+ if let Some(staging_file) = staging {
+ ensure_containing_dir_exists(&cache_file).with_context(|| {
+ ErrorKind::ContainingDirError {
+ path: cache_file.clone(),
+ }
+ })?;
+ staging_file
+ .persist(cache_file)
+ .with_context(|| ErrorKind::PersistInventoryError {
+ tool: "pnpm".into(),
+ })?;
+ }
+
+ Ok(())
+}
+
+/// Unpack the pnpm archive into the image directory so that it is ready for use
+fn unpack_archive(archive: Box<dyn Archive>, version: &Version) -> Fallible<()> {
+ let temp = create_staging_dir()?;
+ debug!("Unpacking pnpm into '{}'", temp.path().display());
+
+ let progress = progress_bar(
+ archive.origin(),
+ &tool_version("pnpm", version),
+ archive.compressed_size(),
+ );
+ let version_string = version.to_string();
+
+ archive
+ .unpack(temp.path(), &mut |_, read| {
+ progress.inc(read as u64);
+ })
+ .with_context(|| ErrorKind::UnpackArchiveError {
+ tool: "pnpm".into(),
+ version: version_string.clone(),
+ })?;
+
+ let bin_path = temp.path().join("package").join("bin");
+ write_launcher(&bin_path, "pnpm")?;
+ write_launcher(&bin_path, "pnpx")?;
+
+ #[cfg(windows)]
+ {
+ write_cmd_launcher(&bin_path, "pnpm")?;
+ write_cmd_launcher(&bin_path, "pnpx")?;
+ }
+
+ let dest = volta_home()?.pnpm_image_dir(&version_string);
+ ensure_containing_dir_exists(&dest)
+ .with_context(|| ErrorKind::ContainingDirError { path: dest.clone() })?;
+
+ rename(temp.path().join("package"), &dest).with_context(|| ErrorKind::SetupToolImageError {
+ tool: "pnpm".into(),
+ version: version_string.clone(),
+ dir: dest.clone(),
+ })?;
+
+ progress.finish_and_clear();
+
+ // Note: We write this after the progress bar is finished to avoid display bugs with re-renders of the progress
+ debug!("Installing pnpm in '{}'", dest.display());
+
+ Ok(())
+}
+
+/// Return the archive if it is valid. It may have been corrupted or interrupted in the middle of
+/// downloading.
+// ISSUE(#134) - verify checksum
+fn load_cached_distro(file: &Path) -> Option<Box<dyn Archive>> {
+ if file.is_file() {
+ let file = File::open(file).ok()?;
+ Tarball::load(file).ok()
+ } else {
+ None
+ }
+}
+
+/// Determine the remote URL to download from, using the hooks if avaialble
+fn determine_remote_url(version: &Version, hooks: Option<&ToolHooks<Pnpm>>) -> Fallible<String> {
+ let version_str = version.to_string();
+ match hooks {
+ Some(&ToolHooks {
+ distro: Some(ref hook),
+ ..
+ }) => {
+ debug!("Using pnpm.distro hook to determine download URL");
+ let distro_file_name = Pnpm::archive_filename(&version_str);
+ hook.resolve(version, &distro_file_name)
+ }
+ _ => Ok(public_registry_package("pnpm", &version_str)),
+ }
+}
+
+/// Fetch the distro archive from the internet
+fn fetch_remote_distro(
+ version: &Version,
+ url: &str,
+ staging_path: &Path,
+) -> Fallible<Box<dyn Archive>> {
+ debug!("Downloading {} from {}", tool_version("pnpm", version), url);
+ Tarball::fetch(url, staging_path).with_context(download_tool_error(
+ tool::Spec::Pnpm(VersionSpec::Exact(version.clone())),
+ url,
+ ))
+}
+
+/// Create executable launchers for the pnpm and pnpx binaries
+fn write_launcher(base_path: &Path, tool: &str) -> Fallible<()> {
+ let path = base_path.join(tool);
+ write(
+ &path,
+ format!(
+ r#"#!/bin/sh
+(set -o igncr) 2>/dev/null && set -o igncr; # cygwin encoding fix
+
+basedir=`dirname "$0"`
+
+case `uname` in
+ *CYGWIN*) basedir=`cygpath -w "$basedir"`;;
+esac
+
+node "$basedir/{}.cjs" "$@"
+"#,
+ tool
+ ),
+ )
+ .and_then(|_| set_executable(&path))
+ .with_context(|| ErrorKind::WriteLauncherError { tool: tool.into() })
+}
+
+/// Create CMD executable launchers for the pnpm and pnpx binaries for Windows
+#[cfg(windows)]
+fn write_cmd_launcher(base_path: &Path, tool: &str) -> Fallible<()> {
+ write(
+ base_path.join(format!("{}.cmd", tool)),
+ format!("@echo off\nnode \"%~dp0\\{}.cjs\" %*", tool),
+ )
+ .with_context(|| ErrorKind::WriteLauncherError { tool: tool.into() })
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +
use node_semver::Version;
+use std::fmt::{self, Display};
+
+use crate::error::{ErrorKind, Fallible};
+use crate::inventory::pnpm_available;
+use crate::session::Session;
+use crate::style::tool_version;
+use crate::sync::VoltaLock;
+
+use super::{
+ check_fetched, check_shim_reachable, debug_already_fetched, info_fetched, info_installed,
+ info_pinned, info_project_version, FetchStatus, Tool,
+};
+
+mod fetch;
+mod resolve;
+
+pub use resolve::resolve;
+
+/// The Tool implementation for fetching and installing pnpm
+pub struct Pnpm {
+ pub(super) version: Version,
+}
+
+impl Pnpm {
+ pub fn new(version: Version) -> Self {
+ Pnpm { version }
+ }
+
+ pub fn archive_basename(version: &str) -> String {
+ format!("pnpm-{}", version)
+ }
+
+ pub fn archive_filename(version: &str) -> String {
+ format!("{}.tgz", Pnpm::archive_basename(version))
+ }
+
+ pub(crate) fn ensure_fetched(&self, session: &mut Session) -> Fallible<()> {
+ match check_fetched(|| pnpm_available(&self.version))? {
+ FetchStatus::AlreadyFetched => {
+ debug_already_fetched(self);
+ Ok(())
+ }
+ FetchStatus::FetchNeeded(_lock) => fetch::fetch(&self.version, session.hooks()?.pnpm()),
+ }
+ }
+}
+
+impl Tool for Pnpm {
+ fn fetch(self: Box<Self>, session: &mut Session) -> Fallible<()> {
+ self.ensure_fetched(session)?;
+
+ info_fetched(self);
+ Ok(())
+ }
+
+ fn install(self: Box<Self>, session: &mut Session) -> Fallible<()> {
+ // Acquire a lock on the Volta directory, if possible, to prevent concurrent changes
+ let _lock = VoltaLock::acquire();
+ self.ensure_fetched(session)?;
+
+ session
+ .toolchain_mut()?
+ .set_active_pnpm(Some(self.version.clone()))?;
+
+ info_installed(&self);
+ check_shim_reachable("pnpm");
+
+ if let Ok(Some(project)) = session.project_platform() {
+ if let Some(pnpm) = &project.pnpm {
+ info_project_version(tool_version("pnpm", pnpm), &self);
+ }
+ }
+ Ok(())
+ }
+
+ fn pin(self: Box<Self>, session: &mut Session) -> Fallible<()> {
+ if session.project()?.is_some() {
+ self.ensure_fetched(session)?;
+
+ // Note: We know this will succeed, since we checked above
+ let project = session.project_mut()?.unwrap();
+ project.pin_pnpm(Some(self.version.clone()))?;
+
+ info_pinned(self);
+ Ok(())
+ } else {
+ Err(ErrorKind::NotInPackage.into())
+ }
+ }
+}
+
+impl Display for Pnpm {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.write_str(&tool_version("pnpm", &self.version))
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_pnpm_archive_basename() {
+ assert_eq!(Pnpm::archive_basename("1.2.3"), "pnpm-1.2.3");
+ }
+
+ #[test]
+ fn test_pnpm_archive_filename() {
+ assert_eq!(Pnpm::archive_filename("1.2.3"), "pnpm-1.2.3.tgz");
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +
use log::debug;
+use node_semver::{Range, Version};
+
+use crate::error::{ErrorKind, Fallible};
+use crate::hook::ToolHooks;
+use crate::session::Session;
+use crate::tool::registry::{fetch_npm_registry, public_registry_index, PackageIndex};
+use crate::tool::{PackageDetails, Pnpm};
+use crate::version::{VersionSpec, VersionTag};
+
+pub fn resolve(matching: VersionSpec, session: &mut Session) -> Fallible<Version> {
+ let hooks = session.hooks()?.pnpm();
+ match matching {
+ VersionSpec::Semver(requirement) => resolve_semver(requirement, hooks),
+ VersionSpec::Exact(version) => Ok(version),
+ VersionSpec::None | VersionSpec::Tag(VersionTag::Latest) => resolve_tag("latest", hooks),
+ VersionSpec::Tag(tag) => resolve_tag(&tag.to_string(), hooks),
+ }
+}
+
+fn resolve_tag(tag: &str, hooks: Option<&ToolHooks<Pnpm>>) -> Fallible<Version> {
+ let (url, mut index) = fetch_pnpm_index(hooks)?;
+
+ match index.tags.remove(tag) {
+ Some(version) => {
+ debug!("Found pnpm@{} matching tag '{}' from {}", version, tag, url);
+ Ok(version)
+ }
+ None => Err(ErrorKind::PnpmVersionNotFound {
+ matching: tag.into(),
+ }
+ .into()),
+ }
+}
+
+fn resolve_semver(matching: Range, hooks: Option<&ToolHooks<Pnpm>>) -> Fallible<Version> {
+ let (url, index) = fetch_pnpm_index(hooks)?;
+
+ let details_opt = index
+ .entries
+ .into_iter()
+ .find(|PackageDetails { version, .. }| matching.satisfies(version));
+
+ match details_opt {
+ Some(details) => {
+ debug!(
+ "Found pnpm@{} matching requirement '{}' from {}",
+ details.version, matching, url
+ );
+ Ok(details.version)
+ }
+ None => Err(ErrorKind::PnpmVersionNotFound {
+ matching: matching.to_string(),
+ }
+ .into()),
+ }
+}
+
+/// Fetch the index of available pnpm versions from the npm registry
+fn fetch_pnpm_index(hooks: Option<&ToolHooks<Pnpm>>) -> Fallible<(String, PackageIndex)> {
+ let url = match hooks {
+ Some(&ToolHooks {
+ index: Some(ref hook),
+ ..
+ }) => {
+ debug!("Using pnpm.index hook to determine pnpm index URL");
+ hook.resolve("pnpm")?
+ }
+ _ => public_registry_index("pnpm"),
+ };
+
+ fetch_npm_registry(url, "pnpm")
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +
use std::collections::HashMap;
+use std::path::{Path, PathBuf};
+
+use super::registry_fetch_error;
+use crate::error::{Context, ErrorKind, Fallible};
+use crate::fs::read_dir_eager;
+use crate::style::progress_spinner;
+use crate::version::{hashmap_version_serde, version_serde};
+use attohttpc::header::ACCEPT;
+use attohttpc::Response;
+use cfg_if::cfg_if;
+use node_semver::Version;
+use serde::Deserialize;
+
+// Accept header needed to request the abbreviated metadata from the npm registry
+// See https://github.com/npm/registry/blob/master/docs/responses/package-metadata.md
+pub const NPM_ABBREVIATED_ACCEPT_HEADER: &str =
+ "application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*";
+
+cfg_if! {
+ if #[cfg(feature = "mock-network")] {
+ // TODO: We need to reconsider our mocking strategy in light of mockito deprecating the
+ // SERVER_URL constant: Since our acceptance tests run the binary in a separate process,
+ // we can't use `mockito::server_url()`, which relies on shared memory.
+ #[allow(deprecated)]
+ const SERVER_URL: &str = mockito::SERVER_URL;
+ pub fn public_registry_index(package: &str) -> String {
+ format!("{}/{}", SERVER_URL, package)
+ }
+ } else {
+ pub fn public_registry_index(package: &str) -> String {
+ format!("https://registry.npmjs.org/{}", package)
+ }
+ }
+}
+
+// fetch a registry that returns info in Npm format
+pub fn fetch_npm_registry(url: String, name: &str) -> Fallible<(String, PackageIndex)> {
+ let spinner = progress_spinner(format!("Fetching npm registry: {}", url));
+ let metadata: RawPackageMetadata = attohttpc::get(&url)
+ .header(ACCEPT, NPM_ABBREVIATED_ACCEPT_HEADER)
+ .send()
+ .and_then(Response::error_for_status)
+ .and_then(Response::json)
+ .with_context(registry_fetch_error(name, &url))?;
+
+ spinner.finish_and_clear();
+ Ok((url, metadata.into()))
+}
+
+pub fn public_registry_package(package: &str, version: &str) -> String {
+ format!(
+ "{}/-/{}-{}.tgz",
+ public_registry_index(package),
+ package,
+ version
+ )
+}
+
+// need package and filename for namespaced tools like @yarnpkg/cli-dist, which is located at
+// https://registry.npmjs.org/@yarnpkg/cli-dist/-/cli-dist-1.2.3.tgz
+pub fn scoped_public_registry_package(scope: &str, package: &str, version: &str) -> String {
+ format!(
+ "{}/{}/-/{}-{}.tgz",
+ public_registry_index(scope),
+ package,
+ package,
+ version
+ )
+}
+
+/// Figure out the unpacked package directory name dynamically
+///
+/// Packages typically extract to a "package" directory, but not always
+pub fn find_unpack_dir(in_dir: &Path) -> Fallible<PathBuf> {
+ let dirs: Vec<_> = read_dir_eager(in_dir)
+ .with_context(|| ErrorKind::PackageUnpackError)?
+ .collect();
+
+ // if there is only one directory, return that
+ if let [(entry, metadata)] = dirs.as_slice() {
+ if metadata.is_dir() {
+ return Ok(entry.path());
+ }
+ }
+ // there is more than just a single directory here, something is wrong
+ Err(ErrorKind::PackageUnpackError.into())
+}
+
+/// Details about a package in the npm Registry
+#[derive(Debug)]
+pub struct PackageDetails {
+ pub(crate) version: Version,
+}
+
+/// Index of versions of a specific package from the npm Registry
+pub struct PackageIndex {
+ pub tags: HashMap<String, Version>,
+ pub entries: Vec<PackageDetails>,
+}
+
+/// Package Metadata Response
+///
+/// See npm registry API doc:
+/// https://github.com/npm/registry/blob/master/docs/REGISTRY-API.md
+#[derive(Deserialize, Debug)]
+pub struct RawPackageMetadata {
+ pub name: String,
+ pub versions: HashMap<String, RawPackageVersionInfo>,
+ #[serde(
+ rename = "dist-tags",
+ deserialize_with = "hashmap_version_serde::deserialize"
+ )]
+ pub dist_tags: HashMap<String, Version>,
+}
+
+#[derive(Deserialize, Debug)]
+pub struct RawPackageVersionInfo {
+ // there's a lot more in there, but right now just care about the version
+ #[serde(with = "version_serde")]
+ pub version: Version,
+ pub dist: RawDistInfo,
+}
+
+#[derive(Deserialize, Clone, Debug)]
+pub struct RawDistInfo {
+ pub shasum: String,
+ pub tarball: String,
+}
+
+impl From<RawPackageMetadata> for PackageIndex {
+ fn from(serial: RawPackageMetadata) -> PackageIndex {
+ let mut entries: Vec<PackageDetails> = serial
+ .versions
+ .into_values()
+ .map(|version_info| PackageDetails {
+ version: version_info.version,
+ })
+ .collect();
+
+ entries.sort_by(|a, b| b.version.cmp(&a.version));
+
+ PackageIndex {
+ tags: serial.dist_tags,
+ entries,
+ }
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402 +403 +404 +405 +406 +407 +408 +409 +410 +411 +412 +413 +414 +415 +416 +417 +418 +419 +420 +421 +422 +423 +424 +425 +426 +427 +428 +429 +430 +431 +432 +433 +434 +435 +436 +437 +438 +439 +440 +441 +442 +443 +444 +445 +446 +447 +448 +449 +450 +451 +452 +453 +454 +455 +456 +457 +458 +459 +460 +461 +462 +463 +464 +465 +466 +467 +468 +469 +470 +471 +472 +473 +474 +475 +476 +477 +478 +479 +480 +481 +482 +483 +484 +485 +486 +487 +488 +489 +490 +491 +492 +493 +494 +495 +496 +497 +498 +499 +500 +501 +502 +503 +504 +505 +506 +507 +508 +509 +510 +511 +512 +
use std::cmp::Ordering;
+
+use super::Spec;
+use crate::error::{ErrorKind, Fallible};
+use crate::version::{VersionSpec, VersionTag};
+use once_cell::sync::Lazy;
+use regex::Regex;
+use validate_npm_package_name::{validate, Validity};
+
+static TOOL_SPEC_PATTERN: Lazy<Regex> = Lazy::new(|| {
+ Regex::new("^(?P<name>(?:@([^/]+?)[/])?([^/]+?))(@(?P<version>.+))?$").expect("regex is valid")
+});
+static HAS_VERSION: Lazy<Regex> = Lazy::new(|| Regex::new(r"^[^\s]+@").expect("regex is valid"));
+
+/// Methods for parsing a Spec out of string values
+impl Spec {
+ pub fn from_str_and_version(tool_name: &str, version: VersionSpec) -> Self {
+ match tool_name {
+ "node" => Spec::Node(version),
+ "npm" => Spec::Npm(version),
+ "pnpm" => Spec::Pnpm(version),
+ "yarn" => Spec::Yarn(version),
+ package => Spec::Package(package.to_string(), version),
+ }
+ }
+
+ /// Try to parse a tool and version from a string like `<tool>[@<version>].
+ pub fn try_from_str(tool_spec: &str) -> Fallible<Self> {
+ let captures =
+ TOOL_SPEC_PATTERN
+ .captures(tool_spec)
+ .ok_or_else(|| ErrorKind::ParseToolSpecError {
+ tool_spec: tool_spec.into(),
+ })?;
+
+ // Validate that the captured name is a valid NPM package name.
+ let name = &captures["name"];
+ if let Validity::Invalid { errors, .. } = validate(name) {
+ return Err(ErrorKind::InvalidToolName {
+ name: name.into(),
+ errors,
+ }
+ .into());
+ }
+
+ let version = captures
+ .name("version")
+ .map(|version| version.as_str().parse())
+ .transpose()?
+ .unwrap_or_default();
+
+ Ok(match name {
+ "node" => Spec::Node(version),
+ "npm" => Spec::Npm(version),
+ "pnpm" => Spec::Pnpm(version),
+ "yarn" => Spec::Yarn(version),
+ package => Spec::Package(package.into(), version),
+ })
+ }
+
+ /// Get a valid, sorted `Vec<Spec>` given a `Vec<String>`.
+ ///
+ /// Accounts for the following error conditions:
+ ///
+ /// - `volta install node 12`, where the user intended to install `node@12`
+ /// but used syntax like in nodenv or nvm
+ /// - invalid version specs
+ ///
+ /// Returns a listed sorted so that if `node` is included in the list, it is
+ /// always first.
+ pub fn from_strings<T>(tool_strs: &[T], action: &str) -> Fallible<Vec<Spec>>
+ where
+ T: AsRef<str>,
+ {
+ Self::check_args(tool_strs, action)?;
+
+ let mut tools = tool_strs
+ .iter()
+ .map(|arg| Self::try_from_str(arg.as_ref()))
+ .collect::<Fallible<Vec<Spec>>>()?;
+
+ tools.sort_by(Self::sort_comparator);
+ Ok(tools)
+ }
+
+ /// Check the args for the bad patterns of
+ /// - `volta install <number>`
+ /// - `volta install <tool> <number>`
+ fn check_args<T>(args: &[T], action: &str) -> Fallible<()>
+ where
+ T: AsRef<str>,
+ {
+ let mut args = args.iter();
+
+ match (args.next(), args.next(), args.next()) {
+ // The case we are concerned with here is where we have `<number>`.
+ // That is, exactly one argument, which is a valid version specifier.
+ //
+ // - `volta install node@12` is allowed.
+ // - `volta install 12` is an error.
+ // - `volta install lts` is an error.
+ (Some(maybe_version), None, None) if is_version_like(maybe_version.as_ref()) => {
+ Err(ErrorKind::InvalidInvocationOfBareVersion {
+ action: action.to_string(),
+ version: maybe_version.as_ref().to_string(),
+ }
+ .into())
+ }
+ // The case we are concerned with here is where we have `<tool> <number>`.
+ // This is only interesting if there are exactly two args. Then we care
+ // whether the two items are a bare name (with no `@version`), followed
+ // by a valid version specifier (ignoring custom tags). That is:
+ //
+ // - `volta install node@lts latest` is allowed.
+ // - `volta install node latest` is an error.
+ // - `volta install node latest yarn` is allowed.
+ (Some(name), Some(maybe_version), None)
+ if !HAS_VERSION.is_match(name.as_ref())
+ && is_version_like(maybe_version.as_ref()) =>
+ {
+ Err(ErrorKind::InvalidInvocation {
+ action: action.to_string(),
+ name: name.as_ref().to_string(),
+ version: maybe_version.as_ref().to_string(),
+ }
+ .into())
+ }
+ _ => Ok(()),
+ }
+ }
+
+ /// Compare `Spec`s for sorting when converting from strings
+ ///
+ /// We want to preserve the original order as much as possible, so we treat tools in
+ /// the same tool category as equal. We still need to pull Node to the front of the
+ /// list, followed by Npm, pnpm, Yarn, and then Packages last.
+ fn sort_comparator(left: &Spec, right: &Spec) -> Ordering {
+ match (left, right) {
+ (Spec::Node(_), Spec::Node(_)) => Ordering::Equal,
+ (Spec::Node(_), _) => Ordering::Less,
+ (_, Spec::Node(_)) => Ordering::Greater,
+ (Spec::Npm(_), Spec::Npm(_)) => Ordering::Equal,
+ (Spec::Npm(_), _) => Ordering::Less,
+ (_, Spec::Npm(_)) => Ordering::Greater,
+ (Spec::Pnpm(_), Spec::Pnpm(_)) => Ordering::Equal,
+ (Spec::Pnpm(_), _) => Ordering::Less,
+ (_, Spec::Pnpm(_)) => Ordering::Greater,
+ (Spec::Yarn(_), Spec::Yarn(_)) => Ordering::Equal,
+ (Spec::Yarn(_), _) => Ordering::Less,
+ (_, Spec::Yarn(_)) => Ordering::Greater,
+ (Spec::Package(_, _), Spec::Package(_, _)) => Ordering::Equal,
+ }
+ }
+}
+
+/// Determine if a given string is "version-like".
+///
+/// This means it is either 'latest', 'lts', a Version, or a Version Range.
+fn is_version_like(value: &str) -> bool {
+ matches!(
+ value.parse(),
+ Ok(VersionSpec::Exact(_))
+ | Ok(VersionSpec::Semver(_))
+ | Ok(VersionSpec::Tag(VersionTag::Latest))
+ | Ok(VersionSpec::Tag(VersionTag::Lts))
+ )
+}
+
+#[cfg(test)]
+mod tests {
+ mod try_from_str {
+ use std::str::FromStr as _;
+
+ use super::super::super::Spec;
+ use crate::version::{VersionSpec, VersionTag};
+
+ const LTS: &str = "lts";
+ const LATEST: &str = "latest";
+ const MAJOR: &str = "3";
+ const MINOR: &str = "3.0";
+ const PATCH: &str = "3.0.0";
+ const BETA: &str = "beta";
+
+ /// Convenience macro for generating the <tool>@<version> string.
+ macro_rules! versioned_tool {
+ ($tool:expr, $version:expr) => {
+ format!("{}@{}", $tool, $version)
+ };
+ }
+
+ #[test]
+ fn parses_bare_node() {
+ assert_eq!(
+ Spec::try_from_str("node").expect("succeeds"),
+ Spec::Node(VersionSpec::default())
+ );
+ }
+
+ #[test]
+ fn parses_node_with_valid_versions() {
+ let tool = "node";
+
+ assert_eq!(
+ Spec::try_from_str(&versioned_tool!(tool, MAJOR)).expect("succeeds"),
+ Spec::Node(VersionSpec::from_str(MAJOR).expect("`VersionSpec` has its own tests"))
+ );
+
+ assert_eq!(
+ Spec::try_from_str(&versioned_tool!(tool, MINOR)).expect("succeeds"),
+ Spec::Node(VersionSpec::from_str(MINOR).expect("`VersionSpec` has its own tests"))
+ );
+
+ assert_eq!(
+ Spec::try_from_str(&versioned_tool!(tool, PATCH)).expect("succeeds"),
+ Spec::Node(VersionSpec::from_str(PATCH).expect("`VersionSpec` has its own tests"))
+ );
+
+ assert_eq!(
+ Spec::try_from_str(&versioned_tool!(tool, LATEST)).expect("succeeds"),
+ Spec::Node(VersionSpec::Tag(VersionTag::Latest))
+ );
+
+ assert_eq!(
+ Spec::try_from_str(&versioned_tool!(tool, LTS)).expect("succeeds"),
+ Spec::Node(VersionSpec::Tag(VersionTag::Lts))
+ );
+ }
+
+ #[test]
+ fn parses_bare_yarn() {
+ assert_eq!(
+ Spec::try_from_str("yarn").expect("succeeds"),
+ Spec::Yarn(VersionSpec::default())
+ );
+ }
+
+ #[test]
+ fn parses_yarn_with_valid_versions() {
+ let tool = "yarn";
+
+ assert_eq!(
+ Spec::try_from_str(&versioned_tool!(tool, MAJOR)).expect("succeeds"),
+ Spec::Yarn(VersionSpec::from_str(MAJOR).expect("`VersionSpec` has its own tests"))
+ );
+
+ assert_eq!(
+ Spec::try_from_str(&versioned_tool!(tool, MINOR)).expect("succeeds"),
+ Spec::Yarn(VersionSpec::from_str(MINOR).expect("`VersionSpec` has its own tests"))
+ );
+
+ assert_eq!(
+ Spec::try_from_str(&versioned_tool!(tool, PATCH)).expect("succeeds"),
+ Spec::Yarn(VersionSpec::from_str(PATCH).expect("`VersionSpec` has its own tests"))
+ );
+
+ assert_eq!(
+ Spec::try_from_str(&versioned_tool!(tool, LATEST)).expect("succeeds"),
+ Spec::Yarn(VersionSpec::Tag(VersionTag::Latest))
+ );
+ }
+
+ #[test]
+ fn parses_bare_packages() {
+ let package = "ember-cli";
+ assert_eq!(
+ Spec::try_from_str(package).expect("succeeds"),
+ Spec::Package(package.into(), VersionSpec::default())
+ );
+ }
+
+ #[test]
+ fn parses_namespaced_packages() {
+ let package = "@types/lodash";
+ assert_eq!(
+ Spec::try_from_str(package).expect("succeeds"),
+ Spec::Package(package.into(), VersionSpec::default())
+ );
+ }
+
+ #[test]
+ fn parses_bare_packages_with_valid_versions() {
+ let package = "something-awesome";
+
+ assert_eq!(
+ Spec::try_from_str(&versioned_tool!(package, MAJOR)).expect("succeeds"),
+ Spec::Package(
+ package.into(),
+ VersionSpec::from_str(MAJOR).expect("`VersionSpec` has its own tests")
+ )
+ );
+
+ assert_eq!(
+ Spec::try_from_str(&versioned_tool!(package, MINOR)).expect("succeeds"),
+ Spec::Package(
+ package.into(),
+ VersionSpec::from_str(MINOR).expect("`VersionSpec` has its own tests")
+ )
+ );
+
+ assert_eq!(
+ Spec::try_from_str(&versioned_tool!(package, PATCH)).expect("succeeds"),
+ Spec::Package(
+ package.into(),
+ VersionSpec::from_str(PATCH).expect("`VersionSpec` has its own tests")
+ )
+ );
+
+ assert_eq!(
+ Spec::try_from_str(&versioned_tool!(package, LATEST)).expect("succeeds"),
+ Spec::Package(package.into(), VersionSpec::Tag(VersionTag::Latest))
+ );
+
+ assert_eq!(
+ Spec::try_from_str(&versioned_tool!(package, LTS)).expect("succeeds"),
+ Spec::Package(package.into(), VersionSpec::Tag(VersionTag::Lts))
+ );
+
+ assert_eq!(
+ Spec::try_from_str(&versioned_tool!(package, BETA)).expect("succeeds"),
+ Spec::Package(
+ package.into(),
+ VersionSpec::Tag(VersionTag::Custom(BETA.into()))
+ )
+ );
+ }
+
+ #[test]
+ fn parses_namespaced_packages_with_valid_versions() {
+ let package = "@something/awesome";
+
+ assert_eq!(
+ Spec::try_from_str(&versioned_tool!(package, MAJOR)).expect("succeeds"),
+ Spec::Package(
+ package.into(),
+ VersionSpec::from_str(MAJOR).expect("`VersionSpec` has its own tests")
+ )
+ );
+
+ assert_eq!(
+ Spec::try_from_str(&versioned_tool!(package, MINOR)).expect("succeeds"),
+ Spec::Package(
+ package.into(),
+ VersionSpec::from_str(MINOR).expect("`VersionSpec` has its own tests")
+ )
+ );
+
+ assert_eq!(
+ Spec::try_from_str(&versioned_tool!(package, PATCH)).expect("succeeds"),
+ Spec::Package(
+ package.into(),
+ VersionSpec::from_str(PATCH).expect("`VersionSpec` has its own tests")
+ )
+ );
+
+ assert_eq!(
+ Spec::try_from_str(&versioned_tool!(package, LATEST)).expect("succeeds"),
+ Spec::Package(package.into(), VersionSpec::Tag(VersionTag::Latest))
+ );
+
+ assert_eq!(
+ Spec::try_from_str(&versioned_tool!(package, LTS)).expect("succeeds"),
+ Spec::Package(package.into(), VersionSpec::Tag(VersionTag::Lts))
+ );
+
+ assert_eq!(
+ Spec::try_from_str(&versioned_tool!(package, BETA)).expect("succeeds"),
+ Spec::Package(
+ package.into(),
+ VersionSpec::Tag(VersionTag::Custom(BETA.into()))
+ )
+ );
+ }
+ }
+
+ mod from_strings {
+ use super::super::*;
+ use std::str::FromStr;
+
+ static PIN: &str = "pin";
+
+ #[test]
+ fn special_cases_just_number() {
+ let version = "1.2.3";
+ let args: Vec<String> = vec![version.into()];
+
+ let err = Spec::from_strings(&args, PIN).unwrap_err();
+
+ assert_eq!(
+ err.kind(),
+ &ErrorKind::InvalidInvocationOfBareVersion {
+ action: PIN.into(),
+ version: version.into()
+ },
+ "`volta <action> number` results in the correct error"
+ );
+ }
+
+ #[test]
+ fn special_cases_tool_space_number() {
+ let name = "potato";
+ let version = "1.2.3";
+ let args: Vec<String> = vec![name.into(), version.into()];
+
+ let err = Spec::from_strings(&args, PIN).unwrap_err();
+
+ assert_eq!(
+ err.kind(),
+ &ErrorKind::InvalidInvocation {
+ action: PIN.into(),
+ name: name.into(),
+ version: version.into()
+ },
+ "`volta <action> tool number` results in the correct error"
+ );
+ }
+
+ #[test]
+ fn leaves_other_scenarios_alone() {
+ let empty: Vec<&str> = Vec::new();
+ assert_eq!(
+ Spec::from_strings(&empty, PIN).expect("is ok").len(),
+ empty.len(),
+ "when there are no args"
+ );
+
+ let only_one = ["node".to_owned()];
+ assert_eq!(
+ Spec::from_strings(&only_one, PIN).expect("is ok").len(),
+ only_one.len(),
+ "when there is only one arg"
+ );
+
+ let one_with_explicit_verson = ["10@latest".to_owned()];
+ assert_eq!(
+ Spec::from_strings(&one_with_explicit_verson, PIN)
+ .expect("is ok")
+ .len(),
+ only_one.len(),
+ "when the sole arg is version-like but has an explicit version"
+ );
+
+ let two_but_unmistakable = ["12".to_owned(), "node".to_owned()];
+ assert_eq!(
+ Spec::from_strings(&two_but_unmistakable, PIN)
+ .expect("is ok")
+ .len(),
+ two_but_unmistakable.len(),
+ "when there are two args but the order is not likely to be a mistake"
+ );
+
+ let two_but_valid_first = ["node@lts".to_owned(), "12".to_owned()];
+ assert_eq!(
+ Spec::from_strings(&two_but_valid_first, PIN)
+ .expect("is ok")
+ .len(),
+ two_but_valid_first.len(),
+ "when there are two args but the first is a valid tool spec"
+ );
+
+ let more_than_two_tools = ["node".to_owned(), "12".to_owned(), "yarn".to_owned()];
+ assert_eq!(
+ Spec::from_strings(&more_than_two_tools, PIN)
+ .expect("is ok")
+ .len(),
+ more_than_two_tools.len(),
+ "when there are more than two args"
+ );
+ }
+
+ #[test]
+ fn sorts_node_npm_yarn_to_front() {
+ let multiple = [
+ "ember-cli@3".to_owned(),
+ "yarn".to_owned(),
+ "npm@5".to_owned(),
+ "node@latest".to_owned(),
+ ];
+ let expected = [
+ Spec::Node(VersionSpec::Tag(VersionTag::Latest)),
+ Spec::Npm(VersionSpec::from_str("5").expect("requirement is valid")),
+ Spec::Yarn(VersionSpec::default()),
+ Spec::Package(
+ "ember-cli".to_owned(),
+ VersionSpec::from_str("3").expect("requirement is valid"),
+ ),
+ ];
+ assert_eq!(Spec::from_strings(&multiple, PIN).expect("is ok"), expected);
+ }
+
+ #[test]
+ fn keeps_package_order_unchanged() {
+ let packages_with_node = ["typescript@latest", "ember-cli@3", "node@lts", "mocha"];
+ let expected = [
+ Spec::Node(VersionSpec::Tag(VersionTag::Lts)),
+ Spec::Package(
+ "typescript".to_owned(),
+ VersionSpec::Tag(VersionTag::Latest),
+ ),
+ Spec::Package(
+ "ember-cli".to_owned(),
+ VersionSpec::from_str("3").expect("requirement is valid"),
+ ),
+ Spec::Package("mocha".to_owned(), VersionSpec::default()),
+ ];
+
+ assert_eq!(
+ Spec::from_strings(&packages_with_node, PIN).expect("is ok"),
+ expected
+ );
+ }
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +
//! Provides fetcher for Yarn distributions
+
+use std::fs::File;
+use std::path::Path;
+
+use super::super::download_tool_error;
+use super::super::registry::{
+ find_unpack_dir, public_registry_package, scoped_public_registry_package,
+};
+use crate::error::{Context, ErrorKind, Fallible};
+use crate::fs::{create_staging_dir, create_staging_file, rename, set_executable};
+use crate::hook::YarnHooks;
+use crate::layout::volta_home;
+use crate::style::{progress_bar, tool_version};
+use crate::tool::{self, Yarn};
+use crate::version::VersionSpec;
+use archive::{Archive, Tarball};
+use fs_utils::ensure_containing_dir_exists;
+use log::debug;
+use node_semver::Version;
+
+pub fn fetch(version: &Version, hooks: Option<&YarnHooks>) -> Fallible<()> {
+ let yarn_dir = volta_home()?.yarn_inventory_dir();
+ let cache_file = yarn_dir.join(Yarn::archive_filename(&version.to_string()));
+
+ let (archive, staging) = match load_cached_distro(&cache_file) {
+ Some(archive) => {
+ debug!(
+ "Loading {} from cached archive at '{}'",
+ tool_version("yarn", version),
+ cache_file.display(),
+ );
+ (archive, None)
+ }
+ None => {
+ let staging = create_staging_file()?;
+ let remote_url = determine_remote_url(version, hooks)?;
+ let archive = fetch_remote_distro(version, &remote_url, staging.path())?;
+ (archive, Some(staging))
+ }
+ };
+
+ unpack_archive(archive, version)?;
+
+ if let Some(staging_file) = staging {
+ ensure_containing_dir_exists(&cache_file).with_context(|| {
+ ErrorKind::ContainingDirError {
+ path: cache_file.clone(),
+ }
+ })?;
+ staging_file
+ .persist(cache_file)
+ .with_context(|| ErrorKind::PersistInventoryError {
+ tool: "Yarn".into(),
+ })?;
+ }
+
+ Ok(())
+}
+
+/// Unpack the yarn archive into the image directory so that it is ready for use
+fn unpack_archive(archive: Box<dyn Archive>, version: &Version) -> Fallible<()> {
+ let temp = create_staging_dir()?;
+ debug!("Unpacking yarn into '{}'", temp.path().display());
+
+ let progress = progress_bar(
+ archive.origin(),
+ &tool_version("yarn", version),
+ archive.compressed_size(),
+ );
+ let version_string = version.to_string();
+
+ archive
+ .unpack(temp.path(), &mut |_, read| {
+ progress.inc(read as u64);
+ })
+ .with_context(|| ErrorKind::UnpackArchiveError {
+ tool: "Yarn".into(),
+ version: version_string.clone(),
+ })?;
+
+ let unpack_dir = find_unpack_dir(temp.path())?;
+ // "bin/yarn" is not executable in the @yarnpkg/cli-dist package
+ ensure_bin_is_executable(&unpack_dir, "yarn")?;
+
+ let dest = volta_home()?.yarn_image_dir(&version_string);
+ ensure_containing_dir_exists(&dest)
+ .with_context(|| ErrorKind::ContainingDirError { path: dest.clone() })?;
+
+ rename(unpack_dir, &dest).with_context(|| ErrorKind::SetupToolImageError {
+ tool: "Yarn".into(),
+ version: version_string.clone(),
+ dir: dest.clone(),
+ })?;
+
+ progress.finish_and_clear();
+
+ // Note: We write this after the progress bar is finished to avoid display bugs with re-renders of the progress
+ debug!("Installing yarn in '{}'", dest.display());
+
+ Ok(())
+}
+
+/// Return the archive if it is valid. It may have been corrupted or interrupted in the middle of
+/// downloading.
+// ISSUE(#134) - verify checksum
+fn load_cached_distro(file: &Path) -> Option<Box<dyn Archive>> {
+ if file.is_file() {
+ let file = File::open(file).ok()?;
+ Tarball::load(file).ok()
+ } else {
+ None
+ }
+}
+
+/// Determine the remote URL to download from, using the hooks if available
+fn determine_remote_url(version: &Version, hooks: Option<&YarnHooks>) -> Fallible<String> {
+ let version_str = version.to_string();
+ match hooks {
+ Some(&YarnHooks {
+ distro: Some(ref hook),
+ ..
+ }) => {
+ debug!("Using yarn.distro hook to determine download URL");
+ let distro_file_name = Yarn::archive_filename(&version_str);
+ hook.resolve(version, &distro_file_name)
+ }
+ _ => {
+ if version.major >= 2 {
+ Ok(scoped_public_registry_package(
+ "@yarnpkg",
+ "cli-dist",
+ &version_str,
+ ))
+ } else {
+ Ok(public_registry_package("yarn", &version_str))
+ }
+ }
+ }
+}
+
+/// Fetch the distro archive from the internet
+fn fetch_remote_distro(
+ version: &Version,
+ url: &str,
+ staging_path: &Path,
+) -> Fallible<Box<dyn Archive>> {
+ debug!("Downloading {} from {}", tool_version("yarn", version), url);
+ Tarball::fetch(url, staging_path).with_context(download_tool_error(
+ tool::Spec::Yarn(VersionSpec::Exact(version.clone())),
+ url,
+ ))
+}
+
+fn ensure_bin_is_executable(unpack_dir: &Path, tool: &str) -> Fallible<()> {
+ let exec_path = unpack_dir.join("bin").join(tool);
+ set_executable(&exec_path).with_context(|| ErrorKind::SetToolExecutable { tool: tool.into() })
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +
use std::collections::BTreeSet;
+
+use crate::version::version_serde;
+use node_semver::Version;
+use serde::Deserialize;
+
+/// The public Yarn index.
+pub struct YarnIndex {
+ pub(super) entries: BTreeSet<Version>,
+}
+
+#[derive(Deserialize)]
+pub struct RawYarnIndex(Vec<RawYarnEntry>);
+
+#[derive(Deserialize)]
+pub struct RawYarnEntry {
+ /// Yarn releases are given a tag name of the form "v$version" where $version
+ /// is the release's version string.
+ #[serde(with = "version_serde")]
+ pub tag_name: Version,
+
+ /// The GitHub API provides a list of assets. Some Yarn releases don't include
+ /// a tarball, so we don't support them and remove them from the set of available
+ /// Yarn versions.
+ pub assets: Vec<RawYarnAsset>,
+}
+
+impl RawYarnEntry {
+ /// Is this entry a full release, i.e., does this entry's asset list include a
+ /// proper release tarball?
+ fn is_full_release(&self) -> bool {
+ let release_filename = &format!("yarn-v{}.tar.gz", self.tag_name)[..];
+ self.assets
+ .iter()
+ .any(|raw_asset| raw_asset.name == release_filename)
+ }
+}
+
+#[derive(Deserialize)]
+pub struct RawYarnAsset {
+ /// The filename of an asset included in a Yarn GitHub release.
+ pub name: String,
+}
+
+impl From<RawYarnIndex> for YarnIndex {
+ fn from(raw: RawYarnIndex) -> YarnIndex {
+ let mut entries = BTreeSet::new();
+ for entry in raw.0 {
+ if entry.is_full_release() {
+ entries.insert(entry.tag_name);
+ }
+ }
+ YarnIndex { entries }
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +
use std::fmt::{self, Display};
+
+use super::{
+ check_fetched, check_shim_reachable, debug_already_fetched, info_fetched, info_installed,
+ info_pinned, info_project_version, FetchStatus, Tool,
+};
+use crate::error::{ErrorKind, Fallible};
+use crate::inventory::yarn_available;
+use crate::session::Session;
+use crate::style::tool_version;
+use crate::sync::VoltaLock;
+use node_semver::Version;
+
+mod fetch;
+mod metadata;
+mod resolve;
+
+pub use resolve::resolve;
+
+/// The Tool implementation for fetching and installing Yarn
+pub struct Yarn {
+ pub(super) version: Version,
+}
+
+impl Yarn {
+ pub fn new(version: Version) -> Self {
+ Yarn { version }
+ }
+
+ pub fn archive_basename(version: &str) -> String {
+ format!("yarn-v{}", version)
+ }
+
+ pub fn archive_filename(version: &str) -> String {
+ format!("{}.tar.gz", Yarn::archive_basename(version))
+ }
+
+ pub(crate) fn ensure_fetched(&self, session: &mut Session) -> Fallible<()> {
+ match check_fetched(|| yarn_available(&self.version))? {
+ FetchStatus::AlreadyFetched => {
+ debug_already_fetched(self);
+ Ok(())
+ }
+ FetchStatus::FetchNeeded(_lock) => fetch::fetch(&self.version, session.hooks()?.yarn()),
+ }
+ }
+}
+
+impl Tool for Yarn {
+ fn fetch(self: Box<Self>, session: &mut Session) -> Fallible<()> {
+ self.ensure_fetched(session)?;
+
+ info_fetched(self);
+ Ok(())
+ }
+ fn install(self: Box<Self>, session: &mut Session) -> Fallible<()> {
+ // Acquire a lock on the Volta directory, if possible, to prevent concurrent changes
+ let _lock = VoltaLock::acquire();
+ self.ensure_fetched(session)?;
+
+ session
+ .toolchain_mut()?
+ .set_active_yarn(Some(self.version.clone()))?;
+
+ info_installed(&self);
+ check_shim_reachable("yarn");
+
+ if let Ok(Some(project)) = session.project_platform() {
+ if let Some(yarn) = &project.yarn {
+ info_project_version(tool_version("yarn", yarn), &self);
+ }
+ }
+ Ok(())
+ }
+ fn pin(self: Box<Self>, session: &mut Session) -> Fallible<()> {
+ if session.project()?.is_some() {
+ self.ensure_fetched(session)?;
+
+ // Note: We know this will succeed, since we checked above
+ let project = session.project_mut()?.unwrap();
+ project.pin_yarn(Some(self.version.clone()))?;
+
+ info_pinned(self);
+ Ok(())
+ } else {
+ Err(ErrorKind::NotInPackage.into())
+ }
+ }
+}
+
+impl Display for Yarn {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.write_str(&tool_version("yarn", &self.version))
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_yarn_archive_basename() {
+ assert_eq!(Yarn::archive_basename("1.2.3"), "yarn-v1.2.3");
+ }
+
+ #[test]
+ fn test_yarn_archive_filename() {
+ assert_eq!(Yarn::archive_filename("1.2.3"), "yarn-v1.2.3.tar.gz");
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +
//! Provides resolution of Yarn requirements into specific versions
+
+use super::super::registry::{
+ fetch_npm_registry, public_registry_index, PackageDetails, PackageIndex,
+};
+use super::super::registry_fetch_error;
+use super::metadata::{RawYarnIndex, YarnIndex};
+use crate::error::{Context, ErrorKind, Fallible};
+use crate::hook::{RegistryFormat, YarnHooks};
+use crate::session::Session;
+use crate::style::progress_spinner;
+use crate::version::{parse_version, VersionSpec, VersionTag};
+use attohttpc::Response;
+use log::debug;
+use node_semver::{Range, Version};
+
+pub fn resolve(matching: VersionSpec, session: &mut Session) -> Fallible<Version> {
+ let hooks = session.hooks()?.yarn();
+ match matching {
+ VersionSpec::Semver(requirement) => resolve_semver(requirement, hooks),
+ VersionSpec::Exact(version) => Ok(version),
+ VersionSpec::None => resolve_tag(VersionTag::Latest, hooks),
+ VersionSpec::Tag(tag) => resolve_tag(tag, hooks),
+ }
+}
+
+fn resolve_tag(tag: VersionTag, hooks: Option<&YarnHooks>) -> Fallible<Version> {
+ // This triage is complicated because we need to maintain the legacy behavior of hooks
+ // First, if the tag is 'latest' and we have a 'latest' hook, we use the old behavior
+ // Next, if the tag is 'latest' and we _do not_ have a 'latest' hook, we use the new behavior
+ // Next, if the tag is _not_ 'latest' and we have an 'index' hook, we show an error since
+ // the previous behavior did not support generic tags
+ // Finally, we don't have any relevant hooks, so we can use the new behavior
+ match (tag, hooks) {
+ (
+ VersionTag::Latest,
+ Some(&YarnHooks {
+ latest: Some(ref hook),
+ ..
+ }),
+ ) => {
+ debug!("Using yarn.latest hook to determine latest-version URL");
+ // does yarn3 use latest-version? no
+ resolve_latest_legacy(hook.resolve("latest-version")?)
+ }
+ (VersionTag::Latest, _) => resolve_custom_tag(VersionTag::Latest.to_string()),
+ (tag, Some(&YarnHooks { index: Some(_), .. })) => Err(ErrorKind::YarnVersionNotFound {
+ matching: tag.to_string(),
+ }
+ .into()),
+ (tag, _) => resolve_custom_tag(tag.to_string()),
+ }
+}
+
+fn resolve_semver(matching: Range, hooks: Option<&YarnHooks>) -> Fallible<Version> {
+ // For semver, the triage is less complicated: The previous behavior _always_ used
+ // the 'index' hook, so we can check for that to decide which behavior to use.
+ //
+ // If the user specifies a format for the registry, we use that. Otherwise Github format
+ // is the default legacy behavior.
+ if let Some(&YarnHooks {
+ index: Some(ref hook),
+ ..
+ }) = hooks
+ {
+ debug!("Using yarn.index hook to determine yarn index URL");
+ match hook.format {
+ RegistryFormat::Github => resolve_semver_legacy(matching, hook.resolve("releases")?),
+ RegistryFormat::Npm => resolve_semver_npm(matching, hook.resolve("")?),
+ }
+ } else {
+ resolve_semver_from_registry(matching)
+ }
+}
+
+fn fetch_yarn_index(package: &str) -> Fallible<(String, PackageIndex)> {
+ let url = public_registry_index(package);
+ fetch_npm_registry(url, "Yarn")
+}
+
+fn resolve_custom_tag(tag: String) -> Fallible<Version> {
+ // first try yarn2+, which uses "@yarnpkg/cli-dist" instead of "yarn"
+ if let Ok((url, mut index)) = fetch_yarn_index("@yarnpkg/cli-dist") {
+ if let Some(version) = index.tags.remove(&tag) {
+ debug!("Found yarn@{} matching tag '{}' from {}", version, tag, url);
+ if version.major == 2 {
+ return Err(ErrorKind::Yarn2NotSupported.into());
+ }
+ return Ok(version);
+ }
+ }
+ debug!(
+ "Did not find yarn matching tag '{}' from @yarnpkg/cli-dist",
+ tag
+ );
+
+ let (url, mut index) = fetch_yarn_index("yarn")?;
+ match index.tags.remove(&tag) {
+ Some(version) => {
+ debug!("Found yarn@{} matching tag '{}' from {}", version, tag, url);
+ Ok(version)
+ }
+ None => Err(ErrorKind::YarnVersionNotFound { matching: tag }.into()),
+ }
+}
+
+fn resolve_latest_legacy(url: String) -> Fallible<Version> {
+ let response_text = attohttpc::get(&url)
+ .send()
+ .and_then(Response::error_for_status)
+ .and_then(Response::text)
+ .with_context(|| ErrorKind::YarnLatestFetchError {
+ from_url: url.clone(),
+ })?;
+
+ debug!("Found yarn latest version ({}) from {}", response_text, url);
+ parse_version(response_text)
+}
+
+fn resolve_semver_from_registry(matching: Range) -> Fallible<Version> {
+ // first try yarn2+, which uses "@yarnpkg/cli-dist" instead of "yarn"
+ if let Ok((url, index)) = fetch_yarn_index("@yarnpkg/cli-dist") {
+ let matching_entries: Vec<PackageDetails> = index
+ .entries
+ .into_iter()
+ .filter(|PackageDetails { version, .. }| matching.satisfies(version))
+ .collect();
+
+ if !matching_entries.is_empty() {
+ let details_opt = matching_entries
+ .iter()
+ .find(|PackageDetails { version, .. }| version.major >= 3);
+
+ match details_opt {
+ Some(details) => {
+ debug!(
+ "Found yarn@{} matching requirement '{}' from {}",
+ details.version, matching, url
+ );
+ return Ok(details.version.clone());
+ }
+ None => {
+ return Err(ErrorKind::Yarn2NotSupported.into());
+ }
+ }
+ }
+ }
+ debug!(
+ "Did not find yarn matching requirement '{}' for @yarnpkg/cli-dist",
+ matching
+ );
+
+ let (url, index) = fetch_yarn_index("yarn")?;
+
+ let details_opt = index
+ .entries
+ .into_iter()
+ .find(|PackageDetails { version, .. }| matching.satisfies(version));
+
+ match details_opt {
+ Some(details) => {
+ debug!(
+ "Found yarn@{} matching requirement '{}' from {}",
+ details.version, matching, url
+ );
+ Ok(details.version)
+ }
+ // at this point Yarn is not found in either registry
+ None => Err(ErrorKind::YarnVersionNotFound {
+ matching: matching.to_string(),
+ }
+ .into()),
+ }
+}
+
+fn resolve_semver_legacy(matching: Range, url: String) -> Fallible<Version> {
+ let spinner = progress_spinner(format!("Fetching registry: {}", url));
+ let releases: RawYarnIndex = attohttpc::get(&url)
+ .send()
+ .and_then(Response::error_for_status)
+ .and_then(Response::json)
+ .with_context(registry_fetch_error("Yarn", &url))?;
+ let index = YarnIndex::from(releases);
+ let releases = index.entries;
+ spinner.finish_and_clear();
+ let version_opt = releases.into_iter().rev().find(|v| matching.satisfies(v));
+
+ match version_opt {
+ Some(version) => {
+ debug!(
+ "Found yarn@{} matching requirement '{}' from {}",
+ version, matching, url
+ );
+ Ok(version)
+ }
+ None => Err(ErrorKind::YarnVersionNotFound {
+ matching: matching.to_string(),
+ }
+ .into()),
+ }
+}
+
+fn resolve_semver_npm(matching: Range, url: String) -> Fallible<Version> {
+ let (url, index) = fetch_npm_registry(url, "Yarn")?;
+
+ let details_opt = index
+ .entries
+ .into_iter()
+ .find(|PackageDetails { version, .. }| matching.satisfies(version));
+
+ match details_opt {
+ Some(details) => {
+ debug!(
+ "Found yarn@{} matching requirement '{}' from {}",
+ details.version, matching, url
+ );
+ Ok(details.version)
+ }
+ None => Err(ErrorKind::YarnVersionNotFound {
+ matching: matching.to_string(),
+ }
+ .into()),
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +
use std::fs::write;
+
+use crate::error::{Context, ErrorKind, Fallible};
+use crate::fs::touch;
+use crate::layout::volta_home;
+use crate::platform::PlatformSpec;
+use log::debug;
+use node_semver::Version;
+use once_cell::unsync::OnceCell;
+use readext::ReadExt;
+
+pub mod serial;
+
+/// Lazily loaded toolchain
+pub struct LazyToolchain {
+ toolchain: OnceCell<Toolchain>,
+}
+
+impl LazyToolchain {
+ /// Creates a new `LazyToolchain`
+ pub fn init() -> Self {
+ LazyToolchain {
+ toolchain: OnceCell::new(),
+ }
+ }
+
+ /// Forces loading of the toolchain and returns an immutable reference to it
+ pub fn get(&self) -> Fallible<&Toolchain> {
+ self.toolchain.get_or_try_init(Toolchain::current)
+ }
+
+ /// Forces loading of the toolchain and returns a mutable reference to it
+ pub fn get_mut(&mut self) -> Fallible<&mut Toolchain> {
+ let _ = self.toolchain.get_or_try_init(Toolchain::current)?;
+ Ok(self.toolchain.get_mut().unwrap())
+ }
+}
+
+pub struct Toolchain {
+ platform: Option<PlatformSpec>,
+}
+
+impl Toolchain {
+ fn current() -> Fallible<Toolchain> {
+ let path = volta_home()?.default_platform_file();
+ let src = touch(path)
+ .and_then(|mut file| file.read_into_string())
+ .with_context(|| ErrorKind::ReadPlatformError {
+ file: path.to_owned(),
+ })?;
+
+ let platform: Option<PlatformSpec> = serial::Platform::try_from(src)?.into();
+ if platform.is_some() {
+ debug!("Found default configuration at '{}'", path.display());
+ }
+ Ok(Toolchain { platform })
+ }
+
+ pub fn platform(&self) -> Option<&PlatformSpec> {
+ self.platform.as_ref()
+ }
+
+ /// Set the active Node version in the default platform file.
+ pub fn set_active_node(&mut self, node_version: &Version) -> Fallible<()> {
+ let mut dirty = false;
+
+ match self.platform.as_mut() {
+ Some(platform) => {
+ if platform.node != *node_version {
+ platform.node = node_version.clone();
+ dirty = true;
+ }
+ }
+ None => {
+ self.platform = Some(PlatformSpec {
+ node: node_version.clone(),
+ npm: None,
+ pnpm: None,
+ yarn: None,
+ });
+ dirty = true;
+ }
+ }
+
+ if dirty {
+ self.save()?;
+ }
+
+ Ok(())
+ }
+
+ /// Set the active Yarn version in the default platform file.
+ pub fn set_active_yarn(&mut self, yarn: Option<Version>) -> Fallible<()> {
+ if let Some(platform) = self.platform.as_mut() {
+ if platform.yarn != yarn {
+ platform.yarn = yarn;
+ self.save()?;
+ }
+ } else if yarn.is_some() {
+ return Err(ErrorKind::NoDefaultNodeVersion {
+ tool: "Yarn".into(),
+ }
+ .into());
+ }
+
+ Ok(())
+ }
+
+ /// Set the active pnpm version in the default platform file.
+ pub fn set_active_pnpm(&mut self, pnpm: Option<Version>) -> Fallible<()> {
+ if let Some(platform) = self.platform.as_mut() {
+ if platform.pnpm != pnpm {
+ platform.pnpm = pnpm;
+ self.save()?;
+ }
+ } else if pnpm.is_some() {
+ return Err(ErrorKind::NoDefaultNodeVersion {
+ tool: "pnpm".into(),
+ }
+ .into());
+ }
+
+ Ok(())
+ }
+
+ /// Set the active Npm version in the default platform file.
+ pub fn set_active_npm(&mut self, npm: Option<Version>) -> Fallible<()> {
+ if let Some(platform) = self.platform.as_mut() {
+ if platform.npm != npm {
+ platform.npm = npm;
+ self.save()?;
+ }
+ } else if npm.is_some() {
+ return Err(ErrorKind::NoDefaultNodeVersion { tool: "npm".into() }.into());
+ }
+
+ Ok(())
+ }
+
+ pub fn save(&self) -> Fallible<()> {
+ let path = volta_home()?.default_platform_file();
+ let result = match &self.platform {
+ Some(platform) => {
+ let src = serial::Platform::of(platform).into_json()?;
+ write(path, src)
+ }
+ None => write(path, "{}"),
+ };
+ result.with_context(|| ErrorKind::WritePlatformError {
+ file: path.to_owned(),
+ })
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +
use crate::error::{Context, ErrorKind, Fallible, VoltaError};
+use crate::platform::PlatformSpec;
+use crate::version::{option_version_serde, version_serde};
+use node_semver::Version;
+use serde::{Deserialize, Serialize};
+
+#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
+pub struct NodeVersion {
+ #[serde(with = "version_serde")]
+ pub runtime: Version,
+ #[serde(with = "option_version_serde")]
+ pub npm: Option<Version>,
+}
+
+#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
+pub struct Platform {
+ #[serde(default)]
+ pub node: Option<NodeVersion>,
+ #[serde(default)]
+ #[serde(with = "option_version_serde")]
+ pub pnpm: Option<Version>,
+ #[serde(default)]
+ #[serde(with = "option_version_serde")]
+ pub yarn: Option<Version>,
+}
+
+impl Platform {
+ pub fn of(source: &PlatformSpec) -> Self {
+ Platform {
+ node: Some(NodeVersion {
+ runtime: source.node.clone(),
+ npm: source.npm.clone(),
+ }),
+ pnpm: source.pnpm.clone(),
+ yarn: source.yarn.clone(),
+ }
+ }
+
+ /// Serialize the Platform to a JSON String
+ pub fn into_json(self) -> Fallible<String> {
+ serde_json::to_string_pretty(&self).with_context(|| ErrorKind::StringifyPlatformError)
+ }
+}
+
+impl TryFrom<String> for Platform {
+ type Error = VoltaError;
+ fn try_from(src: String) -> Fallible<Self> {
+ let result = if src.is_empty() {
+ serde_json::de::from_str("{}")
+ } else {
+ serde_json::de::from_str(&src)
+ };
+
+ result.with_context(|| ErrorKind::ParsePlatformError)
+ }
+}
+
+impl From<Platform> for Option<PlatformSpec> {
+ fn from(platform: Platform) -> Option<PlatformSpec> {
+ let yarn = platform.yarn;
+ let pnpm = platform.pnpm;
+ platform.node.map(|node_version| PlatformSpec {
+ node: node_version.runtime,
+ npm: node_version.npm,
+ pnpm,
+ yarn,
+ })
+ }
+}
+
+#[cfg(test)]
+pub mod tests {
+
+ use super::*;
+ use crate::platform;
+ use node_semver::Version;
+
+ // NOTE: serde_json is required with the "preserve_order" feature in Cargo.toml,
+ // so these tests will serialized/deserialize in a predictable order
+
+ const BASIC_JSON_STR: &str = r#"{
+ "node": {
+ "runtime": "4.5.6",
+ "npm": "7.8.9"
+ },
+ "pnpm": "3.2.1",
+ "yarn": "1.2.3"
+}"#;
+
+ #[test]
+ fn test_from_json() {
+ let json_str = BASIC_JSON_STR.to_string();
+ let platform = Platform::try_from(json_str).expect("could not parse JSON string");
+ let expected_platform = Platform {
+ pnpm: Some(Version::parse("3.2.1").expect("could not parse version")),
+ yarn: Some(Version::parse("1.2.3").expect("could not parse version")),
+ node: Some(NodeVersion {
+ runtime: Version::parse("4.5.6").expect("could not parse version"),
+ npm: Some(Version::parse("7.8.9").expect("could not parse version")),
+ }),
+ };
+ assert_eq!(platform, expected_platform);
+ }
+
+ #[test]
+ fn test_from_json_empty_string() {
+ let json_str = "".to_string();
+ let platform = Platform::try_from(json_str).expect("could not parse JSON string");
+ let expected_platform = Platform {
+ node: None,
+ pnpm: None,
+ yarn: None,
+ };
+ assert_eq!(platform, expected_platform);
+ }
+
+ #[test]
+ fn test_into_json() {
+ let platform_spec = platform::PlatformSpec {
+ pnpm: Some(Version::parse("3.2.1").expect("could not parse version")),
+ yarn: Some(Version::parse("1.2.3").expect("could not parse version")),
+ node: Version::parse("4.5.6").expect("could not parse version"),
+ npm: Some(Version::parse("7.8.9").expect("could not parse version")),
+ };
+ let json_str = Platform::of(&platform_spec)
+ .into_json()
+ .expect("could not serialize platform to JSON");
+ let expected_json_str = BASIC_JSON_STR.to_string();
+ assert_eq!(json_str, expected_json_str);
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +
use std::fmt;
+use std::str::FromStr;
+
+use crate::error::{Context, ErrorKind, Fallible, VoltaError};
+use node_semver::{Range, Version};
+
+mod serial;
+
+#[derive(Debug, Default)]
+#[cfg_attr(test, derive(PartialEq, Eq))]
+pub enum VersionSpec {
+ /// No version specified (default)
+ #[default]
+ None,
+
+ /// SemVer Range
+ Semver(Range),
+
+ /// Exact Version
+ Exact(Version),
+
+ /// Arbitrary Version Tag
+ Tag(VersionTag),
+}
+
+#[derive(Debug)]
+#[cfg_attr(test, derive(PartialEq, Eq))]
+pub enum VersionTag {
+ /// The 'latest' tag, a special case that exists for all packages
+ Latest,
+
+ /// The 'lts' tag, a special case for Node
+ Lts,
+
+ /// An arbitrary tag version
+ Custom(String),
+}
+
+impl fmt::Display for VersionSpec {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ VersionSpec::None => write!(f, "<default>"),
+ VersionSpec::Semver(req) => req.fmt(f),
+ VersionSpec::Exact(version) => version.fmt(f),
+ VersionSpec::Tag(tag) => tag.fmt(f),
+ }
+ }
+}
+
+impl fmt::Display for VersionTag {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ VersionTag::Latest => write!(f, "latest"),
+ VersionTag::Lts => write!(f, "lts"),
+ VersionTag::Custom(s) => s.fmt(f),
+ }
+ }
+}
+
+impl FromStr for VersionSpec {
+ type Err = VoltaError;
+
+ fn from_str(s: &str) -> Fallible<Self> {
+ if let Ok(version) = parse_version(s) {
+ Ok(VersionSpec::Exact(version))
+ } else if let Ok(req) = parse_requirements(s) {
+ Ok(VersionSpec::Semver(req))
+ } else {
+ s.parse().map(VersionSpec::Tag)
+ }
+ }
+}
+
+impl FromStr for VersionTag {
+ type Err = VoltaError;
+
+ fn from_str(s: &str) -> Fallible<Self> {
+ if s == "latest" {
+ Ok(VersionTag::Latest)
+ } else if s == "lts" {
+ Ok(VersionTag::Lts)
+ } else {
+ Ok(VersionTag::Custom(s.into()))
+ }
+ }
+}
+
+pub fn parse_requirements(s: impl AsRef<str>) -> Fallible<Range> {
+ let s = s.as_ref();
+ serial::parse_requirements(s)
+ .with_context(|| ErrorKind::VersionParseError { version: s.into() })
+}
+
+pub fn parse_version(s: impl AsRef<str>) -> Fallible<Version> {
+ let s = s.as_ref();
+ s.parse()
+ .with_context(|| ErrorKind::VersionParseError { version: s.into() })
+}
+
+// remove the leading 'v' from the version string, if present
+fn trim_version(s: &str) -> &str {
+ let s = s.trim();
+ match s.strip_prefix('v') {
+ Some(stripped) => stripped,
+ None => s,
+ }
+}
+
+// custom serialization and de-serialization for Version
+// because Version doesn't work with serde out of the box
+pub mod version_serde {
+ use node_semver::Version;
+ use serde::de::{Error, Visitor};
+ use serde::{self, Deserializer, Serializer};
+ use std::fmt;
+
+ struct VersionVisitor;
+
+ impl<'de> Visitor<'de> for VersionVisitor {
+ type Value = Version;
+
+ fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter.write_str("string")
+ }
+
+ // parse the version from the string
+ fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
+ where
+ E: Error,
+ {
+ Version::parse(super::trim_version(value)).map_err(Error::custom)
+ }
+ }
+
+ pub fn serialize<S>(version: &Version, s: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ s.serialize_str(&version.to_string())
+ }
+
+ pub fn deserialize<'de, D>(deserializer: D) -> Result<Version, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ deserializer.deserialize_string(VersionVisitor)
+ }
+}
+
+// custom serialization and de-serialization for Option<Version>
+// because Version doesn't work with serde out of the box
+pub mod option_version_serde {
+ use node_semver::Version;
+ use serde::de::Error;
+ use serde::{self, Deserialize, Deserializer, Serializer};
+
+ pub fn serialize<S>(version: &Option<Version>, s: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ match version {
+ Some(v) => s.serialize_str(&v.to_string()),
+ None => s.serialize_none(),
+ }
+ }
+
+ pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<Version>, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ let s: Option<String> = Option::deserialize(deserializer)?;
+ if let Some(v) = s {
+ return Ok(Some(
+ Version::parse(super::trim_version(&v)).map_err(Error::custom)?,
+ ));
+ }
+ Ok(None)
+ }
+}
+
+// custom deserialization for HashMap<String, Version>
+// because Version doesn't work with serde out of the box
+pub mod hashmap_version_serde {
+ use super::version_serde;
+ use node_semver::Version;
+ use serde::{self, Deserialize, Deserializer};
+ use std::collections::HashMap;
+
+ #[derive(Deserialize)]
+ struct Wrapper(#[serde(deserialize_with = "version_serde::deserialize")] Version);
+
+ pub fn deserialize<'de, D>(deserializer: D) -> Result<HashMap<String, Version>, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ let m = HashMap::<String, Wrapper>::deserialize(deserializer)?;
+ Ok(m.into_iter().map(|(k, Wrapper(v))| (k, v)).collect())
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +
use node_semver::{Range, SemverError};
+
+// NOTE: using `parse_compat` here because the semver crate defaults to
+// parsing in a cargo-compatible way. This is normally fine, except for
+// 2 cases (that I know about):
+// * "1.2.3" parses as `^1.2.3` for cargo, but `=1.2.3` for Node
+// * `>1.2.3 <2.0.0` serializes to ">1.2.3, <2.0.0" for cargo (with the
+// comma), but ">1.2.3 <2.0.0" for Node (no comma, because Node parses
+// commas differently)
+//
+// Because we are parsing the version requirements from the command line,
+// then serializing them to pass to `npm view`, they need to be handled in
+// a Node-compatible way (or we get the wrong version info returned).
+pub fn parse_requirements(src: &str) -> Result<Range, SemverError> {
+ let src = src.trim().trim_start_matches('v');
+
+ Range::parse(src)
+}
+
+#[cfg(test)]
+pub mod tests {
+
+ use crate::version::serial::parse_requirements;
+ use node_semver::Range;
+
+ #[test]
+ fn test_parse_requirements() {
+ assert_eq!(
+ parse_requirements("1.2.3").unwrap(),
+ Range::parse("=1.2.3").unwrap()
+ );
+ assert_eq!(
+ parse_requirements("v1.5").unwrap(),
+ Range::parse("=1.5").unwrap()
+ );
+ assert_eq!(
+ parse_requirements("=1.2.3").unwrap(),
+ Range::parse("=1.2.3").unwrap()
+ );
+ assert_eq!(
+ parse_requirements("^1.2").unwrap(),
+ Range::parse("^1.2").unwrap()
+ );
+ assert_eq!(
+ parse_requirements(">=1.4").unwrap(),
+ Range::parse(">=1.4").unwrap()
+ );
+ assert_eq!(
+ parse_requirements("8.11 - 8.17 || 10.* || >= 12").unwrap(),
+ Range::parse("8.11 - 8.17 || 10.* || >= 12").unwrap()
+ );
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +
use std::path::PathBuf;
+
+use super::executable;
+use volta_layout_macro::layout;
+
+layout! {
+ pub struct VoltaInstall {
+ "shim[.exe]": shim_executable;
+ }
+
+ pub struct VoltaHome {
+ "cache": cache_dir {
+ "node": node_cache_dir {
+ "index.json": node_index_file;
+ "index.json.expires": node_index_expiry_file;
+ }
+ }
+ "bin": shim_dir {}
+ "log": log_dir {}
+ "tools": tools_dir {
+ "inventory": inventory_dir {
+ "node": node_inventory_dir {}
+ "packages": package_inventory_dir {}
+ "yarn": yarn_inventory_dir {}
+ }
+ "image": image_dir {
+ "node": node_image_root_dir {}
+ "yarn": yarn_image_root_dir {}
+ "packages": package_image_root_dir {}
+ }
+ "user": default_toolchain_dir {
+ "bins": default_bin_dir {}
+ "packages": default_package_dir {}
+ "platform.json": default_platform_file;
+ }
+ }
+ "tmp": tmp_dir {}
+ "hooks.json": default_hooks_file;
+ }
+}
+
+impl VoltaHome {
+ pub fn package_distro_file(&self, name: &str, version: &str) -> PathBuf {
+ path_buf!(
+ self.package_inventory_dir.clone(),
+ format!("{}-{}.tgz", name, version)
+ )
+ }
+
+ pub fn package_distro_shasum(&self, name: &str, version: &str) -> PathBuf {
+ path_buf!(
+ self.package_inventory_dir.clone(),
+ format!("{}-{}.shasum", name, version)
+ )
+ }
+
+ pub fn node_image_dir(&self, node: &str, npm: &str) -> PathBuf {
+ path_buf!(self.node_image_root_dir.clone(), node, npm)
+ }
+
+ pub fn yarn_image_dir(&self, version: &str) -> PathBuf {
+ path_buf!(self.yarn_image_root_dir.clone(), version)
+ }
+
+ pub fn yarn_image_bin_dir(&self, version: &str) -> PathBuf {
+ path_buf!(self.yarn_image_dir(version), "bin")
+ }
+
+ pub fn package_image_dir(&self, name: &str, version: &str) -> PathBuf {
+ path_buf!(self.package_image_root_dir.clone(), name, version)
+ }
+
+ pub fn default_package_config_file(&self, package_name: &str) -> PathBuf {
+ path_buf!(
+ self.default_package_dir.clone(),
+ format!("{}.json", package_name)
+ )
+ }
+
+ pub fn default_tool_bin_config(&self, bin_name: &str) -> PathBuf {
+ path_buf!(self.default_bin_dir.clone(), format!("{}.json", bin_name))
+ }
+
+ pub fn node_npm_version_file(&self, version: &str) -> PathBuf {
+ path_buf!(
+ self.node_inventory_dir.clone(),
+ format!("node-v{}-npm", version)
+ )
+ }
+
+ pub fn shim_file(&self, toolname: &str) -> PathBuf {
+ path_buf!(self.shim_dir.clone(), executable(toolname))
+ }
+}
+
+#[cfg(windows)]
+impl VoltaHome {
+ pub fn shim_git_bash_script_file(&self, toolname: &str) -> PathBuf {
+ path_buf!(self.shim_dir.clone(), toolname)
+ }
+
+ pub fn node_image_bin_dir(&self, node: &str, npm: &str) -> PathBuf {
+ self.node_image_dir(node, npm)
+ }
+}
+
+#[cfg(windows)]
+impl VoltaInstall {
+ pub fn bin_dir(&self) -> PathBuf {
+ path_buf!(self.root.clone(), "bin")
+ }
+}
+
+#[cfg(unix)]
+impl VoltaHome {
+ pub fn node_image_bin_dir(&self, node: &str, npm: &str) -> PathBuf {
+ path_buf!(self.node_image_dir(node, npm), "bin")
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +
use std::path::PathBuf;
+
+use super::executable;
+use volta_layout_macro::layout;
+
+layout! {
+ pub struct VoltaInstall {
+ "volta-shim[.exe]": shim_executable;
+ "volta[.exe]": main_executable;
+ "volta-migrate[.exe]": migrate_executable;
+ }
+
+ pub struct VoltaHome {
+ "cache": cache_dir {
+ "node": node_cache_dir {
+ "index.json": node_index_file;
+ "index.json.expires": node_index_expiry_file;
+ }
+ }
+ "bin": shim_dir {}
+ "log": log_dir {}
+ "tools": tools_dir {
+ "inventory": inventory_dir {
+ "node": node_inventory_dir {}
+ "packages": package_inventory_dir {}
+ "yarn": yarn_inventory_dir {}
+ }
+ "image": image_dir {
+ "node": node_image_root_dir {}
+ "yarn": yarn_image_root_dir {}
+ "packages": package_image_root_dir {}
+ }
+ "user": default_toolchain_dir {
+ "bins": default_bin_dir {}
+ "packages": default_package_dir {}
+ "platform.json": default_platform_file;
+ }
+ }
+ "tmp": tmp_dir {}
+ "hooks.json": default_hooks_file;
+ "layout.v1": layout_file;
+ }
+}
+
+impl VoltaHome {
+ pub fn package_distro_file(&self, name: &str, version: &str) -> PathBuf {
+ path_buf!(
+ self.package_inventory_dir.clone(),
+ format!("{}-{}.tgz", name, version)
+ )
+ }
+
+ pub fn package_distro_shasum(&self, name: &str, version: &str) -> PathBuf {
+ path_buf!(
+ self.package_inventory_dir.clone(),
+ format!("{}-{}.shasum", name, version)
+ )
+ }
+
+ pub fn node_image_dir(&self, node: &str, npm: &str) -> PathBuf {
+ path_buf!(self.node_image_root_dir.clone(), node, npm)
+ }
+
+ pub fn yarn_image_dir(&self, version: &str) -> PathBuf {
+ path_buf!(self.yarn_image_root_dir.clone(), version)
+ }
+
+ pub fn yarn_image_bin_dir(&self, version: &str) -> PathBuf {
+ path_buf!(self.yarn_image_dir(version), "bin")
+ }
+
+ pub fn package_image_dir(&self, name: &str, version: &str) -> PathBuf {
+ path_buf!(self.package_image_root_dir.clone(), name, version)
+ }
+
+ pub fn default_package_config_file(&self, package_name: &str) -> PathBuf {
+ path_buf!(
+ self.default_package_dir.clone(),
+ format!("{}.json", package_name)
+ )
+ }
+
+ pub fn default_tool_bin_config(&self, bin_name: &str) -> PathBuf {
+ path_buf!(self.default_bin_dir.clone(), format!("{}.json", bin_name))
+ }
+
+ pub fn node_npm_version_file(&self, version: &str) -> PathBuf {
+ path_buf!(
+ self.node_inventory_dir.clone(),
+ format!("node-v{}-npm", version)
+ )
+ }
+
+ pub fn shim_file(&self, toolname: &str) -> PathBuf {
+ path_buf!(self.shim_dir.clone(), executable(toolname))
+ }
+}
+
+#[cfg(windows)]
+impl VoltaHome {
+ pub fn shim_git_bash_script_file(&self, toolname: &str) -> PathBuf {
+ path_buf!(self.shim_dir.clone(), toolname)
+ }
+
+ pub fn node_image_bin_dir(&self, node: &str, npm: &str) -> PathBuf {
+ self.node_image_dir(node, npm)
+ }
+}
+
+#[cfg(unix)]
+impl VoltaHome {
+ pub fn node_image_bin_dir(&self, node: &str, npm: &str) -> PathBuf {
+ path_buf!(self.node_image_dir(node, npm), "bin")
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +
use std::path::PathBuf;
+
+use super::executable;
+use volta_layout_macro::layout;
+
+pub use crate::v1::VoltaInstall;
+
+layout! {
+ pub struct VoltaHome {
+ "cache": cache_dir {
+ "node": node_cache_dir {
+ "index.json": node_index_file;
+ "index.json.expires": node_index_expiry_file;
+ }
+ }
+ "bin": shim_dir {}
+ "log": log_dir {}
+ "tools": tools_dir {
+ "inventory": inventory_dir {
+ "node": node_inventory_dir {}
+ "npm": npm_inventory_dir {}
+ "packages": package_inventory_dir {}
+ "yarn": yarn_inventory_dir {}
+ }
+ "image": image_dir {
+ "node": node_image_root_dir {}
+ "npm": npm_image_root_dir {}
+ "yarn": yarn_image_root_dir {}
+ "packages": package_image_root_dir {}
+ }
+ "user": default_toolchain_dir {
+ "bins": default_bin_dir {}
+ "packages": default_package_dir {}
+ "platform.json": default_platform_file;
+ }
+ }
+ "tmp": tmp_dir {}
+ "hooks.json": default_hooks_file;
+ "layout.v2": layout_file;
+ }
+}
+
+impl VoltaHome {
+ pub fn package_distro_file(&self, name: &str, version: &str) -> PathBuf {
+ path_buf!(
+ self.package_inventory_dir.clone(),
+ format!("{}-{}.tgz", name, version)
+ )
+ }
+
+ pub fn package_distro_shasum(&self, name: &str, version: &str) -> PathBuf {
+ path_buf!(
+ self.package_inventory_dir.clone(),
+ format!("{}-{}.shasum", name, version)
+ )
+ }
+
+ pub fn node_image_dir(&self, node: &str) -> PathBuf {
+ path_buf!(self.node_image_root_dir.clone(), node)
+ }
+
+ pub fn npm_image_dir(&self, npm: &str) -> PathBuf {
+ path_buf!(self.npm_image_root_dir.clone(), npm)
+ }
+
+ pub fn npm_image_bin_dir(&self, npm: &str) -> PathBuf {
+ path_buf!(self.npm_image_dir(npm), "bin")
+ }
+
+ pub fn yarn_image_dir(&self, version: &str) -> PathBuf {
+ path_buf!(self.yarn_image_root_dir.clone(), version)
+ }
+
+ pub fn yarn_image_bin_dir(&self, version: &str) -> PathBuf {
+ path_buf!(self.yarn_image_dir(version), "bin")
+ }
+
+ pub fn package_image_dir(&self, name: &str, version: &str) -> PathBuf {
+ path_buf!(self.package_image_root_dir.clone(), name, version)
+ }
+
+ pub fn default_package_config_file(&self, package_name: &str) -> PathBuf {
+ path_buf!(
+ self.default_package_dir.clone(),
+ format!("{}.json", package_name)
+ )
+ }
+
+ pub fn default_tool_bin_config(&self, bin_name: &str) -> PathBuf {
+ path_buf!(self.default_bin_dir.clone(), format!("{}.json", bin_name))
+ }
+
+ pub fn node_npm_version_file(&self, version: &str) -> PathBuf {
+ path_buf!(
+ self.node_inventory_dir.clone(),
+ format!("node-v{}-npm", version)
+ )
+ }
+
+ pub fn shim_file(&self, toolname: &str) -> PathBuf {
+ path_buf!(self.shim_dir.clone(), executable(toolname))
+ }
+}
+
+#[cfg(windows)]
+impl VoltaHome {
+ pub fn shim_git_bash_script_file(&self, toolname: &str) -> PathBuf {
+ path_buf!(self.shim_dir.clone(), toolname)
+ }
+
+ pub fn node_image_bin_dir(&self, node: &str) -> PathBuf {
+ self.node_image_dir(node)
+ }
+}
+
+#[cfg(unix)]
+impl VoltaHome {
+ pub fn node_image_bin_dir(&self, node: &str) -> PathBuf {
+ path_buf!(self.node_image_dir(node), "bin")
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +
use std::path::PathBuf;
+
+use super::executable;
+use volta_layout_macro::layout;
+
+pub use crate::v1::VoltaInstall;
+
+layout! {
+ pub struct VoltaHome {
+ "cache": cache_dir {
+ "node": node_cache_dir {
+ "index.json": node_index_file;
+ "index.json.expires": node_index_expiry_file;
+ }
+ }
+ "bin": shim_dir {}
+ "log": log_dir {}
+ "tools": tools_dir {
+ "inventory": inventory_dir {
+ "node": node_inventory_dir {}
+ "npm": npm_inventory_dir {}
+ "pnpm": pnpm_inventory_dir {}
+ "yarn": yarn_inventory_dir {}
+ }
+ "image": image_dir {
+ "node": node_image_root_dir {}
+ "npm": npm_image_root_dir {}
+ "pnpm": pnpm_image_root_dir {}
+ "yarn": yarn_image_root_dir {}
+ "packages": package_image_root_dir {}
+ }
+ "shared": shared_lib_root {}
+ "user": default_toolchain_dir {
+ "bins": default_bin_dir {}
+ "packages": default_package_dir {}
+ "platform.json": default_platform_file;
+ }
+ }
+ "tmp": tmp_dir {}
+ "hooks.json": default_hooks_file;
+ "layout.v3": layout_file;
+ }
+}
+
+impl VoltaHome {
+ pub fn node_image_dir(&self, node: &str) -> PathBuf {
+ path_buf!(self.node_image_root_dir.clone(), node)
+ }
+
+ pub fn npm_image_dir(&self, npm: &str) -> PathBuf {
+ path_buf!(self.npm_image_root_dir.clone(), npm)
+ }
+
+ pub fn npm_image_bin_dir(&self, npm: &str) -> PathBuf {
+ path_buf!(self.npm_image_dir(npm), "bin")
+ }
+
+ pub fn pnpm_image_dir(&self, version: &str) -> PathBuf {
+ path_buf!(self.pnpm_image_root_dir.clone(), version)
+ }
+
+ pub fn pnpm_image_bin_dir(&self, version: &str) -> PathBuf {
+ path_buf!(self.pnpm_image_dir(version), "bin")
+ }
+
+ pub fn yarn_image_dir(&self, version: &str) -> PathBuf {
+ path_buf!(self.yarn_image_root_dir.clone(), version)
+ }
+
+ pub fn yarn_image_bin_dir(&self, version: &str) -> PathBuf {
+ path_buf!(self.yarn_image_dir(version), "bin")
+ }
+
+ pub fn package_image_dir(&self, name: &str) -> PathBuf {
+ path_buf!(self.package_image_root_dir.clone(), name)
+ }
+
+ pub fn default_package_config_file(&self, package_name: &str) -> PathBuf {
+ path_buf!(
+ self.default_package_dir.clone(),
+ format!("{}.json", package_name)
+ )
+ }
+
+ pub fn default_tool_bin_config(&self, bin_name: &str) -> PathBuf {
+ path_buf!(self.default_bin_dir.clone(), format!("{}.json", bin_name))
+ }
+
+ pub fn node_npm_version_file(&self, version: &str) -> PathBuf {
+ path_buf!(
+ self.node_inventory_dir.clone(),
+ format!("node-v{}-npm", version)
+ )
+ }
+
+ pub fn shim_file(&self, toolname: &str) -> PathBuf {
+ path_buf!(self.shim_dir.clone(), executable(toolname))
+ }
+
+ pub fn shared_lib_dir(&self, library: &str) -> PathBuf {
+ path_buf!(self.shared_lib_root.clone(), library)
+ }
+}
+
+#[cfg(windows)]
+impl VoltaHome {
+ pub fn shim_git_bash_script_file(&self, toolname: &str) -> PathBuf {
+ path_buf!(self.shim_dir.clone(), toolname)
+ }
+
+ pub fn node_image_bin_dir(&self, node: &str) -> PathBuf {
+ self.node_image_dir(node)
+ }
+}
+
+#[cfg(unix)]
+impl VoltaHome {
+ pub fn node_image_bin_dir(&self, node: &str) -> PathBuf {
+ path_buf!(self.node_image_dir(node), "bin")
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +
use std::path::PathBuf;
+
+use volta_layout_macro::layout;
+
+pub use crate::v1::VoltaInstall;
+
+layout! {
+ pub struct VoltaHome {
+ "cache": cache_dir {
+ "node": node_cache_dir {
+ "index.json": node_index_file;
+ "index.json.expires": node_index_expiry_file;
+ }
+ }
+ "bin": shim_dir {}
+ "log": log_dir {}
+ "tools": tools_dir {
+ "inventory": inventory_dir {
+ "node": node_inventory_dir {}
+ "npm": npm_inventory_dir {}
+ "pnpm": pnpm_inventory_dir {}
+ "yarn": yarn_inventory_dir {}
+ }
+ "image": image_dir {
+ "node": node_image_root_dir {}
+ "npm": npm_image_root_dir {}
+ "pnpm": pnpm_image_root_dir {}
+ "yarn": yarn_image_root_dir {}
+ "packages": package_image_root_dir {}
+ }
+ "shared": shared_lib_root {}
+ "user": default_toolchain_dir {
+ "bins": default_bin_dir {}
+ "packages": default_package_dir {}
+ "platform.json": default_platform_file;
+ }
+ }
+ "tmp": tmp_dir {}
+ "hooks.json": default_hooks_file;
+ "layout.v4": layout_file;
+ }
+}
+
+impl VoltaHome {
+ pub fn node_image_dir(&self, node: &str) -> PathBuf {
+ path_buf!(self.node_image_root_dir.clone(), node)
+ }
+
+ pub fn npm_image_dir(&self, npm: &str) -> PathBuf {
+ path_buf!(self.npm_image_root_dir.clone(), npm)
+ }
+
+ pub fn npm_image_bin_dir(&self, npm: &str) -> PathBuf {
+ path_buf!(self.npm_image_dir(npm), "bin")
+ }
+
+ pub fn pnpm_image_dir(&self, version: &str) -> PathBuf {
+ path_buf!(self.pnpm_image_root_dir.clone(), version)
+ }
+
+ pub fn pnpm_image_bin_dir(&self, version: &str) -> PathBuf {
+ path_buf!(self.pnpm_image_dir(version), "bin")
+ }
+
+ pub fn yarn_image_dir(&self, version: &str) -> PathBuf {
+ path_buf!(self.yarn_image_root_dir.clone(), version)
+ }
+
+ pub fn yarn_image_bin_dir(&self, version: &str) -> PathBuf {
+ path_buf!(self.yarn_image_dir(version), "bin")
+ }
+
+ pub fn package_image_dir(&self, name: &str) -> PathBuf {
+ path_buf!(self.package_image_root_dir.clone(), name)
+ }
+
+ pub fn default_package_config_file(&self, package_name: &str) -> PathBuf {
+ path_buf!(
+ self.default_package_dir.clone(),
+ format!("{}.json", package_name)
+ )
+ }
+
+ pub fn default_tool_bin_config(&self, bin_name: &str) -> PathBuf {
+ path_buf!(self.default_bin_dir.clone(), format!("{}.json", bin_name))
+ }
+
+ pub fn node_npm_version_file(&self, version: &str) -> PathBuf {
+ path_buf!(
+ self.node_inventory_dir.clone(),
+ format!("node-v{}-npm", version)
+ )
+ }
+
+ pub fn shim_file(&self, toolname: &str) -> PathBuf {
+ // On Windows, shims are created as `<name>.cmd` since they
+ // are thin scripts that use `volta run` to execute the command
+ #[cfg(windows)]
+ let toolname = format!("{}{}", toolname, ".cmd");
+
+ path_buf!(self.shim_dir.clone(), toolname)
+ }
+
+ pub fn shared_lib_dir(&self, library: &str) -> PathBuf {
+ path_buf!(self.shared_lib_root.clone(), library)
+ }
+}
+
+#[cfg(windows)]
+impl VoltaHome {
+ pub fn shim_git_bash_script_file(&self, toolname: &str) -> PathBuf {
+ path_buf!(self.shim_dir.clone(), toolname)
+ }
+
+ pub fn node_image_bin_dir(&self, node: &str) -> PathBuf {
+ self.node_image_dir(node)
+ }
+}
+
+#[cfg(unix)]
+impl VoltaHome {
+ pub fn node_image_bin_dir(&self, node: &str) -> PathBuf {
+ path_buf!(self.node_image_dir(node), "bin")
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +
use crate::ir::{Entry, Ir};
+use proc_macro2::TokenStream;
+use std::collections::HashMap;
+use syn::parse::{self, Parse, ParseStream};
+use syn::punctuated::Punctuated;
+use syn::{braced, Attribute, Ident, LitStr, Token, Visibility};
+
+pub(crate) type Result<T> = ::std::result::Result<T, TokenStream>;
+
+/// Abstract syntax tree (AST) for the surface syntax of the `layout!` macro.
+///
+/// The surface syntax of the `layout!` macro takes the form:
+///
+/// ```text,no_run
+/// Attribute* Visibility "struct" Ident Directory
+/// ```
+///
+/// This AST gets lowered by the `flatten` method to a vector of intermediate
+/// representation (IR) trees. See the `Ir` type for details.
+pub(crate) struct Ast {
+ decls: Vec<LayoutStruct>,
+}
+
+impl Parse for Ast {
+ fn parse(input: ParseStream) -> parse::Result<Self> {
+ let mut decls = Vec::new();
+ while !input.is_empty() {
+ let decl = input.call(LayoutStruct::parse)?;
+ decls.push(decl);
+ }
+ Ok(Ast { decls })
+ }
+}
+
+impl Ast {
+ /// Compiles (macro-expands) the AST.
+ pub(crate) fn compile(self) -> TokenStream {
+ self.decls
+ .into_iter()
+ .map(|decl| match decl.flatten() {
+ Ok(ir) => ir.codegen(),
+ Err(err) => err,
+ })
+ .collect()
+ }
+}
+
+/// Represents a single type LayoutStruct in the AST, which takes the form:
+///
+/// ```text,no_run
+/// Attribute* Visibility "struct" Ident Directory
+/// ```
+///
+/// This AST gets lowered by the `flatten` method to a flat list of entries,
+/// organized by entry type. See the `Ir` type for details.
+pub(crate) struct LayoutStruct {
+ attrs: Vec<Attribute>,
+ visibility: Visibility,
+ name: Ident,
+ directory: Directory,
+}
+
+impl Parse for LayoutStruct {
+ fn parse(input: ParseStream) -> parse::Result<Self> {
+ let attrs: Vec<Attribute> = input.call(Attribute::parse_outer)?;
+ let visibility: Visibility = input.parse()?;
+ input.parse::<Token![struct]>()?;
+ let name: Ident = input.parse()?;
+ let directory: Directory = input.parse()?;
+ Ok(LayoutStruct {
+ attrs,
+ visibility,
+ name,
+ directory,
+ })
+ }
+}
+
+impl LayoutStruct {
+ /// Lowers the AST to a flattened intermediate representation.
+ fn flatten(self) -> Result<Ir> {
+ let mut results = Ir {
+ name: self.name,
+ attrs: self.attrs,
+ visibility: self.visibility,
+ dirs: vec![],
+ files: vec![],
+ exes: vec![],
+ };
+
+ self.directory.flatten(&mut results, vec![])?;
+
+ Ok(results)
+ }
+}
+
+/// Represents a directory entry in the AST, which can recursively contain
+/// more entries.
+///
+/// The surface syntax of a directory takes the form:
+///
+/// ```text,no_run
+/// {
+/// (FieldPrefix)FieldContents*
+/// }
+/// ```
+struct Directory {
+ entries: Punctuated<FieldPrefix, FieldContents>,
+}
+
+impl Parse for Directory {
+ fn parse(input: ParseStream) -> parse::Result<Self> {
+ let content;
+ braced!(content in input);
+ Ok(Directory {
+ entries: content.parse_terminated(FieldPrefix::parse)?,
+ })
+ }
+}
+
+enum EntryKind {
+ Exe,
+ File,
+ Dir,
+}
+
+impl Directory {
+ /// Lowers the directory to a flattened intermediate representation.
+ fn flatten(self, results: &mut Ir, context: Vec<LitStr>) -> Result<()> {
+ let mut visited_entries = HashMap::new();
+
+ for pair in self.entries.into_pairs() {
+ let (prefix, punc) = pair.into_tuple();
+
+ let mut entry = Entry {
+ name: prefix.name,
+ context: context.clone(),
+ filename: prefix.filename.clone(),
+ };
+
+ match punc {
+ Some(FieldContents::Dir(dir)) => {
+ let filename = prefix.filename.value();
+
+ if filename.ends_with(".exe") || filename.ends_with("[.exe]") {
+ let error = syn::Error::new(
+ prefix.filename.span(),
+ "the `.exe` extension is not allowed for directory names",
+ );
+ return Err(error.to_compile_error());
+ }
+
+ if let Some(kind) = visited_entries.get(&filename) {
+ let message = match kind {
+ EntryKind::Exe => {
+ format!("filename `{}` is a duplicate of `{}` executable on non-Windows operating systems", filename, filename)
+ }
+ _ => {
+ format!("duplicate filename `{}`", filename)
+ }
+ };
+ let error = syn::Error::new(prefix.filename.span(), message);
+ return Err(error.to_compile_error());
+ }
+
+ visited_entries.insert(filename.clone(), EntryKind::Dir);
+
+ results.dirs.push(entry);
+ let mut sub_context = context.clone();
+ sub_context.push(prefix.filename);
+ dir.flatten(results, sub_context)?;
+ }
+ _ => {
+ let filename = prefix.filename.value();
+ if filename.ends_with("[.exe]") {
+ let filename = &filename[0..filename.len() - 6];
+
+ if let Some(kind) = visited_entries.get(filename) {
+ let message = match kind {
+ EntryKind::Exe => {
+ format!("duplicate filename `{}.exe`", filename)
+ }
+ EntryKind::File => {
+ format!("executable `{}` (on non-Windows operating systems) is a duplicate of `{}` filename", filename, filename)
+ }
+ EntryKind::Dir => {
+ format!("executable `{}` (on non-Windows operating systems) is a duplicate of `{}` directory name", filename, filename)
+ }
+ };
+ let error = syn::Error::new(prefix.filename.span(), message);
+ return Err(error.to_compile_error());
+ }
+
+ visited_entries.insert(filename.to_string(), EntryKind::Exe);
+ entry.filename = LitStr::new(filename, prefix.filename.span());
+ results.exes.push(entry);
+ } else {
+ if let Some(kind) = visited_entries.get(&filename) {
+ let message = match kind {
+ EntryKind::Exe => {
+ format!("filename `{}` is a duplicate of `{}` executable on non-Windows operating systems", filename, filename)
+ }
+ _ => {
+ format!("duplicate filename `{}`", filename)
+ }
+ };
+ let error = syn::Error::new(prefix.filename.span(), message);
+ return Err(error.to_compile_error());
+ }
+
+ visited_entries.insert(filename, EntryKind::File);
+ results.files.push(entry);
+ }
+ }
+ }
+ }
+ Ok(())
+ }
+}
+
+/// AST for the common prefix of a single field in a `layout!` struct declaration,
+/// which is of the form:
+///
+/// ```text,no_run
+/// LitStr ":" Ident
+/// ```
+///
+/// This is followed either by a semicolon (`;`), indicating that the field is a
+/// file, or a braced directory entry, indicating that the field is a directory.
+///
+/// If the `LitStr` contains the suffix `"[.exe]"` it is treated specially as an
+/// executable file, whose suffix (or lack thereof) is determined by the current
+/// operating system (using the `std::env::consts::EXE_SUFFIX` constant).
+struct FieldPrefix {
+ filename: LitStr,
+ name: Ident,
+}
+
+impl Parse for FieldPrefix {
+ fn parse(input: ParseStream) -> parse::Result<Self> {
+ let filename = input.parse()?;
+ input.parse::<Token![:]>()?;
+ let name = input.parse()?;
+ Ok(FieldPrefix { filename, name })
+ }
+}
+
+/// AST for the suffix of a field in a `layout!` struct declaration.
+enum FieldContents {
+ /// A file field suffix, which consists of a single semicolon (`;`).
+ File(Token![;]),
+
+ /// A directory field suffix, which consists of a braced directory.
+ Dir(Directory),
+}
+
+impl Parse for FieldContents {
+ fn parse(input: ParseStream) -> parse::Result<Self> {
+ let lookahead = input.lookahead1();
+ Ok(if lookahead.peek(Token![;]) {
+ let semi = input.parse()?;
+ FieldContents::File(semi)
+ } else {
+ let directory = input.parse()?;
+ FieldContents::Dir(directory)
+ })
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +
// The `proc_macro2` crate is a polyfill for advanced functionality of Rust's
+// procedural macros, not all of which have shipped in stable Rust. It's used by
+// the `syn` and `quote` crates to produce a shimmed version of the standard
+// `TokenStream` type. So internally that's the type we have to use for the
+// implementation of our macro. The actual front-end for the macro takes this
+// shimmed `TokenStream` type and converts it to the built-in `TokenStream` type
+// required by the Rust macro system.
+use proc_macro2::TokenStream;
+use quote::quote;
+use syn::{Attribute, Ident, LitStr, Visibility};
+
+// These seem to be leaked implementation details of the `quote` macro that have
+// to be imported by users. You can ignore them; they simply pacify the compiler.
+#[allow(unused_imports)]
+use quote::{pounded_var_names, quote_each_token, quote_spanned};
+
+/// The intermediate representation (IR) of a struct type defined by the `layout!`
+/// macro, which contains the flattened directory entries, organized into three
+/// categories:
+///
+/// - Directories
+/// - Executable files
+/// - Other files
+pub(crate) struct Ir {
+ pub(crate) name: Ident,
+ pub(crate) attrs: Vec<Attribute>,
+ pub(crate) visibility: Visibility,
+ pub(crate) dirs: Vec<Entry>,
+ pub(crate) files: Vec<Entry>,
+ pub(crate) exes: Vec<Entry>,
+}
+
+impl Ir {
+ fn dir_names(&self) -> impl Iterator<Item = &Ident> {
+ self.dirs.iter().map(|entry| &entry.name)
+ }
+
+ fn file_names(&self) -> impl Iterator<Item = &Ident> {
+ self.files.iter().map(|entry| &entry.name)
+ }
+
+ fn exe_names(&self) -> impl Iterator<Item = &Ident> {
+ self.exes.iter().map(|entry| &entry.name)
+ }
+
+ fn field_names(&self) -> impl Iterator<Item = &Ident> {
+ let dir_names = self.dir_names();
+ let file_names = self.file_names();
+ let exe_names = self.exe_names();
+ dir_names.chain(file_names).chain(exe_names)
+ }
+
+ fn to_struct_decl(&self) -> TokenStream {
+ let name = &self.name;
+
+ let attrs = self.attrs.iter();
+ let visibility = self.visibility.clone();
+
+ let field_names = self.field_names().map(|field_name| {
+ // Use the field name's span for good duplicate-field-name error messages.
+ quote_spanned! {field_name.span()=>
+ #field_name : ::std::path::PathBuf ,
+ }
+ });
+
+ quote! {
+ #(#attrs)* #visibility struct #name {
+ #(#field_names)*
+ root: ::std::path::PathBuf,
+ }
+ }
+ }
+
+ fn to_create_method(&self) -> TokenStream {
+ let name = &self.name;
+ let dir_names = self.dir_names();
+
+ quote! {
+ impl #name {
+ /// Creates all subdirectories in this directory layout.
+ pub fn create(&self) -> ::std::io::Result<()> {
+ #(::std::fs::create_dir_all(self.#dir_names())?;)*
+ ::std::result::Result::Ok(())
+ }
+ }
+ }
+ }
+
+ fn to_item_methods(&self) -> TokenStream {
+ let name = &self.name;
+
+ let methods = self.field_names().map(|field_name| {
+ // Markdown-formatted field name for the doc comment.
+ let markdown_field_name = format!("`{}`", field_name);
+ let markdown_field_name = LitStr::new(&markdown_field_name, field_name.span());
+
+ // Use the field name's span for good duplicate-method-name error messages.
+ quote_spanned! {field_name.span()=>
+ #[doc = "Returns the "]
+ #[doc = #markdown_field_name]
+ #[doc = " path."]
+ pub fn #field_name(&self) -> &::std::path::Path { &self.#field_name }
+ }
+ });
+
+ quote! {
+ impl #name {
+ #(#methods)*
+
+ /// Returns the root path for this directory layout.
+ pub fn root(&self) -> &::std::path::Path { &self.root }
+ }
+ }
+ }
+
+ fn to_ctor(&self) -> TokenStream {
+ let name = &self.name;
+ let root = Ident::new("root", self.name.span());
+
+ let dir_names = self.dir_names();
+ let dir_inits = self.dirs.iter().map(|entry| entry.to_normal_init(&root));
+
+ let file_names = self.file_names();
+ let file_inits = self.files.iter().map(|entry| entry.to_normal_init(&root));
+
+ let exe_names = self.exe_names();
+ let exe_inits = self.exes.iter().map(|entry| entry.to_exe_init(&root));
+
+ let all_names = dir_names.chain(file_names).chain(exe_names);
+ let all_inits = dir_inits.chain(file_inits).chain(exe_inits);
+
+ let markdown_struct_name = format!("`{}`", name);
+ let markdown_struct_name = LitStr::new(&markdown_struct_name, name.span());
+
+ quote! {
+ impl #name {
+ #[doc = "Constructs a new instance of the "]
+ #[doc = #markdown_struct_name]
+ #[doc = " layout, rooted at `root`."]
+ pub fn new(#root: ::std::path::PathBuf) -> Self {
+ Self {
+ #(#all_names: #all_inits),* ,
+ #root: #root
+ }
+ }
+ }
+ }
+ }
+
+ pub(crate) fn codegen(&self) -> TokenStream {
+ let struct_decl = self.to_struct_decl();
+ let ctor = self.to_ctor();
+ let item_methods = self.to_item_methods();
+ let create_method = self.to_create_method();
+
+ quote! {
+ #struct_decl
+ #ctor
+ #item_methods
+ #create_method
+ }
+ }
+}
+
+pub(crate) struct Entry {
+ pub(crate) name: Ident,
+ pub(crate) context: Vec<LitStr>,
+ pub(crate) filename: LitStr,
+}
+
+impl Entry {
+ fn to_normal_init(&self, root: &Ident) -> TokenStream {
+ let name = &self.name;
+ let path_items = self.context.iter();
+ let name_replicated = self.context.iter().map(|_| name);
+ let filename = &self.filename;
+
+ quote! {
+ {
+ let mut #name = #root.clone();
+ #(#name_replicated.push(#path_items);)*
+ #name.push(#filename);
+ #name
+ }
+ }
+ }
+
+ fn to_exe_init(&self, root: &Ident) -> TokenStream {
+ let name = &self.name;
+ let path_items = self.context.iter();
+ let name_replicated = self.context.iter().map(|_| name);
+ let filename = &self.filename;
+
+ quote! {
+ {
+ let mut #name = #root.clone();
+ #(#name_replicated.push(#path_items);)*
+ #name.push(::std::format!("{}{}", #filename, ::std::env::consts::EXE_SUFFIX));
+ #name
+ }
+ }
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +
#![recursion_limit = "128"]
+
+extern crate proc_macro;
+
+mod ast;
+mod ir;
+
+use crate::ast::Ast;
+use proc_macro::TokenStream;
+use syn::parse_macro_input;
+
+/// A macro for defining Volta directory layout hierarchies.
+///
+/// The syntax of `layout!` takes the form:
+///
+/// ```text,no_run
+/// layout! {
+/// LayoutStruct*
+/// }
+/// ```
+///
+/// The syntax of a `LayoutStruct` takes the form:
+///
+/// ```text,no_run
+/// Attribute* Visibility "struct" Ident Directory
+/// ```
+///
+/// The syntax of a `Directory` takes the form:
+///
+/// ```text,no_run
+/// {
+/// (FieldPrefix)FieldContents*
+/// }
+/// ```
+///
+/// The syntax of a `FieldPrefix` takes the form:
+///
+/// ```text,no_run
+/// LitStr ":" Ident
+/// ```
+///
+/// The syntax of a `FieldContents` is either:
+///
+/// ```text,no_run
+/// ";"
+/// ```
+///
+/// or:
+///
+/// ```text,no_run
+/// Directory
+/// ```
+#[proc_macro]
+pub fn layout(input: TokenStream) -> TokenStream {
+ let ast = parse_macro_input!(input as Ast);
+ let expanded = ast.compile();
+ TokenStream::from(expanded)
+}
+
use std::path::PathBuf;
+
+/// Represents an Empty (or uninitialized) Volta layout, one that has never been used by any prior version
+///
+/// This is the easiest to migrate from, as we simply need to create the current layout within the .volta
+/// directory
+pub struct Empty {
+ pub home: PathBuf,
+}
+
+impl Empty {
+ pub fn new(home: PathBuf) -> Self {
+ Empty { home }
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +
//! Provides types for modeling the current state of the Volta directory and for migrating between versions
+//!
+//! A new layout should be represented by its own struct (as in the existing v0 or v1 modules)
+//! Migrations between types should be represented by `TryFrom` implementations between the layout types
+//! (see v1.rs for examples)
+//!
+//! NOTE: Since the layout file is written once the migration is complete, all migration implementations
+//! need to be aware that they may be partially applied (if something fails in the process) and should be
+//! able to re-start gracefully from an interrupted migration
+
+use std::path::Path;
+
+mod empty;
+mod v0;
+mod v1;
+mod v2;
+mod v3;
+mod v4;
+
+use v0::V0;
+use v1::V1;
+use v2::V2;
+use v3::V3;
+use v4::V4;
+
+use log::{debug, info};
+use volta_core::error::Fallible;
+use volta_core::layout::volta_home;
+#[cfg(unix)]
+use volta_core::layout::volta_install;
+use volta_core::shim::regenerate_shims_for_dir;
+use volta_core::sync::VoltaLock;
+
+/// Represents the state of the Volta directory at every point in the migration process
+///
+/// Migrations should be applied sequentially, migrating from V0 to V1 to ... as needed, cycling
+/// through the possible MigrationState values.
+enum MigrationState {
+ Empty(empty::Empty),
+ V0(Box<V0>),
+ V1(Box<V1>),
+ V2(Box<V2>),
+ V3(Box<V3>),
+ V4(Box<V4>),
+}
+
+/// Macro to simplify the boilerplate associated with detecting a tagged state.
+///
+/// Should be passed a series of tuples, each of which contains (in this order):
+///
+/// * The layout version (module name from `volta_layout` crate, e.g. `v1`)
+/// * The `MigrationState` variant name (e.g. `V1`)
+/// * The migration object itself (e.g. `V1` from the v1 module in _this_ crate)
+///
+/// The tuples should be in reverse chronological order, so that the newest is first, e.g.:
+///
+/// detect_tagged!((v3, V3, V3), (v2, V2, V2), (v1, V1, V1));
+macro_rules! detect_tagged {
+ ($(($layout:ident, $variant:ident, $migration:ident)),*) => {
+ impl MigrationState {
+ fn detect_tagged_state(home: &::std::path::Path) -> Option<Self> {
+ None
+ $(
+ .or_else(|| detect::$layout(home))
+ )*
+ }
+ }
+
+ mod detect {
+ $(
+ pub(super) fn $layout(home: &::std::path::Path) -> Option<super::MigrationState> {
+ let volta_home = volta_layout::$layout::VoltaHome::new(home.to_owned());
+ if volta_home.layout_file().exists() {
+ Some(super::MigrationState::$variant(Box::new(super::$migration::new(home.to_owned()))))
+ } else {
+ None
+ }
+ }
+ )*
+ }
+ }
+}
+
+detect_tagged!((v4, V4, V4), (v3, V3, V3), (v2, V2, V2), (v1, V1, V1));
+
+impl MigrationState {
+ fn current() -> Fallible<Self> {
+ // First look for a tagged version (V1+). If that can't be found, then go through the triage
+ // for detecting a legacy version
+
+ let home = volta_home()?;
+
+ match MigrationState::detect_tagged_state(home.root()) {
+ Some(state) => Ok(state),
+ None => MigrationState::detect_legacy_state(home.root()),
+ }
+ }
+
+ #[allow(clippy::unnecessary_wraps)] // Needs to be Fallible for Unix
+ fn detect_legacy_state(home: &Path) -> Fallible<Self> {
+ /*
+ Triage for determining the legacy layout version:
+ - Does Volta Home exist?
+ - If yes (Windows) then V0
+ - If yes (Unix) then check if Volta Install is outside shim_dir?
+ - If yes, then V0
+ - If no, then check if $VOLTA_HOME/load.sh exists? If yes then V0
+ - Else Empty
+
+ The extra logic on Unix is necessary because Unix installs can be either inside or outside $VOLTA_HOME
+ If it is inside, then the directory necessarily must exist, so we can't use that as a determination.
+ If it is outside (and for Windows which is always outside), then if $VOLTA_HOME exists, it must be from a
+ previous, V0 installation.
+ */
+
+ let volta_home = home.to_owned();
+
+ if volta_home.exists() {
+ #[cfg(windows)]
+ return Ok(MigrationState::V0(Box::new(V0::new(volta_home))));
+
+ #[cfg(unix)]
+ {
+ let install = volta_install()?;
+ if install.root().starts_with(&volta_home) {
+ // Installed inside $VOLTA_HOME, so need to look for `load.sh` as a marker
+ if volta_home.join("load.sh").exists() {
+ return Ok(MigrationState::V0(Box::new(V0::new(volta_home))));
+ }
+ } else {
+ // Installed outside of $VOLTA_HOME, so it must exist from a previous V0 install
+ return Ok(MigrationState::V0(Box::new(V0::new(volta_home))));
+ }
+ }
+ }
+
+ Ok(MigrationState::Empty(empty::Empty::new(volta_home)))
+ }
+}
+
+pub fn run_migration() -> Fallible<()> {
+ // Acquire an exclusive lock on the Volta directory, to ensure that no other migrations are running.
+ // If this fails, however, we still need to run the migration
+ match VoltaLock::acquire() {
+ Ok(_lock) => {
+ // The lock was acquired, so we can be confident that no other migrations are running
+ detect_and_migrate()
+ }
+ Err(_) => {
+ debug!("Unable to acquire lock on Volta directory! Running migration anyway.");
+ detect_and_migrate()
+ }
+ }
+}
+
+fn detect_and_migrate() -> Fallible<()> {
+ info!("Updating your Volta directory. This may take a few moments...");
+ let mut state = MigrationState::current()?;
+
+ // To keep the complexity of writing a new migration from continuously increasing, each new
+ // layout version only needs to implement a migration from 2 states: Empty and the previously
+ // latest version. We then apply the migrations sequentially here: V0 -> V1 -> ... -> VX
+ loop {
+ state = match state {
+ MigrationState::Empty(e) => MigrationState::V3(Box::new(e.try_into()?)),
+ MigrationState::V0(zero) => MigrationState::V1(Box::new((*zero).try_into()?)),
+ MigrationState::V1(one) => MigrationState::V2(Box::new((*one).try_into()?)),
+ MigrationState::V2(two) => MigrationState::V3(Box::new((*two).try_into()?)),
+ MigrationState::V3(three) => MigrationState::V4(Box::new((*three).try_into()?)),
+ MigrationState::V4(_) => {
+ break;
+ }
+ };
+ }
+
+ regenerate_shims_for_dir(volta_home()?.shim_dir())?;
+
+ Ok(())
+}
+
use std::path::PathBuf;
+
+use volta_layout::v0::VoltaHome;
+
+/// Represents a V0 Volta layout (from before v0.7.0)
+///
+/// This needs some migration work to move up to V1, so we keep a reference to the V0 layout
+/// struct to allow for easy comparison between versions
+pub struct V0 {
+ pub home: VoltaHome,
+}
+
+impl V0 {
+ pub fn new(home: PathBuf) -> Self {
+ V0 {
+ home: VoltaHome::new(home),
+ }
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +
#[cfg(unix)]
+use std::fs::remove_file;
+use std::fs::File;
+use std::path::PathBuf;
+
+use super::empty::Empty;
+use super::v0::V0;
+use log::debug;
+use volta_core::error::{Context, ErrorKind, Fallible, VoltaError};
+#[cfg(unix)]
+use volta_core::fs::{read_dir_eager, remove_file_if_exists};
+use volta_layout::v1;
+
+/// Represents a V1 Volta Layout (used by Volta v0.7.0 - v0.7.2)
+///
+/// Holds a reference to the V1 layout struct to support potential future migrations
+pub struct V1 {
+ pub home: v1::VoltaHome,
+}
+
+impl V1 {
+ pub fn new(home: PathBuf) -> Self {
+ V1 {
+ home: v1::VoltaHome::new(home),
+ }
+ }
+
+ /// Write the layout file to mark migration to V1 as complete
+ ///
+ /// Should only be called once all other migration steps are finished, so that we don't
+ /// accidentally mark an incomplete migration as completed
+ fn complete_migration(home: v1::VoltaHome) -> Fallible<Self> {
+ debug!("Writing layout marker file");
+ File::create(home.layout_file()).with_context(|| ErrorKind::CreateLayoutFileError {
+ file: home.layout_file().to_owned(),
+ })?;
+
+ Ok(V1 { home })
+ }
+}
+
+impl TryFrom<Empty> for V1 {
+ type Error = VoltaError;
+
+ fn try_from(old: Empty) -> Fallible<V1> {
+ debug!("New Volta installation detected, creating fresh layout");
+
+ let home = v1::VoltaHome::new(old.home);
+ home.create().with_context(|| ErrorKind::CreateDirError {
+ dir: home.root().to_owned(),
+ })?;
+
+ V1::complete_migration(home)
+ }
+}
+
+impl TryFrom<V0> for V1 {
+ type Error = VoltaError;
+
+ fn try_from(old: V0) -> Fallible<V1> {
+ debug!("Existing Volta installation detected, migrating from V0 layout");
+
+ let new_home = v1::VoltaHome::new(old.home.root().to_owned());
+ new_home
+ .create()
+ .with_context(|| ErrorKind::CreateDirError {
+ dir: new_home.root().to_owned(),
+ })?;
+
+ #[cfg(unix)]
+ {
+ debug!("Removing unnecessary 'load.*' files");
+ let root_contents =
+ read_dir_eager(new_home.root()).with_context(|| ErrorKind::ReadDirError {
+ dir: new_home.root().to_owned(),
+ })?;
+ for (entry, _) in root_contents {
+ let path = entry.path();
+ if let Some(stem) = path.file_stem() {
+ if stem == "load" && path.is_file() {
+ remove_file(&path)
+ .with_context(|| ErrorKind::DeleteFileError { file: path })?;
+ }
+ }
+ }
+
+ debug!("Removing old Volta binaries");
+
+ let old_volta_bin = new_home.root().join("volta");
+ remove_file_if_exists(old_volta_bin)?;
+
+ let old_shim_bin = new_home.root().join("shim");
+ remove_file_if_exists(old_shim_bin)?;
+ }
+
+ V1::complete_migration(new_home)
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +
use std::fs::{read_to_string, write, File};
+use std::io;
+use std::path::{Path, PathBuf};
+
+use super::empty::Empty;
+use super::v1::V1;
+use log::debug;
+use node_semver::Version;
+use tempfile::tempdir_in;
+use volta_core::error::{Context, ErrorKind, Fallible, VoltaError};
+use volta_core::fs::{read_dir_eager, remove_dir_if_exists, remove_file_if_exists, rename};
+use volta_core::tool::load_default_npm_version;
+use volta_core::toolchain::serial::Platform;
+use volta_core::version::parse_version;
+use volta_layout::{v1, v2};
+
+/// Represents a V2 Volta Layout (used by Volta v0.7.3 and above)
+///
+/// Holds a reference to the V2 layout struct to support potential future migrations
+pub struct V2 {
+ pub home: v2::VoltaHome,
+}
+
+impl V2 {
+ pub fn new(home: PathBuf) -> Self {
+ V2 {
+ home: v2::VoltaHome::new(home),
+ }
+ }
+
+ /// Write the layout file to mark migration to V2 as complete
+ ///
+ /// Should only be called once all other migration steps are finished, so that we don't
+ /// accidentally mark an incomplete migration as completed
+ fn complete_migration(home: v2::VoltaHome) -> Fallible<Self> {
+ debug!("Writing layout marker file");
+ File::create(home.layout_file()).with_context(|| ErrorKind::CreateLayoutFileError {
+ file: home.layout_file().to_owned(),
+ })?;
+
+ Ok(V2 { home })
+ }
+}
+
+impl TryFrom<Empty> for V2 {
+ type Error = VoltaError;
+
+ fn try_from(old: Empty) -> Fallible<V2> {
+ debug!("New Volta installation detected, creating fresh layout");
+
+ let home = v2::VoltaHome::new(old.home);
+ home.create().with_context(|| ErrorKind::CreateDirError {
+ dir: home.root().to_owned(),
+ })?;
+
+ V2::complete_migration(home)
+ }
+}
+
+impl TryFrom<V1> for V2 {
+ type Error = VoltaError;
+
+ fn try_from(old: V1) -> Fallible<V2> {
+ debug!("Migrating from V1 layout");
+
+ let new_home = v2::VoltaHome::new(old.home.root().to_owned());
+ new_home
+ .create()
+ .with_context(|| ErrorKind::CreateDirError {
+ dir: new_home.root().to_owned(),
+ })?;
+
+ // Perform the core of the migration
+ clear_default_npm(old.home.default_platform_file())?;
+ shift_node_images(&old.home, &new_home)?;
+
+ // Complete the migration, writing the V2 layout file
+ let layout = V2::complete_migration(new_home)?;
+
+ // Remove the V1 layout file, since we're now on V2 (do this after writing the V2 so that we know the migration succeeded)
+ let old_layout_file = old.home.layout_file();
+ remove_file_if_exists(old_layout_file)?;
+ Ok(layout)
+ }
+}
+
+/// Clear npm from the default `platform.json` file if it is set to the same value as that bundled with Node
+///
+/// This will ensure that we don't treat the default npm from a prior version of Volta as a "custom" npm that
+/// the user explicitly requested
+fn clear_default_npm(platform_file: &Path) -> Fallible<()> {
+ let platform_json = match read_to_string(platform_file) {
+ Ok(json) => json,
+ Err(error) => {
+ if error.kind() == io::ErrorKind::NotFound {
+ return Ok(());
+ } else {
+ return Err(VoltaError::from_source(
+ error,
+ ErrorKind::ReadPlatformError {
+ file: platform_file.to_path_buf(),
+ },
+ ));
+ }
+ }
+ };
+ let mut existing_platform = Platform::try_from(platform_json)?;
+
+ if let Some(ref mut node_version) = &mut existing_platform.node {
+ if let Some(npm) = &node_version.npm {
+ if let Ok(default_npm) = load_default_npm_version(&node_version.runtime) {
+ if *npm == default_npm {
+ node_version.npm = None;
+ write(platform_file, existing_platform.into_json()?).with_context(|| {
+ ErrorKind::WritePlatformError {
+ file: platform_file.to_owned(),
+ }
+ })?;
+ }
+ }
+ }
+ }
+
+ Ok(())
+}
+
+/// Move all Node images up one directory, removing the default npm version directory
+///
+/// In the V1 layout, we kept all node images in /<node_version>/<npm_version>/, however we will be
+/// storing custom npm versions in a separate image directory, so there is no need to maintain the
+/// bundled npm version in the file structure any more. This also will make it slightly easier to access
+/// the Node image, as we no longer will need to look up the bundled npm version every time.
+fn shift_node_images(old_home: &v1::VoltaHome, new_home: &v2::VoltaHome) -> Fallible<()> {
+ let temp_dir =
+ tempdir_in(new_home.tmp_dir()).with_context(|| ErrorKind::CreateTempDirError {
+ in_dir: new_home.tmp_dir().to_owned(),
+ })?;
+ let node_installs = read_dir_eager(old_home.node_image_root_dir())
+ .with_context(|| ErrorKind::ReadDirError {
+ dir: old_home.node_image_root_dir().to_owned(),
+ })?
+ .filter_map(|(entry, metadata)| {
+ if metadata.is_dir() {
+ parse_version(entry.file_name().to_string_lossy()).ok()
+ } else {
+ None
+ }
+ });
+
+ for node_version in node_installs {
+ remove_npm_version_from_node_image_dir(old_home, new_home, node_version, temp_dir.path())?;
+ }
+
+ Ok(())
+}
+
+/// Move a single node image up a directory, if it currently has the npm version in its path
+fn remove_npm_version_from_node_image_dir(
+ old_home: &v1::VoltaHome,
+ new_home: &v2::VoltaHome,
+ node_version: Version,
+ temp_dir: &Path,
+) -> Fallible<()> {
+ let node_string = node_version.to_string();
+ let npm_version = load_default_npm_version(&node_version)?;
+ let old_install = old_home.node_image_dir(&node_string, &npm_version.to_string());
+
+ if old_install.exists() {
+ let temp_image = temp_dir.join(&node_string);
+ let new_install = new_home.node_image_dir(&node_string);
+ rename(&old_install, &temp_image).with_context(|| ErrorKind::SetupToolImageError {
+ tool: "Node".into(),
+ version: node_string.clone(),
+ dir: temp_image.clone(),
+ })?;
+ remove_dir_if_exists(&new_install)?;
+ rename(&temp_image, &new_install).with_context(|| ErrorKind::SetupToolImageError {
+ tool: "Node".into(),
+ version: node_string,
+ dir: temp_image,
+ })?;
+ }
+ Ok(())
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +
use std::fs::File;
+use std::path::{Path, PathBuf};
+
+use crate::empty::Empty;
+use crate::v2::V2;
+use log::{debug, warn};
+use volta_core::error::{Context, ErrorKind, Fallible, VoltaError};
+use volta_core::fs::{remove_dir_if_exists, remove_file_if_exists};
+use volta_core::platform::PlatformSpec;
+use volta_core::session::Session;
+use volta_core::tool::{Package, PackageConfig};
+use volta_core::version::VersionSpec;
+use volta_layout::{v2, v3};
+use walkdir::WalkDir;
+
+mod config;
+
+use config::LegacyPackageConfig;
+
+/// Represents a V3 Volta layout (used by Volta v0.9.0 and above)
+///
+/// Holds a reference to the V3 layout struct to support future migrations
+pub struct V3 {
+ pub home: v3::VoltaHome,
+}
+
+impl V3 {
+ pub fn new(home: PathBuf) -> Self {
+ V3 {
+ home: v3::VoltaHome::new(home),
+ }
+ }
+
+ /// Write the layout file to mark migration to V2 as complete
+ ///
+ /// Should only be called once all other migration steps are finished, so that we don't
+ /// accidentally mark an incomplete migration as completed
+ fn complete_migration(home: v3::VoltaHome) -> Fallible<Self> {
+ debug!("Writing layout marker file");
+ File::create(home.layout_file()).with_context(|| ErrorKind::CreateLayoutFileError {
+ file: home.layout_file().to_owned(),
+ })?;
+
+ Ok(V3 { home })
+ }
+}
+
+impl TryFrom<Empty> for V3 {
+ type Error = VoltaError;
+
+ fn try_from(old: Empty) -> Fallible<Self> {
+ debug!("New Volta installation detected, creating fresh layout");
+
+ let home = v3::VoltaHome::new(old.home);
+ home.create().with_context(|| ErrorKind::CreateDirError {
+ dir: home.root().to_owned(),
+ })?;
+
+ V3::complete_migration(home)
+ }
+}
+
+impl TryFrom<V2> for V3 {
+ type Error = VoltaError;
+
+ fn try_from(old: V2) -> Fallible<Self> {
+ debug!("Migrating from V2 layout");
+
+ let new_home = v3::VoltaHome::new(old.home.root().to_owned());
+ new_home
+ .create()
+ .with_context(|| ErrorKind::CreateDirError {
+ dir: new_home.root().to_owned(),
+ })?;
+
+ // Migrate installed packages to the new workflow
+ migrate_packages(&old.home)?;
+
+ // Remove the package inventory directory, as we no longer cache package tarballs
+ remove_dir_if_exists(old.home.package_inventory_dir())?;
+
+ // Complete the migration, writing the V3 layout file
+ let layout = V3::complete_migration(new_home)?;
+
+ // Remove the V2 layout file, since we're now on V3 (do this after writing the V3 file so that we know the migration succeeded)
+ remove_file_if_exists(old.home.layout_file())?;
+
+ Ok(layout)
+ }
+}
+
+fn migrate_packages(old_home: &v2::VoltaHome) -> Fallible<()> {
+ let packages = get_installed_packages(old_home);
+ let mut session = Session::init();
+
+ for package in packages {
+ migrate_single_package(package, &mut session)?;
+ }
+
+ Ok(())
+}
+
+/// Determine a list of all installed packages that are using the legacy package config
+fn get_installed_packages(old_home: &v2::VoltaHome) -> Vec<LegacyPackageConfig> {
+ WalkDir::new(old_home.default_package_dir())
+ .max_depth(2)
+ .into_iter()
+ .filter_map(|res| match res {
+ Ok(entry) => {
+ if entry.file_type().is_file() {
+ let config = LegacyPackageConfig::from_file(entry.path());
+
+ // If unable to parse the config file and this isn't an already-migrated
+ // package, then show debug information and a warning for the user.
+ if config.is_none() && !is_migrated_config(entry.path()) {
+ debug!("Unable to parse config file: {}", entry.path().display());
+ if let Some(name) = entry.path().file_stem() {
+ let name = name.to_string_lossy();
+ warn!(
+ "Could not migrate {}. Please run `volta install {0}` to migrate the package manually.",
+ name
+ );
+ }
+ }
+
+ config
+ } else {
+ None
+ }
+ }
+ Err(error) => {
+ debug!("Error reading directory entry: {}", error);
+ None
+ }
+ })
+ .collect()
+}
+
+/// Determine if a package has already been migrated by attempting to read the V3 PackageConfig
+fn is_migrated_config(config_path: &Path) -> bool {
+ PackageConfig::from_file(config_path).is_ok()
+}
+
+/// Migrate a single package to the new workflow
+///
+/// Note: This relies on the package install logic in `volta_core`. If that logic changes, then
+/// the end result may not be a valid V3 layout any more, and this migration will need to be
+/// updated. Specifically, the invariants we rely on are:
+///
+/// - Package image directory is in the same location
+/// - Package config files are in the same location and the same format
+/// - Binary config files are in the same location and the same format
+///
+/// If any of those are violated, this migration may be invalid and need to be reworked / scrapped
+fn migrate_single_package(config: LegacyPackageConfig, session: &mut Session) -> Fallible<()> {
+ let tool = Package::new(config.name, VersionSpec::Exact(config.version))?;
+
+ let platform: PlatformSpec = config.platform.into();
+ let image = platform.as_binary().checkout(session)?;
+
+ // Run the global install command
+ tool.run_install(&image)?;
+ // Overwrite the config files and image directory
+ tool.complete_install(&image)?;
+
+ Ok(())
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +
use std::fs::File;
+use std::path::Path;
+
+use node_semver::Version;
+use volta_core::platform::PlatformSpec;
+use volta_core::version::{option_version_serde, version_serde};
+
+#[derive(serde::Deserialize)]
+pub struct LegacyPackageConfig {
+ pub name: String,
+ #[serde(with = "version_serde")]
+ pub version: Version,
+ pub platform: LegacyPlatform,
+ pub bins: Vec<String>,
+}
+
+#[derive(serde::Deserialize)]
+pub struct LegacyPlatform {
+ pub node: NodeVersion,
+ #[serde(with = "option_version_serde")]
+ pub yarn: Option<Version>,
+}
+
+#[derive(serde::Deserialize)]
+pub struct NodeVersion {
+ #[serde(with = "version_serde")]
+ pub runtime: Version,
+ #[serde(with = "option_version_serde")]
+ pub npm: Option<Version>,
+}
+
+impl LegacyPackageConfig {
+ pub fn from_file(config_file: &Path) -> Option<Self> {
+ let file = File::open(config_file).ok()?;
+
+ serde_json::from_reader(file).ok()
+ }
+}
+
+impl From<LegacyPlatform> for PlatformSpec {
+ fn from(config_platform: LegacyPlatform) -> Self {
+ PlatformSpec {
+ node: config_platform.node.runtime,
+ npm: config_platform.node.npm,
+ // LegacyPlatform (layout.v2) doesn't have a pnpm field
+ pnpm: None,
+ yarn: config_platform.yarn,
+ }
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +
use std::fs::File;
+use std::path::PathBuf;
+
+use super::empty::Empty;
+use super::v3::V3;
+use log::debug;
+use volta_core::error::{Context, ErrorKind, Fallible, VoltaError};
+#[cfg(windows)]
+use volta_core::fs::read_dir_eager;
+use volta_core::fs::remove_file_if_exists;
+use volta_layout::v4;
+
+/// Represents a V4 Volta Layout (used by Volta v2.0.0 and above)
+///
+/// Holds a reference to the V4 layout struct to support potential future migrations
+pub struct V4 {
+ pub home: v4::VoltaHome,
+}
+
+impl V4 {
+ pub fn new(home: PathBuf) -> Self {
+ V4 {
+ home: v4::VoltaHome::new(home),
+ }
+ }
+
+ /// Write the layout file to mark migration to V4 as complete
+ ///
+ /// Should only be called once all other migration steps are finished, so that we don't
+ /// accidentally mark an incomplete migration as completed
+ fn complete_migration(home: v4::VoltaHome) -> Fallible<Self> {
+ debug!("Writing layout marker file");
+ File::create(home.layout_file()).with_context(|| ErrorKind::CreateLayoutFileError {
+ file: home.layout_file().to_owned(),
+ })?;
+
+ Ok(V4 { home })
+ }
+}
+
+impl TryFrom<Empty> for V4 {
+ type Error = VoltaError;
+
+ fn try_from(old: Empty) -> Fallible<V4> {
+ debug!("New Volta installation detected, creating fresh layout");
+
+ let home = v4::VoltaHome::new(old.home);
+ home.create().with_context(|| ErrorKind::CreateDirError {
+ dir: home.root().to_owned(),
+ })?;
+
+ V4::complete_migration(home)
+ }
+}
+
+impl TryFrom<V3> for V4 {
+ type Error = VoltaError;
+
+ fn try_from(old: V3) -> Fallible<V4> {
+ debug!("Migrating from V3 layout");
+
+ let new_home = v4::VoltaHome::new(old.home.root().to_owned());
+ new_home
+ .create()
+ .with_context(|| ErrorKind::CreateDirError {
+ dir: new_home.root().to_owned(),
+ })?;
+
+ // Perform the core of the migration
+ #[cfg(windows)]
+ {
+ migrate_shims(&new_home)?;
+ migrate_shared_directory(&new_home)?;
+ }
+
+ // Complete the migration, writing the V4 layout file
+ let layout = V4::complete_migration(new_home)?;
+
+ // Remove the V3 layout file, since we're now on V4 (do this after writing the V4 so that we know the migration succeeded)
+ let old_layout_file = old.home.layout_file();
+ remove_file_if_exists(old_layout_file)?;
+ Ok(layout)
+ }
+}
+
+/// Migrate Windows shims to use the new non-symlink approach. Previously, shims were created in
+/// the same way as on Unix: With symlinks to the `volta-shim` executable. Now, we use scripts that
+/// call `volta run` to execute the underlying tool. This allows us to avoid needing developer
+/// mode, making Volta more broadly usable for Windows devs.
+///
+/// To migrate the shims, we read the shim directory looking for symlinks, remove those, and then
+/// file stem (name without extension) to generate new shims.
+#[cfg(windows)]
+fn migrate_shims(new_home: &v4::VoltaHome) -> Fallible<()> {
+ use std::ffi::OsStr;
+
+ let entries = read_dir_eager(new_home.shim_dir()).with_context(|| ErrorKind::ReadDirError {
+ dir: new_home.shim_dir().to_owned(),
+ })?;
+
+ for (entry, metadata) in entries {
+ if metadata.is_symlink() {
+ let path = entry.path();
+ remove_file_if_exists(&path)?;
+
+ if let Some(shim_name) = path.file_stem().and_then(OsStr::to_str) {
+ volta_core::shim::create(shim_name)?;
+ }
+ }
+ }
+
+ Ok(())
+}
+
+/// Migrate Windows shared directory to use junctions rather than directory symlinks. Similar to
+/// the shims, we previously used symlinks to create the shared global package directory, which
+/// requires developer mode. By using junctions, we can avoid that requirement entirely.
+///
+/// To migrate the directories, we read the shim directory, determine the target of each symlink,
+/// delete the link, and then create a junction (using volta_core::fs::symlink_dir which delegates
+/// to `junction` internally)
+#[cfg(windows)]
+fn migrate_shared_directory(new_home: &v4::VoltaHome) -> Fallible<()> {
+ use std::fs::read_link;
+ use volta_core::fs::{remove_dir_if_exists, symlink_dir};
+
+ let entries =
+ read_dir_eager(new_home.shared_lib_root()).with_context(|| ErrorKind::ReadDirError {
+ dir: new_home.shared_lib_root().to_owned(),
+ })?;
+
+ for (entry, metadata) in entries {
+ if metadata.is_symlink() {
+ let path = entry.path();
+ let source = read_link(&path).with_context(|| ErrorKind::ReadDirError {
+ dir: new_home.shared_lib_root().to_owned(),
+ })?;
+
+ remove_dir_if_exists(&path)?;
+ symlink_dir(source, path).with_context(|| ErrorKind::CreateSharedLinkError {
+ name: entry.file_name().to_string_lossy().to_string(),
+ })?;
+ }
+ }
+
+ Ok(())
+}
+
use volta_core::error::{report_error, ExitCode};
+use volta_core::layout::volta_home;
+use volta_core::log::{LogContext, LogVerbosity, Logger};
+use volta_migrate::run_migration;
+
+pub fn main() {
+ Logger::init(LogContext::Migration, LogVerbosity::Default)
+ .expect("Only a single Logger should be initialized");
+
+ // In order to migrate the existing Volta directory while avoiding unconditional changes to the user's system,
+ // the Homebrew formula runs volta-migrate with `--no-create` flag in the post-install phase.
+ let no_create = matches!(std::env::args_os().nth(1), Some(flag) if flag == "--no-create");
+ if no_create && volta_home().map_or(true, |home| !home.root().exists()) {
+ ExitCode::Success.exit();
+ }
+
+ let exit_code = match run_migration() {
+ Ok(()) => ExitCode::Success,
+ Err(err) => {
+ report_error(env!("CARGO_PKG_VERSION"), &err);
+ err.exit_code()
+ }
+ };
+
+ exit_code.exit();
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +
use std::process::{Command, ExitStatus};
+
+use volta_core::error::{Context, ErrorKind, VoltaError};
+use volta_core::layout::{volta_home, volta_install};
+
+pub enum Error {
+ Volta(VoltaError),
+ Tool(i32),
+}
+
+pub fn ensure_layout() -> Result<(), Error> {
+ let home = volta_home().map_err(Error::Volta)?;
+
+ if !home.layout_file().exists() {
+ let install = volta_install().map_err(Error::Volta)?;
+ Command::new(install.migrate_executable())
+ .env("VOLTA_LOGLEVEL", format!("{}", log::max_level()))
+ .status()
+ .with_context(|| ErrorKind::CouldNotStartMigration)
+ .into_result()?;
+ }
+
+ Ok(())
+}
+
+pub trait IntoResult<T> {
+ fn into_result(self) -> Result<T, Error>;
+}
+
+impl IntoResult<()> for Result<ExitStatus, VoltaError> {
+ fn into_result(self) -> Result<(), Error> {
+ match self {
+ Ok(status) => {
+ if status.success() {
+ Ok(())
+ } else {
+ let code = status.code().unwrap_or(1);
+ Err(Error::Tool(code))
+ }
+ }
+ Err(err) => Err(Error::Volta(err)),
+ }
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +
mod common;
+
+use common::{ensure_layout, Error, IntoResult};
+use volta_core::error::{report_error, ExitCode};
+use volta_core::log::{LogContext, LogVerbosity, Logger};
+use volta_core::run::execute_shim;
+use volta_core::session::{ActivityKind, Session};
+use volta_core::signal::setup_signal_handler;
+
+pub fn main() {
+ Logger::init(LogContext::Shim, LogVerbosity::Default)
+ .expect("Only a single Logger should be initialized");
+ setup_signal_handler();
+
+ let mut session = Session::init();
+ session.add_event_start(ActivityKind::Tool);
+
+ let result = ensure_layout().and_then(|()| execute_shim(&mut session).into_result());
+ match result {
+ Ok(()) => {
+ session.add_event_end(ActivityKind::Tool, ExitCode::Success);
+ session.exit(ExitCode::Success);
+ }
+ Err(Error::Tool(code)) => {
+ session.add_event_tool_end(ActivityKind::Tool, code);
+ session.exit_tool(code);
+ }
+ Err(Error::Volta(err)) => {
+ report_error(env!("CARGO_PKG_VERSION"), &err);
+ session.add_event_error(ActivityKind::Tool, &err);
+ session.add_event_end(ActivityKind::Tool, err.exit_code());
+ session.exit(ExitCode::ExecutionFailure);
+ }
+ }
+}
+
fn:
) to \
+ restrict the search to a given item kind.","Accepted kinds are: fn
, mod
, struct
, \
+ enum
, trait
, type
, macro
, \
+ and const
.","Search functions by type signature (e.g., vec -> usize
or \
+ -> vec
or String, enum:Cow -> bool
)","You can look for items with an exact name by putting double quotes around \
+ your request: \"string\"
","Look for functions that accept or return \
+ slices and \
+ arrays by writing \
+ square brackets (e.g., -> [u8]
or [] -> Option
)","Look for items inside another one by searching for a path: vec::Vec
",].map(x=>""+x+"
").join("");const div_infos=document.createElement("div");addClass(div_infos,"infos");div_infos.innerHTML="${value.replaceAll(" ", " ")}
`}else{error[index]=value}});output+=`Utilities to use with acceptance tests in Volta.
+Redirecting to macro.ok_or_panic.html...
+ + + \ No newline at end of file diff --git a/main/test_support/macro.ok_or_panic.html b/main/test_support/macro.ok_or_panic.html new file mode 100644 index 000000000..78f90ef83 --- /dev/null +++ b/main/test_support/macro.ok_or_panic.html @@ -0,0 +1,3 @@ +macro_rules! ok_or_panic { + { $e:expr } => { ... }; +}
enum MatchKind {
+ Exact,
+ Partial,
+ PartialN(usize),
+ NotPresent,
+ Unordered,
+}
key
and return true
if they are equal.pub fn execs() -> Execs
fn find_mismatch<'a>(
+ expected: &'a Value,
+ actual: &'a Value
+) -> Option<(&'a Value, &'a Value)>
pub fn lines_match(expected: &str, actual: &str) -> bool
Compare a line with an expected pattern.
+[..]
as a wildcard to match 0 or more characters on the same line
+(similar to .*
in a regex).[EXE]
to optionally add .exe
on Windows (empty string on other
+platforms).[COMPILING]
or [WARNING]
)
+to match cargo’s “status” output and allows you to ignore the alignment.
+See substitute_macros
for a complete list of macros.fn substitute_macros(input: &str) -> String
pub struct Execs {
+ expect_stdout: Option<String>,
+ expect_stderr: Option<String>,
+ expect_exit_code: Option<i32>,
+ expect_stdout_contains: Vec<String>,
+ expect_stderr_contains: Vec<String>,
+ expect_either_contains: Vec<String>,
+ expect_stdout_contains_n: Vec<(String, usize)>,
+ expect_stdout_not_contains: Vec<String>,
+ expect_stderr_not_contains: Vec<String>,
+ expect_stderr_unordered: Vec<String>,
+ expect_neither_contains: Vec<String>,
+ expect_json: Option<Vec<Value>>,
+}
expect_stdout: Option<String>
§expect_stderr: Option<String>
§expect_exit_code: Option<i32>
§expect_stdout_contains: Vec<String>
§expect_stderr_contains: Vec<String>
§expect_either_contains: Vec<String>
§expect_stdout_contains_n: Vec<(String, usize)>
§expect_stdout_not_contains: Vec<String>
§expect_stderr_not_contains: Vec<String>
§expect_stderr_unordered: Vec<String>
§expect_neither_contains: Vec<String>
§expect_json: Option<Vec<Value>>
Verify that stdout is equal to the given lines.
+See lines_match
for supported patterns.
Verify that stderr is equal to the given lines.
+See lines_match
for supported patterns.
Verify the exit code from the process.
+Verify that stdout contains the given contiguous lines somewhere in
+its output.
+See lines_match
for supported patterns.
Verify that stderr contains the given contiguous lines somewhere in
+its output.
+See lines_match
for supported patterns.
Verify that either stdout or stderr contains the given contiguous
+lines somewhere in its output.
+See lines_match
for supported patterns.
Verify that stdout contains the given contiguous lines somewhere in
+its output, and should be repeated number
times.
+See lines_match
for supported patterns.
Verify that stdout does not contain the given contiguous lines.
+See lines_match
for supported patterns.
+See note on with_stderr_does_not_contain
.
Verify that stderr does not contain the given contiguous lines.
+See lines_match
for supported patterns.
Care should be taken when using this method because there is a +limitless number of possible things that won’t appear. A typo means +your test will pass without verifying the correct behavior. If +possible, write the test first so that it fails, and then implement +your fix/feature to make it pass.
+Verify that all of the stderr output is equal to the given lines,
+ignoring the order of the lines.
+See lines_match
for supported patterns.
+This is useful when checking the output of cargo build -v
since
+the order of the output is not always deterministic.
+Recommend use with_stderr_contains
instead unless you really want to
+check every line of output.
Be careful when using patterns such as [..]
, because you may end up
+with multiple lines that might match, and this is not smart enough to
+do anything like longest-match. For example, avoid something like:
+[RUNNING] rustc [..] [RUNNING]
rustc –crate-name foo [..]
+This will randomly fail if the other crate name is bar
, and the
+order changes.
Verify the JSON output matches the given JSON.
+Typically used when testing cargo commands that emit JSON.
+Each separate JSON object should be separated by a blank line.
+Example:
+assert_that(
+p.cargo(“metadata”),
+execs().with_json(r#“
+{“example”: “abc”}
+{“example”: “def”}
+“#)
+);
+Objects should match in the order given.
+The order of arrays is ignored.
+Strings support patterns described in lines_match
.
+Use {...}
to match any object.
struct ZipAll<I1: Iterator, I2: Iterator> {
+ first: I1,
+ second: I2,
+}
first: I1
§second: I2
iter_next_chunk
)N
values. Read moreiter_advance_by
)n
elements. Read moren
th element of the iterator. Read moreiter_intersperse
)separator
+between adjacent items of the original iterator. Read moren
elements. Read moren
elements, or fewer
+if the underlying iterator ends sooner. Read moreiter_map_windows
)f
for each contiguous window of size N
over
+self
and returns an iterator over the outputs of f
. Like slice::windows()
,
+the windows during mapping overlap as well. Read moreiter_collect_into
)iter_is_partitioned
)true
precede all those that return false
. Read moreiterator_try_reduce
)try_find
)iter_array_chunks
)N
elements of the iterator at a time. Read moreiter_order_by
)Iterator
with those
+of another with respect to the specified comparison function. Read morePartialOrd
elements of
+this Iterator
with those of another. The comparison works like short-circuit
+evaluation, returning a result without comparing the remaining elements.
+As soon as an order can be determined, the evaluation stops and a result is returned. Read moreiter_order_by
)Iterator
with those
+of another with respect to the specified comparison function. Read moreiter_order_by
)Iterator
are lexicographically
+less than those of another. Read moreIterator
are lexicographically
+less or equal to those of another. Read moreIterator
are lexicographically
+greater than those of another. Read moreIterator
are lexicographically
+greater than or equal to those of another. Read moreis_sorted
)is_sorted
)enum Remove {
+ File,
+ Dir,
+}
fn global_root() -> PathBuf
pub fn home() -> PathBuf
fn init()
pub fn root() -> PathBuf
static NEXT_ID: AtomicUsize
static SMOKE_TEST_DIR: &str
pub trait PathExt {
+ // Required methods
+ fn rm(&self);
+ fn rm_rf(&self);
+ fn rm_contents(&self);
+ fn ensure_empty(&self);
+ fn mkdir_p(&self);
+}
pub fn process<T: AsRef<OsStr>>(cmd: T) -> ProcessBuilder
A helper function to create a ProcessBuilder
.
pub fn process_error(
+ msg: &str,
+ status: Option<ExitStatus>,
+ output: Option<&Output>
+) -> ProcessError
std::process::Command
.ProcessBuilder
.pub struct ProcessBuilder {
+ program: OsString,
+ args: Vec<OsString>,
+ env: HashMap<String, Option<OsString>>,
+ cwd: Option<OsString>,
+}
A builder object for an external process, similar to std::process::Command
.
program: OsString
The program to execute.
+args: Vec<OsString>
A list of arguments to pass to the program.
+env: HashMap<String, Option<OsString>>
Any environment variables that should be set for the program.
+cwd: Option<OsString>
Which directory to run the program from.
+(chainable) Set the executable for the process.
+(chainable) Add an arg to the args list.
+(chainable) Add many args to the args list.
+(chainable) Replace args with new args list
+(chainable) Set the current working directory of the process
+(chainable) Set an environment variable for the process.
+(chainable) Unset an environment variable for the process.
+Get the executable name.
+Get an environment variable as the process will see it (will inherit from environment +unless explicitally unset).
+Get all environment variables explicitly set or unset for the process (not inherited +vars).
+Run the process, waiting for completion, and mapping non-success exit codes to an error.
+Execute the process, returning the stdio output, or an error if non-zero exit status.
+Converts ProcessBuilder into a std::process::Command
source
. Read morepub struct ProcessError {
+ pub desc: String,
+ pub exit: Option<ExitStatus>,
+ pub output: Option<Output>,
+}
desc: String
§exit: Option<ExitStatus>
§output: Option<Output>
iter_advance_by
)n
elements. Read moren
th element of the iterator. Read moreiter_next_chunk
)N
values. Read moreiter_intersperse
)separator
\nbetween adjacent items of the original iterator. Read moren
elements. Read moren
elements, or fewer\nif the underlying iterator ends sooner. Read moreiter_map_windows
)f
for each contiguous window of size N
over\nself
and returns an iterator over the outputs of f
. Like slice::windows()
,\nthe windows during mapping overlap as well. Read moreiter_collect_into
)iter_is_partitioned
)true
precede all those that return false
. Read moreiterator_try_reduce
)try_find
)iter_array_chunks
)N
elements of the iterator at a time. Read moreiter_order_by
)Iterator
with those\nof another with respect to the specified comparison function. Read morePartialOrd
elements of\nthis Iterator
with those of another. The comparison works like short-circuit\nevaluation, returning a result without comparing the remaining elements.\nAs soon as an order can be determined, the evaluation stops and a result is returned. Read moreiter_order_by
)Iterator
with those\nof another with respect to the specified comparison function. Read moreiter_order_by
)Iterator
are lexicographically\nless than those of another. Read moreIterator
are lexicographically\nless or equal to those of another. Read moreIterator
are lexicographically\ngreater than those of another. Read moreIterator
are lexicographically\ngreater than or equal to those of another. Read moreis_sorted
)is_sorted
)iter_advance_by
)n
elements. Read moren
th element from the end of the iterator. Read moreCreates a Chain
from the default values for A
and B
.
struct Foo<'a>(Chain<slice::Iter<'a, u8>, btree_set::Iter<'a, u8>>);\n\nlet set = BTreeSet::<u8>::new();\nlet slice: &[u8] = &[];\nlet mut foo = Foo(slice.iter().chain(set.iter()));\n\n// take requires `Default`\nlet _: Chain<_, _> = mem::take(&mut foo.0);
Returns true
if the result is Ok
and the value inside of it matches a predicate.
let x: Result<u32, &str> = Ok(2);\nassert_eq!(x.is_ok_and(|x| x > 1), true);\n\nlet x: Result<u32, &str> = Ok(0);\nassert_eq!(x.is_ok_and(|x| x > 1), false);\n\nlet x: Result<u32, &str> = Err("hey");\nassert_eq!(x.is_ok_and(|x| x > 1), false);
Returns true
if the result is Err
and the value inside of it matches a predicate.
use std::io::{Error, ErrorKind};\n\nlet x: Result<u32, Error> = Err(Error::new(ErrorKind::NotFound, "!"));\nassert_eq!(x.is_err_and(|x| x.kind() == ErrorKind::NotFound), true);\n\nlet x: Result<u32, Error> = Err(Error::new(ErrorKind::PermissionDenied, "!"));\nassert_eq!(x.is_err_and(|x| x.kind() == ErrorKind::NotFound), false);\n\nlet x: Result<u32, Error> = Ok(123);\nassert_eq!(x.is_err_and(|x| x.kind() == ErrorKind::NotFound), false);
Converts from Result<T, E>
to Option<E>
.
Converts self
into an Option<E>
, consuming self
,\nand discarding the success value, if any.
let x: Result<u32, &str> = Ok(2);\nassert_eq!(x.err(), None);\n\nlet x: Result<u32, &str> = Err("Nothing here");\nassert_eq!(x.err(), Some("Nothing here"));
Converts from &Result<T, E>
to Result<&T, &E>
.
Produces a new Result
, containing a reference\ninto the original, leaving the original in place.
let x: Result<u32, &str> = Ok(2);\nassert_eq!(x.as_ref(), Ok(&2));\n\nlet x: Result<u32, &str> = Err("Error");\nassert_eq!(x.as_ref(), Err(&"Error"));
Converts from &mut Result<T, E>
to Result<&mut T, &mut E>
.
fn mutate(r: &mut Result<i32, i32>) {\n match r.as_mut() {\n Ok(v) => *v = 42,\n Err(e) => *e = 0,\n }\n}\n\nlet mut x: Result<i32, i32> = Ok(2);\nmutate(&mut x);\nassert_eq!(x.unwrap(), 42);\n\nlet mut x: Result<i32, i32> = Err(13);\nmutate(&mut x);\nassert_eq!(x.unwrap_err(), 0);
Maps a Result<T, E>
to Result<U, E>
by applying a function to a\ncontained Ok
value, leaving an Err
value untouched.
This function can be used to compose the results of two functions.
\nPrint the numbers on each line of a string multiplied by two.
\n\nlet line = "1\\n2\\n3\\n4\\n";\n\nfor num in line.lines() {\n match num.parse::<i32>().map(|i| i * 2) {\n Ok(n) => println!("{n}"),\n Err(..) => {}\n }\n}
Returns the provided default (if Err
), or\napplies a function to the contained value (if Ok
).
Arguments passed to map_or
are eagerly evaluated; if you are passing\nthe result of a function call, it is recommended to use map_or_else
,\nwhich is lazily evaluated.
let x: Result<_, &str> = Ok("foo");\nassert_eq!(x.map_or(42, |v| v.len()), 3);\n\nlet x: Result<&str, _> = Err("bar");\nassert_eq!(x.map_or(42, |v| v.len()), 42);
Maps a Result<T, E>
to U
by applying fallback function default
to\na contained Err
value, or function f
to a contained Ok
value.
This function can be used to unpack a successful result\nwhile handling an error.
\nlet k = 21;\n\nlet x : Result<_, &str> = Ok("foo");\nassert_eq!(x.map_or_else(|e| k * 2, |v| v.len()), 3);\n\nlet x : Result<&str, _> = Err("bar");\nassert_eq!(x.map_or_else(|e| k * 2, |v| v.len()), 42);
Maps a Result<T, E>
to Result<T, F>
by applying a function to a\ncontained Err
value, leaving an Ok
value untouched.
This function can be used to pass through a successful result while handling\nan error.
\nfn stringify(x: u32) -> String { format!("error code: {x}") }\n\nlet x: Result<u32, u32> = Ok(2);\nassert_eq!(x.map_err(stringify), Ok(2));\n\nlet x: Result<u32, u32> = Err(13);\nassert_eq!(x.map_err(stringify), Err("error code: 13".to_string()));
result_option_inspect
)result_option_inspect
)Converts from Result<T, E>
(or &Result<T, E>
) to Result<&<T as Deref>::Target, &E>
.
Coerces the Ok
variant of the original Result
via Deref
\nand returns the new Result
.
let x: Result<String, u32> = Ok("hello".to_string());\nlet y: Result<&str, &u32> = Ok("hello");\nassert_eq!(x.as_deref(), y);\n\nlet x: Result<String, u32> = Err(42);\nlet y: Result<&str, &u32> = Err(&42);\nassert_eq!(x.as_deref(), y);
Converts from Result<T, E>
(or &mut Result<T, E>
) to Result<&mut <T as DerefMut>::Target, &mut E>
.
Coerces the Ok
variant of the original Result
via DerefMut
\nand returns the new Result
.
let mut s = "HELLO".to_string();\nlet mut x: Result<String, u32> = Ok("hello".to_string());\nlet y: Result<&mut str, &mut u32> = Ok(&mut s);\nassert_eq!(x.as_deref_mut().map(|x| { x.make_ascii_uppercase(); x }), y);\n\nlet mut i = 42;\nlet mut x: Result<String, u32> = Err(42);\nlet y: Result<&mut str, &mut u32> = Err(&mut i);\nassert_eq!(x.as_deref_mut().map(|x| { x.make_ascii_uppercase(); x }), y);
Returns an iterator over the possibly contained value.
\nThe iterator yields one value if the result is Result::Ok
, otherwise none.
let x: Result<u32, &str> = Ok(7);\nassert_eq!(x.iter().next(), Some(&7));\n\nlet x: Result<u32, &str> = Err("nothing!");\nassert_eq!(x.iter().next(), None);
Returns a mutable iterator over the possibly contained value.
\nThe iterator yields one value if the result is Result::Ok
, otherwise none.
let mut x: Result<u32, &str> = Ok(7);\nmatch x.iter_mut().next() {\n Some(v) => *v = 40,\n None => {},\n}\nassert_eq!(x, Ok(40));\n\nlet mut x: Result<u32, &str> = Err("nothing!");\nassert_eq!(x.iter_mut().next(), None);
Returns the contained Ok
value, consuming the self
value.
Because this function may panic, its use is generally discouraged.\nInstead, prefer to use pattern matching and handle the Err
\ncase explicitly, or call unwrap_or
, unwrap_or_else
, or\nunwrap_or_default
.
Panics if the value is an Err
, with a panic message including the\npassed message, and the content of the Err
.
let x: Result<u32, &str> = Err("emergency failure");\nx.expect("Testing expect"); // panics with `Testing expect: emergency failure`
We recommend that expect
messages are used to describe the reason you\nexpect the Result
should be Ok
.
let path = std::env::var("IMPORTANT_PATH")\n .expect("env variable `IMPORTANT_PATH` should be set by `wrapper_script.sh`");
Hint: If you’re having trouble remembering how to phrase expect\nerror messages remember to focus on the word “should” as in “env\nvariable should be set by blah” or “the given binary should be available\nand executable by the current user”.
\nFor more detail on expect message styles and the reasoning behind our recommendation please\nrefer to the section on “Common Message\nStyles” in the\nstd::error
module docs.
Returns the contained Ok
value, consuming the self
value.
Because this function may panic, its use is generally discouraged.\nInstead, prefer to use pattern matching and handle the Err
\ncase explicitly, or call unwrap_or
, unwrap_or_else
, or\nunwrap_or_default
.
Panics if the value is an Err
, with a panic message provided by the\nErr
’s value.
Basic usage:
\n\nlet x: Result<u32, &str> = Ok(2);\nassert_eq!(x.unwrap(), 2);
let x: Result<u32, &str> = Err("emergency failure");\nx.unwrap(); // panics with `emergency failure`
Returns the contained Ok
value or a default
Consumes the self
argument then, if Ok
, returns the contained\nvalue, otherwise if Err
, returns the default value for that\ntype.
Converts a string to an integer, turning poorly-formed strings\ninto 0 (the default value for integers). parse
converts\na string to any other type that implements FromStr
, returning an\nErr
on error.
let good_year_from_input = "1909";\nlet bad_year_from_input = "190blarg";\nlet good_year = good_year_from_input.parse().unwrap_or_default();\nlet bad_year = bad_year_from_input.parse().unwrap_or_default();\n\nassert_eq!(1909, good_year);\nassert_eq!(0, bad_year);
Returns the contained Err
value, consuming the self
value.
Panics if the value is an Ok
, with a panic message including the\npassed message, and the content of the Ok
.
let x: Result<u32, &str> = Ok(10);\nx.expect_err("Testing expect_err"); // panics with `Testing expect_err: 10`
Returns the contained Err
value, consuming the self
value.
Panics if the value is an Ok
, with a custom panic message provided\nby the Ok
’s value.
let x: Result<u32, &str> = Ok(2);\nx.unwrap_err(); // panics with `2`
let x: Result<u32, &str> = Err("emergency failure");\nassert_eq!(x.unwrap_err(), "emergency failure");
unwrap_infallible
)Returns the contained Ok
value, but never panics.
Unlike unwrap
, this method is known to never panic on the\nresult types it is implemented for. Therefore, it can be used\ninstead of unwrap
as a maintainability safeguard that will fail\nto compile if the error type of the Result
is later changed\nto an error that can actually occur.
\nfn only_good_news() -> Result<String, !> {\n Ok("this is fine".into())\n}\n\nlet s: String = only_good_news().into_ok();\nprintln!("{s}");
unwrap_infallible
)Returns the contained Err
value, but never panics.
Unlike unwrap_err
, this method is known to never panic on the\nresult types it is implemented for. Therefore, it can be used\ninstead of unwrap_err
as a maintainability safeguard that will fail\nto compile if the ok type of the Result
is later changed\nto a type that can actually occur.
\nfn only_bad_news() -> Result<!, String> {\n Err("Oops, it failed".into())\n}\n\nlet error: String = only_bad_news().into_err();\nprintln!("{error}");
Returns res
if the result is Ok
, otherwise returns the Err
value of self
.
Arguments passed to and
are eagerly evaluated; if you are passing the\nresult of a function call, it is recommended to use and_then
, which is\nlazily evaluated.
let x: Result<u32, &str> = Ok(2);\nlet y: Result<&str, &str> = Err("late error");\nassert_eq!(x.and(y), Err("late error"));\n\nlet x: Result<u32, &str> = Err("early error");\nlet y: Result<&str, &str> = Ok("foo");\nassert_eq!(x.and(y), Err("early error"));\n\nlet x: Result<u32, &str> = Err("not a 2");\nlet y: Result<&str, &str> = Err("late error");\nassert_eq!(x.and(y), Err("not a 2"));\n\nlet x: Result<u32, &str> = Ok(2);\nlet y: Result<&str, &str> = Ok("different result type");\nassert_eq!(x.and(y), Ok("different result type"));
Calls op
if the result is Ok
, otherwise returns the Err
value of self
.
This function can be used for control flow based on Result
values.
fn sq_then_to_string(x: u32) -> Result<String, &'static str> {\n x.checked_mul(x).map(|sq| sq.to_string()).ok_or("overflowed")\n}\n\nassert_eq!(Ok(2).and_then(sq_then_to_string), Ok(4.to_string()));\nassert_eq!(Ok(1_000_000).and_then(sq_then_to_string), Err("overflowed"));\nassert_eq!(Err("not a number").and_then(sq_then_to_string), Err("not a number"));
Often used to chain fallible operations that may return Err
.
use std::{io::ErrorKind, path::Path};\n\n// Note: on Windows "/" maps to "C:\\"\nlet root_modified_time = Path::new("/").metadata().and_then(|md| md.modified());\nassert!(root_modified_time.is_ok());\n\nlet should_fail = Path::new("/bad/path").metadata().and_then(|md| md.modified());\nassert!(should_fail.is_err());\nassert_eq!(should_fail.unwrap_err().kind(), ErrorKind::NotFound);
Returns res
if the result is Err
, otherwise returns the Ok
value of self
.
Arguments passed to or
are eagerly evaluated; if you are passing the\nresult of a function call, it is recommended to use or_else
, which is\nlazily evaluated.
let x: Result<u32, &str> = Ok(2);\nlet y: Result<u32, &str> = Err("late error");\nassert_eq!(x.or(y), Ok(2));\n\nlet x: Result<u32, &str> = Err("early error");\nlet y: Result<u32, &str> = Ok(2);\nassert_eq!(x.or(y), Ok(2));\n\nlet x: Result<u32, &str> = Err("not a 2");\nlet y: Result<u32, &str> = Err("late error");\nassert_eq!(x.or(y), Err("late error"));\n\nlet x: Result<u32, &str> = Ok(2);\nlet y: Result<u32, &str> = Ok(100);\nassert_eq!(x.or(y), Ok(2));
Calls op
if the result is Err
, otherwise returns the Ok
value of self
.
This function can be used for control flow based on result values.
\nfn sq(x: u32) -> Result<u32, u32> { Ok(x * x) }\nfn err(x: u32) -> Result<u32, u32> { Err(x) }\n\nassert_eq!(Ok(2).or_else(sq).or_else(sq), Ok(2));\nassert_eq!(Ok(2).or_else(err).or_else(sq), Ok(2));\nassert_eq!(Err(3).or_else(sq).or_else(err), Ok(9));\nassert_eq!(Err(3).or_else(err).or_else(err), Err(3));
Returns the contained Ok
value or a provided default.
Arguments passed to unwrap_or
are eagerly evaluated; if you are passing\nthe result of a function call, it is recommended to use unwrap_or_else
,\nwhich is lazily evaluated.
let default = 2;\nlet x: Result<u32, &str> = Ok(9);\nassert_eq!(x.unwrap_or(default), 9);\n\nlet x: Result<u32, &str> = Err("error");\nassert_eq!(x.unwrap_or(default), default);
Returns the contained Ok
value, consuming the self
value,\nwithout checking that the value is not an Err
.
Calling this method on an Err
is undefined behavior.
let x: Result<u32, &str> = Ok(2);\nassert_eq!(unsafe { x.unwrap_unchecked() }, 2);
let x: Result<u32, &str> = Err("emergency failure");\nunsafe { x.unwrap_unchecked(); } // Undefined behavior!
Returns the contained Err
value, consuming the self
value,\nwithout checking that the value is not an Ok
.
Calling this method on an Ok
is undefined behavior.
let x: Result<u32, &str> = Ok(2);\nunsafe { x.unwrap_err_unchecked() }; // Undefined behavior!
let x: Result<u32, &str> = Err("emergency failure");\nassert_eq!(unsafe { x.unwrap_err_unchecked() }, "emergency failure");
Maps a Result<&mut T, E>
to a Result<T, E>
by copying the contents of the\nOk
part.
let mut val = 12;\nlet x: Result<&mut i32, i32> = Ok(&mut val);\nassert_eq!(x, Ok(&mut 12));\nlet copied = x.copied();\nassert_eq!(copied, Ok(12));
Maps a Result<&mut T, E>
to a Result<T, E>
by cloning the contents of the\nOk
part.
let mut val = 12;\nlet x: Result<&mut i32, i32> = Ok(&mut val);\nassert_eq!(x, Ok(&mut 12));\nlet cloned = x.cloned();\nassert_eq!(cloned, Ok(12));
Transposes a Result
of an Option
into an Option
of a Result
.
Ok(None)
will be mapped to None
.\nOk(Some(_))
and Err(_)
will be mapped to Some(Ok(_))
and Some(Err(_))
.
#[derive(Debug, Eq, PartialEq)]\nstruct SomeErr;\n\nlet x: Result<Option<i32>, SomeErr> = Ok(Some(5));\nlet y: Option<Result<i32, SomeErr>> = Some(Ok(5));\nassert_eq!(x.transpose(), y);
result_flattening
)Converts from Result<Result<T, E>, E>
to Result<T, E>
#![feature(result_flattening)]\nlet x: Result<Result<&'static str, u32>, u32> = Ok(Ok("hello"));\nassert_eq!(Ok("hello"), x.flatten());\n\nlet x: Result<Result<&'static str, u32>, u32> = Ok(Err(6));\nassert_eq!(Err(6), x.flatten());\n\nlet x: Result<Result<&'static str, u32>, u32> = Err(6);\nassert_eq!(Err(6), x.flatten());
Flattening only removes one level of nesting at a time:
\n\n#![feature(result_flattening)]\nlet x: Result<Result<Result<&'static str, u32>, u32>, u32> = Ok(Ok(Ok("hello")));\nassert_eq!(Ok(Ok("hello")), x.flatten());\nassert_eq!(Ok("hello"), x.flatten().flatten());
try_trait_v2
)Residual
type. Read moretry_trait_v2
)?
when not short-circuiting.try_trait_v2
)FromResidual::from_residual
\nas part of ?
when short-circuiting. Read moretry_trait_v2
)Output
type. Read moretry_trait_v2
)?
to decide whether the operator should produce a value\n(because this returned ControlFlow::Continue
)\nor propagate a value back to the caller\n(because this returned ControlFlow::Break
). Read moreTakes each element in the Iterator
: if it is an Err
, no further\nelements are taken, and the Err
is returned. Should no Err
occur, a\ncontainer with the values of each Result
is returned.
Here is an example which increments every integer in a vector,\nchecking for overflow:
\n\nlet v = vec![1, 2];\nlet res: Result<Vec<u32>, &'static str> = v.iter().map(|x: &u32|\n x.checked_add(1).ok_or("Overflow!")\n).collect();\nassert_eq!(res, Ok(vec![2, 3]));
Here is another example that tries to subtract one from another list\nof integers, this time checking for underflow:
\n\nlet v = vec![1, 2, 0];\nlet res: Result<Vec<u32>, &'static str> = v.iter().map(|x: &u32|\n x.checked_sub(1).ok_or("Underflow!")\n).collect();\nassert_eq!(res, Err("Underflow!"));
Here is a variation on the previous example, showing that no\nfurther elements are taken from iter
after the first Err
.
let v = vec![3, 2, 1, 10];\nlet mut shared = 0;\nlet res: Result<Vec<u32>, &'static str> = v.iter().map(|x: &u32| {\n shared += x;\n x.checked_sub(2).ok_or("Underflow!")\n}).collect();\nassert_eq!(res, Err("Underflow!"));\nassert_eq!(shared, 6);
Since the third element caused an underflow, no further elements were taken,\nso the final value of shared
is 6 (= 3 + 2 + 1
), not 16.
Returns a consuming iterator over the possibly contained value.
\nThe iterator yields one value if the result is Result::Ok
, otherwise none.
let x: Result<u32, &str> = Ok(5);\nlet v: Vec<u32> = x.into_iter().collect();\nassert_eq!(v, [5]);\n\nlet x: Result<u32, &str> = Err("nothing!");\nlet v: Vec<u32> = x.into_iter().collect();\nassert_eq!(v, []);
Takes each element in the Iterator
: if it is an Err
, no further\nelements are taken, and the Err
is returned. Should no Err
\noccur, the sum of all elements is returned.
This sums up every integer in a vector, rejecting the sum if a negative\nelement is encountered:
\n\nlet f = |&x: &i32| if x < 0 { Err("Negative element found") } else { Ok(x) };\nlet v = vec![1, 2];\nlet res: Result<i32, _> = v.iter().map(f).sum();\nassert_eq!(res, Ok(3));\nlet v = vec![1, -2];\nlet res: Result<i32, _> = v.iter().map(f).sum();\nassert_eq!(res, Err("Negative element found"));
self
and other
) and is used by the <=
\noperator. Read moreTakes each element in the Iterator
: if it is an Err
, no further\nelements are taken, and the Err
is returned. Should no Err
\noccur, the product of all elements is returned.
This multiplies each number in a vector of strings,\nif a string could not be parsed the operation returns Err
:
let nums = vec!["5", "10", "1", "2"];\nlet total: Result<usize, _> = nums.iter().map(|w| w.parse::<usize>()).product();\nassert_eq!(total, Ok(100));\nlet nums = vec!["5", "10", "one", "2"];\nlet total: Result<usize, _> = nums.iter().map(|w| w.parse::<usize>()).product();\nassert!(total.is_err());
Result
types that return regular std::error::Error
s\ninto a Result
that returns a [Diagnostic
].Returns true
if the result is Ok
and the value inside of it matches a predicate.
let x: Result<u32, &str> = Ok(2);\nassert_eq!(x.is_ok_and(|x| x > 1), true);\n\nlet x: Result<u32, &str> = Ok(0);\nassert_eq!(x.is_ok_and(|x| x > 1), false);\n\nlet x: Result<u32, &str> = Err("hey");\nassert_eq!(x.is_ok_and(|x| x > 1), false);
Returns true
if the result is Err
and the value inside of it matches a predicate.
use std::io::{Error, ErrorKind};\n\nlet x: Result<u32, Error> = Err(Error::new(ErrorKind::NotFound, "!"));\nassert_eq!(x.is_err_and(|x| x.kind() == ErrorKind::NotFound), true);\n\nlet x: Result<u32, Error> = Err(Error::new(ErrorKind::PermissionDenied, "!"));\nassert_eq!(x.is_err_and(|x| x.kind() == ErrorKind::NotFound), false);\n\nlet x: Result<u32, Error> = Ok(123);\nassert_eq!(x.is_err_and(|x| x.kind() == ErrorKind::NotFound), false);
Converts from Result<T, E>
to Option<E>
.
Converts self
into an Option<E>
, consuming self
,\nand discarding the success value, if any.
let x: Result<u32, &str> = Ok(2);\nassert_eq!(x.err(), None);\n\nlet x: Result<u32, &str> = Err("Nothing here");\nassert_eq!(x.err(), Some("Nothing here"));
Converts from &Result<T, E>
to Result<&T, &E>
.
Produces a new Result
, containing a reference\ninto the original, leaving the original in place.
let x: Result<u32, &str> = Ok(2);\nassert_eq!(x.as_ref(), Ok(&2));\n\nlet x: Result<u32, &str> = Err("Error");\nassert_eq!(x.as_ref(), Err(&"Error"));
Converts from &mut Result<T, E>
to Result<&mut T, &mut E>
.
fn mutate(r: &mut Result<i32, i32>) {\n match r.as_mut() {\n Ok(v) => *v = 42,\n Err(e) => *e = 0,\n }\n}\n\nlet mut x: Result<i32, i32> = Ok(2);\nmutate(&mut x);\nassert_eq!(x.unwrap(), 42);\n\nlet mut x: Result<i32, i32> = Err(13);\nmutate(&mut x);\nassert_eq!(x.unwrap_err(), 0);
Maps a Result<T, E>
to Result<U, E>
by applying a function to a\ncontained Ok
value, leaving an Err
value untouched.
This function can be used to compose the results of two functions.
\nPrint the numbers on each line of a string multiplied by two.
\n\nlet line = "1\\n2\\n3\\n4\\n";\n\nfor num in line.lines() {\n match num.parse::<i32>().map(|i| i * 2) {\n Ok(n) => println!("{n}"),\n Err(..) => {}\n }\n}
Returns the provided default (if Err
), or\napplies a function to the contained value (if Ok
).
Arguments passed to map_or
are eagerly evaluated; if you are passing\nthe result of a function call, it is recommended to use map_or_else
,\nwhich is lazily evaluated.
let x: Result<_, &str> = Ok("foo");\nassert_eq!(x.map_or(42, |v| v.len()), 3);\n\nlet x: Result<&str, _> = Err("bar");\nassert_eq!(x.map_or(42, |v| v.len()), 42);
Maps a Result<T, E>
to U
by applying fallback function default
to\na contained Err
value, or function f
to a contained Ok
value.
This function can be used to unpack a successful result\nwhile handling an error.
\nlet k = 21;\n\nlet x : Result<_, &str> = Ok("foo");\nassert_eq!(x.map_or_else(|e| k * 2, |v| v.len()), 3);\n\nlet x : Result<&str, _> = Err("bar");\nassert_eq!(x.map_or_else(|e| k * 2, |v| v.len()), 42);
Maps a Result<T, E>
to Result<T, F>
by applying a function to a\ncontained Err
value, leaving an Ok
value untouched.
This function can be used to pass through a successful result while handling\nan error.
\nfn stringify(x: u32) -> String { format!("error code: {x}") }\n\nlet x: Result<u32, u32> = Ok(2);\nassert_eq!(x.map_err(stringify), Ok(2));\n\nlet x: Result<u32, u32> = Err(13);\nassert_eq!(x.map_err(stringify), Err("error code: 13".to_string()));
result_option_inspect
)result_option_inspect
)Converts from Result<T, E>
(or &Result<T, E>
) to Result<&<T as Deref>::Target, &E>
.
Coerces the Ok
variant of the original Result
via Deref
\nand returns the new Result
.
let x: Result<String, u32> = Ok("hello".to_string());\nlet y: Result<&str, &u32> = Ok("hello");\nassert_eq!(x.as_deref(), y);\n\nlet x: Result<String, u32> = Err(42);\nlet y: Result<&str, &u32> = Err(&42);\nassert_eq!(x.as_deref(), y);
Converts from Result<T, E>
(or &mut Result<T, E>
) to Result<&mut <T as DerefMut>::Target, &mut E>
.
Coerces the Ok
variant of the original Result
via DerefMut
\nand returns the new Result
.
let mut s = "HELLO".to_string();\nlet mut x: Result<String, u32> = Ok("hello".to_string());\nlet y: Result<&mut str, &mut u32> = Ok(&mut s);\nassert_eq!(x.as_deref_mut().map(|x| { x.make_ascii_uppercase(); x }), y);\n\nlet mut i = 42;\nlet mut x: Result<String, u32> = Err(42);\nlet y: Result<&mut str, &mut u32> = Err(&mut i);\nassert_eq!(x.as_deref_mut().map(|x| { x.make_ascii_uppercase(); x }), y);
Returns an iterator over the possibly contained value.
\nThe iterator yields one value if the result is Result::Ok
, otherwise none.
let x: Result<u32, &str> = Ok(7);\nassert_eq!(x.iter().next(), Some(&7));\n\nlet x: Result<u32, &str> = Err("nothing!");\nassert_eq!(x.iter().next(), None);
Returns a mutable iterator over the possibly contained value.
\nThe iterator yields one value if the result is Result::Ok
, otherwise none.
let mut x: Result<u32, &str> = Ok(7);\nmatch x.iter_mut().next() {\n Some(v) => *v = 40,\n None => {},\n}\nassert_eq!(x, Ok(40));\n\nlet mut x: Result<u32, &str> = Err("nothing!");\nassert_eq!(x.iter_mut().next(), None);
Returns the contained Ok
value, consuming the self
value.
Because this function may panic, its use is generally discouraged.\nInstead, prefer to use pattern matching and handle the Err
\ncase explicitly, or call unwrap_or
, unwrap_or_else
, or\nunwrap_or_default
.
Panics if the value is an Err
, with a panic message including the\npassed message, and the content of the Err
.
let x: Result<u32, &str> = Err("emergency failure");\nx.expect("Testing expect"); // panics with `Testing expect: emergency failure`
We recommend that expect
messages are used to describe the reason you\nexpect the Result
should be Ok
.
let path = std::env::var("IMPORTANT_PATH")\n .expect("env variable `IMPORTANT_PATH` should be set by `wrapper_script.sh`");
Hint: If you’re having trouble remembering how to phrase expect\nerror messages remember to focus on the word “should” as in “env\nvariable should be set by blah” or “the given binary should be available\nand executable by the current user”.
\nFor more detail on expect message styles and the reasoning behind our recommendation please\nrefer to the section on “Common Message\nStyles” in the\nstd::error
module docs.
Returns the contained Ok
value, consuming the self
value.
Because this function may panic, its use is generally discouraged.\nInstead, prefer to use pattern matching and handle the Err
\ncase explicitly, or call unwrap_or
, unwrap_or_else
, or\nunwrap_or_default
.
Panics if the value is an Err
, with a panic message provided by the\nErr
’s value.
Basic usage:
\n\nlet x: Result<u32, &str> = Ok(2);\nassert_eq!(x.unwrap(), 2);
let x: Result<u32, &str> = Err("emergency failure");\nx.unwrap(); // panics with `emergency failure`
Returns the contained Ok
value or a default
Consumes the self
argument then, if Ok
, returns the contained\nvalue, otherwise if Err
, returns the default value for that\ntype.
Converts a string to an integer, turning poorly-formed strings\ninto 0 (the default value for integers). parse
converts\na string to any other type that implements FromStr
, returning an\nErr
on error.
let good_year_from_input = "1909";\nlet bad_year_from_input = "190blarg";\nlet good_year = good_year_from_input.parse().unwrap_or_default();\nlet bad_year = bad_year_from_input.parse().unwrap_or_default();\n\nassert_eq!(1909, good_year);\nassert_eq!(0, bad_year);
Returns the contained Err
value, consuming the self
value.
Panics if the value is an Ok
, with a panic message including the\npassed message, and the content of the Ok
.
let x: Result<u32, &str> = Ok(10);\nx.expect_err("Testing expect_err"); // panics with `Testing expect_err: 10`
Returns the contained Err
value, consuming the self
value.
Panics if the value is an Ok
, with a custom panic message provided\nby the Ok
’s value.
let x: Result<u32, &str> = Ok(2);\nx.unwrap_err(); // panics with `2`
let x: Result<u32, &str> = Err("emergency failure");\nassert_eq!(x.unwrap_err(), "emergency failure");
unwrap_infallible
)Returns the contained Ok
value, but never panics.
Unlike unwrap
, this method is known to never panic on the\nresult types it is implemented for. Therefore, it can be used\ninstead of unwrap
as a maintainability safeguard that will fail\nto compile if the error type of the Result
is later changed\nto an error that can actually occur.
\nfn only_good_news() -> Result<String, !> {\n Ok("this is fine".into())\n}\n\nlet s: String = only_good_news().into_ok();\nprintln!("{s}");
unwrap_infallible
)Returns the contained Err
value, but never panics.
Unlike unwrap_err
, this method is known to never panic on the\nresult types it is implemented for. Therefore, it can be used\ninstead of unwrap_err
as a maintainability safeguard that will fail\nto compile if the ok type of the Result
is later changed\nto a type that can actually occur.
\nfn only_bad_news() -> Result<!, String> {\n Err("Oops, it failed".into())\n}\n\nlet error: String = only_bad_news().into_err();\nprintln!("{error}");
Returns res
if the result is Ok
, otherwise returns the Err
value of self
.
Arguments passed to and
are eagerly evaluated; if you are passing the\nresult of a function call, it is recommended to use and_then
, which is\nlazily evaluated.
let x: Result<u32, &str> = Ok(2);\nlet y: Result<&str, &str> = Err("late error");\nassert_eq!(x.and(y), Err("late error"));\n\nlet x: Result<u32, &str> = Err("early error");\nlet y: Result<&str, &str> = Ok("foo");\nassert_eq!(x.and(y), Err("early error"));\n\nlet x: Result<u32, &str> = Err("not a 2");\nlet y: Result<&str, &str> = Err("late error");\nassert_eq!(x.and(y), Err("not a 2"));\n\nlet x: Result<u32, &str> = Ok(2);\nlet y: Result<&str, &str> = Ok("different result type");\nassert_eq!(x.and(y), Ok("different result type"));
Calls op
if the result is Ok
, otherwise returns the Err
value of self
.
This function can be used for control flow based on Result
values.
fn sq_then_to_string(x: u32) -> Result<String, &'static str> {\n x.checked_mul(x).map(|sq| sq.to_string()).ok_or("overflowed")\n}\n\nassert_eq!(Ok(2).and_then(sq_then_to_string), Ok(4.to_string()));\nassert_eq!(Ok(1_000_000).and_then(sq_then_to_string), Err("overflowed"));\nassert_eq!(Err("not a number").and_then(sq_then_to_string), Err("not a number"));
Often used to chain fallible operations that may return Err
.
use std::{io::ErrorKind, path::Path};\n\n// Note: on Windows "/" maps to "C:\\"\nlet root_modified_time = Path::new("/").metadata().and_then(|md| md.modified());\nassert!(root_modified_time.is_ok());\n\nlet should_fail = Path::new("/bad/path").metadata().and_then(|md| md.modified());\nassert!(should_fail.is_err());\nassert_eq!(should_fail.unwrap_err().kind(), ErrorKind::NotFound);
Returns res
if the result is Err
, otherwise returns the Ok
value of self
.
Arguments passed to or
are eagerly evaluated; if you are passing the\nresult of a function call, it is recommended to use or_else
, which is\nlazily evaluated.
let x: Result<u32, &str> = Ok(2);\nlet y: Result<u32, &str> = Err("late error");\nassert_eq!(x.or(y), Ok(2));\n\nlet x: Result<u32, &str> = Err("early error");\nlet y: Result<u32, &str> = Ok(2);\nassert_eq!(x.or(y), Ok(2));\n\nlet x: Result<u32, &str> = Err("not a 2");\nlet y: Result<u32, &str> = Err("late error");\nassert_eq!(x.or(y), Err("late error"));\n\nlet x: Result<u32, &str> = Ok(2);\nlet y: Result<u32, &str> = Ok(100);\nassert_eq!(x.or(y), Ok(2));
Calls op
if the result is Err
, otherwise returns the Ok
value of self
.
This function can be used for control flow based on result values.
\nfn sq(x: u32) -> Result<u32, u32> { Ok(x * x) }\nfn err(x: u32) -> Result<u32, u32> { Err(x) }\n\nassert_eq!(Ok(2).or_else(sq).or_else(sq), Ok(2));\nassert_eq!(Ok(2).or_else(err).or_else(sq), Ok(2));\nassert_eq!(Err(3).or_else(sq).or_else(err), Ok(9));\nassert_eq!(Err(3).or_else(err).or_else(err), Err(3));
Returns the contained Ok
value or a provided default.
Arguments passed to unwrap_or
are eagerly evaluated; if you are passing\nthe result of a function call, it is recommended to use unwrap_or_else
,\nwhich is lazily evaluated.
let default = 2;\nlet x: Result<u32, &str> = Ok(9);\nassert_eq!(x.unwrap_or(default), 9);\n\nlet x: Result<u32, &str> = Err("error");\nassert_eq!(x.unwrap_or(default), default);
Returns the contained Ok
value, consuming the self
value,\nwithout checking that the value is not an Err
.
Calling this method on an Err
is undefined behavior.
let x: Result<u32, &str> = Ok(2);\nassert_eq!(unsafe { x.unwrap_unchecked() }, 2);
let x: Result<u32, &str> = Err("emergency failure");\nunsafe { x.unwrap_unchecked(); } // Undefined behavior!
Returns the contained Err
value, consuming the self
value,\nwithout checking that the value is not an Ok
.
Calling this method on an Ok
is undefined behavior.
let x: Result<u32, &str> = Ok(2);\nunsafe { x.unwrap_err_unchecked() }; // Undefined behavior!
let x: Result<u32, &str> = Err("emergency failure");\nassert_eq!(unsafe { x.unwrap_err_unchecked() }, "emergency failure");
Maps a Result<&mut T, E>
to a Result<T, E>
by copying the contents of the\nOk
part.
let mut val = 12;\nlet x: Result<&mut i32, i32> = Ok(&mut val);\nassert_eq!(x, Ok(&mut 12));\nlet copied = x.copied();\nassert_eq!(copied, Ok(12));
Maps a Result<&mut T, E>
to a Result<T, E>
by cloning the contents of the\nOk
part.
let mut val = 12;\nlet x: Result<&mut i32, i32> = Ok(&mut val);\nassert_eq!(x, Ok(&mut 12));\nlet cloned = x.cloned();\nassert_eq!(cloned, Ok(12));
Transposes a Result
of an Option
into an Option
of a Result
.
Ok(None)
will be mapped to None
.\nOk(Some(_))
and Err(_)
will be mapped to Some(Ok(_))
and Some(Err(_))
.
#[derive(Debug, Eq, PartialEq)]\nstruct SomeErr;\n\nlet x: Result<Option<i32>, SomeErr> = Ok(Some(5));\nlet y: Option<Result<i32, SomeErr>> = Some(Ok(5));\nassert_eq!(x.transpose(), y);
result_flattening
)Converts from Result<Result<T, E>, E>
to Result<T, E>
#![feature(result_flattening)]\nlet x: Result<Result<&'static str, u32>, u32> = Ok(Ok("hello"));\nassert_eq!(Ok("hello"), x.flatten());\n\nlet x: Result<Result<&'static str, u32>, u32> = Ok(Err(6));\nassert_eq!(Err(6), x.flatten());\n\nlet x: Result<Result<&'static str, u32>, u32> = Err(6);\nassert_eq!(Err(6), x.flatten());
Flattening only removes one level of nesting at a time:
\n\n#![feature(result_flattening)]\nlet x: Result<Result<Result<&'static str, u32>, u32>, u32> = Ok(Ok(Ok("hello")));\nassert_eq!(Ok(Ok("hello")), x.flatten());\nassert_eq!(Ok("hello"), x.flatten().flatten());
try_trait_v2
)Residual
type. Read moretry_trait_v2
)?
when not short-circuiting.try_trait_v2
)FromResidual::from_residual
\nas part of ?
when short-circuiting. Read moretry_trait_v2
)Output
type. Read moretry_trait_v2
)?
to decide whether the operator should produce a value\n(because this returned ControlFlow::Continue
)\nor propagate a value back to the caller\n(because this returned ControlFlow::Break
). Read moreTakes each element in the Iterator
: if it is an Err
, no further\nelements are taken, and the Err
is returned. Should no Err
occur, a\ncontainer with the values of each Result
is returned.
Here is an example which increments every integer in a vector,\nchecking for overflow:
\n\nlet v = vec![1, 2];\nlet res: Result<Vec<u32>, &'static str> = v.iter().map(|x: &u32|\n x.checked_add(1).ok_or("Overflow!")\n).collect();\nassert_eq!(res, Ok(vec![2, 3]));
Here is another example that tries to subtract one from another list\nof integers, this time checking for underflow:
\n\nlet v = vec![1, 2, 0];\nlet res: Result<Vec<u32>, &'static str> = v.iter().map(|x: &u32|\n x.checked_sub(1).ok_or("Underflow!")\n).collect();\nassert_eq!(res, Err("Underflow!"));
Here is a variation on the previous example, showing that no\nfurther elements are taken from iter
after the first Err
.
let v = vec![3, 2, 1, 10];\nlet mut shared = 0;\nlet res: Result<Vec<u32>, &'static str> = v.iter().map(|x: &u32| {\n shared += x;\n x.checked_sub(2).ok_or("Underflow!")\n}).collect();\nassert_eq!(res, Err("Underflow!"));\nassert_eq!(shared, 6);
Since the third element caused an underflow, no further elements were taken,\nso the final value of shared
is 6 (= 3 + 2 + 1
), not 16.
Returns a consuming iterator over the possibly contained value.
\nThe iterator yields one value if the result is Result::Ok
, otherwise none.
let x: Result<u32, &str> = Ok(5);\nlet v: Vec<u32> = x.into_iter().collect();\nassert_eq!(v, [5]);\n\nlet x: Result<u32, &str> = Err("nothing!");\nlet v: Vec<u32> = x.into_iter().collect();\nassert_eq!(v, []);
Takes each element in the Iterator
: if it is an Err
, no further\nelements are taken, and the Err
is returned. Should no Err
\noccur, the sum of all elements is returned.
This sums up every integer in a vector, rejecting the sum if a negative\nelement is encountered:
\n\nlet f = |&x: &i32| if x < 0 { Err("Negative element found") } else { Ok(x) };\nlet v = vec![1, 2];\nlet res: Result<i32, _> = v.iter().map(f).sum();\nassert_eq!(res, Ok(3));\nlet v = vec![1, -2];\nlet res: Result<i32, _> = v.iter().map(f).sum();\nassert_eq!(res, Err("Negative element found"));
self
and other
) and is used by the <=
\noperator. Read moreTakes each element in the Iterator
: if it is an Err
, no further\nelements are taken, and the Err
is returned. Should no Err
\noccur, the product of all elements is returned.
This multiplies each number in a vector of strings,\nif a string could not be parsed the operation returns Err
:
let nums = vec!["5", "10", "1", "2"];\nlet total: Result<usize, _> = nums.iter().map(|w| w.parse::<usize>()).product();\nassert_eq!(total, Ok(100));\nlet nums = vec!["5", "10", "one", "2"];\nlet total: Result<usize, _> = nums.iter().map(|w| w.parse::<usize>()).product();\nassert!(total.is_err());
pub(crate) const BLACKLIST: [&str; 2];
pub(crate) const BUILTINS: [&str; 39];
pub enum Validity {
+ Valid,
+ ValidForOldPackages {
+ warnings: Vec<String>,
+ },
+ Invalid {
+ warnings: Vec<String>,
+ errors: Vec<String>,
+ },
+}
Valid for new and old packages
+Valid only for old packages
+Not valid for new or old packages
+pub fn validate(name: &str) -> Validity
A Rust implementation of the validation rules from the core JS package
+validate-npm-package-name
.
encodeURIComponent
pub(crate) static ENCODE_URI_SET: &AsciiSet
The set of characters to encode, matching the characters encoded by
+encodeURIComponent
pub(crate) static SCOPED_PACKAGE: Lazy<Regex>
pub(crate) static SPECIAL_CHARS: Lazy<Regex>
pub(crate) enum Subcommand {
+ Fetch(Fetch),
+ Install(Install),
+ Uninstall(Uninstall),
+ Pin(Pin),
+ List(List),
+ Completions(Completions),
+ Which(Which),
+ Use(Use),
+ Setup(Setup),
+ Run(Run),
+}
Fetches a tool to the local machine
+Installs a tool in your toolchain
+Uninstalls a tool from your toolchain
+Pins your project’s runtime or package manager
+Displays the current toolchain
+Generates Volta completions
+By default, completions will be generated for the value of your current shell,
+shell, i.e. the value of SHELL
. If you set the <shell>
option, completions
+will be generated for that shell instead.
If you specify a directory, the completions will be written to a file there;
+otherwise, they will be written to stdout
.
Locates the actual binary that will be called by Volta
+Enables Volta for the current user / shell
+Run a command with custom Node, npm, pnpm, and/or Yarn versions
+ArgMatches
to self
.ArgMatches
to self
.Command
] so it can instantiate Self
via
+[FromArgMatches::from_arg_matches_mut
] Read moreCommand
] so it can instantiate self
via
+[FromArgMatches::update_from_arg_matches_mut
] Read moreSelf
can parse a specific subcommandpub(crate) struct Volta {
+ pub(crate) command: Option<Subcommand>,
+ pub(crate) verbose: bool,
+ pub(crate) very_verbose: bool,
+ pub(crate) quiet: bool,
+ pub(crate) version: bool,
+}
command: Option<Subcommand>
§verbose: bool
Enables verbose diagnostics
+very_verbose: bool
Enables trace-level diagnostics.
+quiet: bool
Prevents unnecessary output
+version: bool
Prints the current version of Volta
+ArgGroup::id
][crate::ArgGroup::id] for this set of argumentsCommand
] so it can instantiate Self
via
+[FromArgMatches::from_arg_matches_mut
] Read moreCommand
] so it can instantiate self
via
+[FromArgMatches::update_from_arg_matches_mut
] Read moreArgMatches
to self
.ArgMatches
to self
.pub(crate) struct Completions {
+ shell: Shell,
+ out_file: Option<PathBuf>,
+ force: bool,
+}
shell: Shell
Shell to generate completions for
+out_file: Option<PathBuf>
File to write generated completions to
+force: bool
Write over an existing file, if any.
+ArgGroup::id
][crate::ArgGroup::id] for this set of argumentsCommand
] so it can instantiate Self
via
+[FromArgMatches::from_arg_matches_mut
] Read moreCommand
] so it can instantiate self
via
+[FromArgMatches::update_from_arg_matches_mut
] Read moreArgMatches
to self
.ArgMatches
to self
.pub(crate) struct Fetch {
+ tools: Vec<String>,
+}
tools: Vec<String>
Tools to fetch, like node
, yarn@latest
or your-package@^14.4.3
.
ArgGroup::id
][crate::ArgGroup::id] for this set of argumentsCommand
] so it can instantiate Self
via
+[FromArgMatches::from_arg_matches_mut
] Read moreCommand
] so it can instantiate self
via
+[FromArgMatches::update_from_arg_matches_mut
] Read moreArgMatches
to self
.ArgMatches
to self
.pub(crate) struct Install {
+ tools: Vec<String>,
+}
tools: Vec<String>
Tools to install, like node
, yarn@latest
or your-package@^14.4.3
.
ArgGroup::id
][crate::ArgGroup::id] for this set of argumentsCommand
] so it can instantiate Self
via
+[FromArgMatches::from_arg_matches_mut
] Read moreCommand
] so it can instantiate self
via
+[FromArgMatches::update_from_arg_matches_mut
] Read moreArgMatches
to self
.ArgMatches
to self
.enum Filter {
+ Current,
+ Default,
+ None,
+}
How (if at all) should the list query be narrowed?
+Display only the currently active tool(s).
+For example, if the user queries volta list --current yarn
, show only
+the version of Yarn currently in use: project, default, or none.
Show only the user’s default tool(s).
+For example, if the user queries volta list --default node
, show only
+the user’s default Node version.
Do not filter at all. Show all tool(s) matching the query.
+enum Format {
+ Human,
+ Plain,
+}
enum Package {
+ Default {
+ details: PackageDetails,
+ node: Version,
+ tools: Vec<String>,
+ },
+ Project {
+ name: String,
+ tools: Vec<String>,
+ path: PathBuf,
+ },
+ Fetched(PackageDetails),
+}
enum PackageManagerKind {
+ Npm,
+ Pnpm,
+ Yarn,
+}
source
. Read moreself
and other
values to be equal, and is used
+by ==
.self
and other
) and is used by the <=
+operator. Read morekey
and return true
if they are equal.key
and return true
if they are equal.enum Source {
+ Project(PathBuf),
+ Default,
+ None,
+}
The source of a given item, from the perspective of a user.
+Note: this is distinct from volta_core::platform::sourced::Source
, which
+represents the source only of a Platform
, which is a composite structure.
+By contrast, this Source
is concerned only with a single item.
The item is from a project. The wrapped PathBuf
is the path to the
+project’s package.json
.
The item is the user’s default.
+The item is one that has been fetched but is not installed anywhere.
+enum Subcommand {
+ All,
+ Node,
+ Npm,
+ Pnpm,
+ Yarn,
+ PackageOrTool {
+ name: String,
+ },
+}
Which tool should we look up?
+Show every item in the toolchain.
+Show locally cached Node versions.
+Show locally cached npm versions.
+Show locally cached pnpm versions.
+Show locally cached Yarn versions.
+Show locally cached versions of a package or a package binary.
+source
. Read morefn display_all(
+ runtimes: &[Node],
+ package_managers: &[PackageManager],
+ packages: &[Package]
+) -> String
Format the output for Toolchain::All
.
fn display_npms(managers: &[PackageManager]) -> String
Format a set of Toolchain::PackageManager
s for volta list npm
fn display_package_managers(
+ kind: PackageManagerKind,
+ managers: &[PackageManager]
+) -> String
Format a set of Toolchain::PackageManager
s.
fn format_package_manager(package_manager: &PackageManager) -> String
format a single Toolchain::PackageManager
.
fn format_package_manager_kind(kind: PackageManagerKind) -> String
format the title for a kind of package manager
+This is distinct from the Display
impl, because we need ‘Yarn’ to be capitalized for human output
fn format_package_manager_list_condensed(
+ package_managers: &[PackageManager]
+) -> String
format a list of Toolchain::PackageManager
s in condensed form
fn format_package_manager_list_verbose(
+ package_managers: &[PackageManager]
+) -> String
format a list of Toolchain::PackageManager
s in verbose form
Define the “human” format style for list commands.
+Toolchain::Active
.Toolchain::All
.Toolchain::Node
s.Toolchain::PackageManager
s for volta list npm
Toolchain::PackageManager
s.Toolchain::Package
s and their associated tools.Toolchain::Tool
with associated Toolchain::Package
Toolchain::Package
and its associated tools.Toolchain::Package
s and their associated tools.Toolchain::PackageManager
.Toolchain::PackageManager
s in condensed formToolchain::PackageManager
s in verbose formToolchain::Node
.Toolchain::Node
s.Toolchain::Package
without detail informationToolchain::Package
s without detail informationToolchain::Package
.fn describe_package_managers(
+ package_managers: &[PackageManager]
+) -> Option<String>
fn display_package_manager(package_manager: &PackageManager) -> String
pub(crate) struct List {
+ subcommand: Option<Subcommand>,
+ format: Option<Format>,
+ current: bool,
+ default: bool,
+}
subcommand: Option<Subcommand>
The tool to lookup - all
, node
, npm
, yarn
, pnpm
, or the name
+of a package or binary.
format: Option<Format>
Specify the output format.
+Defaults to human
for TTYs, plain
otherwise.
current: bool
Show the currently-active tool(s).
+Equivalent to volta list
when not specifying a specific tool.
default: bool
Show your default tool(s).
+ArgGroup::id
][crate::ArgGroup::id] for this set of argumentsCommand
] so it can instantiate Self
via
+[FromArgMatches::from_arg_matches_mut
] Read moreCommand
] so it can instantiate self
via
+[FromArgMatches::update_from_arg_matches_mut
] Read moreArgMatches
to self
.ArgMatches
to self
.struct Node {
+ pub source: Source,
+ pub version: Version,
+}
source: Source
§version: Version
struct PackageDetails {
+ pub name: String,
+ pub version: Version,
+}
A package and its associated tools, for displaying to the user as part of +their toolchain.
+name: String
The name of the package.
+version: Version
The package’s own version.
+struct PackageManager {
+ kind: PackageManagerKind,
+ source: Source,
+ version: Version,
+}
kind: PackageManagerKind
§source: Source
§version: Version
source
. Read moreenum Lookup {
+ Runtime,
+ Npm,
+ Pnpm,
+ Yarn,
+}
Lightweight rule for which item to get the Source
for.
Look up the Node runtime
+Look up the npm package manager
+Look up the pnpm package manager
+Look up the Yarn package manager
+Determine the Source
for a given kind of tool (Lookup
).
pub(super) enum Toolchain {
+ Node(Vec<Node>),
+ PackageManagers {
+ kind: PackageManagerKind,
+ managers: Vec<PackageManager>,
+ },
+ Packages(Vec<Package>),
+ Tool {
+ name: String,
+ host_packages: Vec<Package>,
+ },
+ Active {
+ runtime: Option<Box<Node>>,
+ package_managers: Vec<PackageManager>,
+ packages: Vec<Package>,
+ },
+ All {
+ runtimes: Vec<Node>,
+ package_managers: Vec<PackageManager>,
+ packages: Vec<Package>,
+ },
+}
pub(crate) struct Pin {
+ tools: Vec<String>,
+}
tools: Vec<String>
Tools to pin, like node@lts
or yarn@^1.14
.
ArgGroup::id
][crate::ArgGroup::id] for this set of argumentsCommand
] so it can instantiate Self
via
+[FromArgMatches::from_arg_matches_mut
] Read moreCommand
] so it can instantiate self
via
+[FromArgMatches::update_from_arg_matches_mut
] Read moreArgMatches
to self
.ArgMatches
to self
.pub(crate) struct Run {
+ node: Option<String>,
+ npm: Option<String>,
+ bundled_npm: bool,
+ pnpm: Option<String>,
+ no_pnpm: bool,
+ yarn: Option<String>,
+ no_yarn: bool,
+ envs: Vec<String>,
+ command_and_args: Vec<OsString>,
+}
node: Option<String>
Set the custom Node version
+npm: Option<String>
Set the custom npm version
+bundled_npm: bool
Forces npm to be the version bundled with Node
+pnpm: Option<String>
Set the custon pnpm version
+no_pnpm: bool
Disables pnpm
+yarn: Option<String>
Set the custom Yarn version
+no_yarn: bool
Disables Yarn
+envs: Vec<String>
Set an environment variable (can be used multiple times)
+command_and_args: Vec<OsString>
The command to run, along with any arguments
+Builds a CliPlatform from the provided cli options
+Will resolve a semver / tag version if necessary
+Convert the environment variable settings passed to the command line into a map
+We ignore any setting that doesn’t have a value associated with it +We also ignore the PATH environment variable as that is set when running a command
+ArgGroup::id
][crate::ArgGroup::id] for this set of argumentsCommand
] so it can instantiate Self
via
+[FromArgMatches::from_arg_matches_mut
] Read moreCommand
] so it can instantiate self
via
+[FromArgMatches::update_from_arg_matches_mut
] Read moreArgMatches
to self
.ArgMatches
to self
.fn add_bash_profiles(home_dir: &Path, shell: &str, profiles: &mut Vec<PathBuf>)
Add bash profile scripts, if necessary
+Note: We only add the bash scripts if they already exist, as creating new files can impact +the processing of existing files in bash (e.g. preventing ~/.profile from being loaded)
+pub(crate) struct Setup {}
ArgGroup::id
][crate::ArgGroup::id] for this set of argumentsCommand
] so it can instantiate Self
via
+[FromArgMatches::from_arg_matches_mut
] Read moreCommand
] so it can instantiate self
via
+[FromArgMatches::update_from_arg_matches_mut
] Read moreArgMatches
to self
.ArgMatches
to self
.pub(crate) struct Uninstall {
+ tool: String,
+}
tool: String
The tool to uninstall, like ember-cli-update
, typescript
, or
ArgGroup::id
][crate::ArgGroup::id] for this set of argumentsCommand
] so it can instantiate Self
via
+[FromArgMatches::from_arg_matches_mut
] Read moreCommand
] so it can instantiate self
via
+[FromArgMatches::update_from_arg_matches_mut
] Read moreArgMatches
to self
.ArgMatches
to self
.pub(crate) struct Use {
+ anything: Vec<String>,
+}
anything: Vec<String>
ArgGroup::id
][crate::ArgGroup::id] for this set of argumentsCommand
] so it can instantiate Self
via
+[FromArgMatches::from_arg_matches_mut
] Read moreCommand
] so it can instantiate self
via
+[FromArgMatches::update_from_arg_matches_mut
] Read moreArgMatches
to self
.ArgMatches
to self
.pub(crate) struct Which {
+ binary: OsString,
+}
binary: OsString
The binary to find, e.g. node
or npm
ArgGroup::id
][crate::ArgGroup::id] for this set of argumentsCommand
] so it can instantiate Self
via
+[FromArgMatches::from_arg_matches_mut
] Read moreCommand
] so it can instantiate self
via
+[FromArgMatches::update_from_arg_matches_mut
] Read moreArgMatches
to self
.ArgMatches
to self
.pub enum Error {
+ Volta(VoltaError),
+ Tool(i32),
+}
pub trait IntoResult<T> {
+ // Required method
+ fn into_result(self) -> Result<T, Error>;
+}
pub fn create_command<E>(exe: E) -> Commandwhere
+ E: AsRef<OsStr>,
pub(crate) const VOLTA_FEATURE_PNPM: &str = "VOLTA_FEATURE_PNPM";
pub enum ErrorKind {
+Show 114 variants
BinaryAlreadyInstalled {
+ bin_name: String,
+ existing_package: String,
+ new_package: String,
+ },
+ BinaryExecError,
+ BinaryNotFound {
+ name: String,
+ },
+ BuildPathError,
+ BypassError {
+ command: String,
+ },
+ CannotFetchPackage {
+ package: String,
+ },
+ CannotPinPackage {
+ package: String,
+ },
+ CompletionsOutFileError {
+ path: PathBuf,
+ },
+ ContainingDirError {
+ path: PathBuf,
+ },
+ CouldNotDetermineTool,
+ CouldNotStartMigration,
+ CreateDirError {
+ dir: PathBuf,
+ },
+ CreateLayoutFileError {
+ file: PathBuf,
+ },
+ CreateSharedLinkError {
+ name: String,
+ },
+ CreateTempDirError {
+ in_dir: PathBuf,
+ },
+ CreateTempFileError {
+ in_dir: PathBuf,
+ },
+ CurrentDirError,
+ DeleteDirectoryError {
+ directory: PathBuf,
+ },
+ DeleteFileError {
+ file: PathBuf,
+ },
+ DeprecatedCommandError {
+ command: String,
+ advice: String,
+ },
+ DownloadToolNetworkError {
+ tool: Spec,
+ from_url: String,
+ },
+ ExecuteHookError {
+ command: String,
+ },
+ ExtensionCycleError {
+ paths: Vec<PathBuf>,
+ duplicate: PathBuf,
+ },
+ ExtensionPathError {
+ path: PathBuf,
+ },
+ HookCommandFailed {
+ command: String,
+ },
+ HookMultipleFieldsSpecified,
+ HookNoFieldsSpecified,
+ HookPathError {
+ command: String,
+ },
+ InstalledPackageNameError,
+ InvalidHookCommand {
+ command: String,
+ },
+ InvalidHookOutput {
+ command: String,
+ },
+ InvalidInvocation {
+ action: String,
+ name: String,
+ version: String,
+ },
+ InvalidInvocationOfBareVersion {
+ action: String,
+ version: String,
+ },
+ InvalidRegistryFormat {
+ format: String,
+ },
+ InvalidToolName {
+ name: String,
+ errors: Vec<String>,
+ },
+ LockAcquireError,
+ NoBundledNpm {
+ command: String,
+ },
+ NoCommandLinePnpm,
+ NoCommandLineYarn,
+ NoDefaultNodeVersion {
+ tool: String,
+ },
+ NodeVersionNotFound {
+ matching: String,
+ },
+ NoHomeEnvironmentVar,
+ NoInstallDir,
+ NoLocalDataDir,
+ NoPinnedNodeVersion {
+ tool: String,
+ },
+ NoPlatform,
+ NoProjectNodeInManifest,
+ NoProjectYarn,
+ NoProjectPnpm,
+ NoShellProfile {
+ env_profile: String,
+ bin_dir: PathBuf,
+ },
+ NotInPackage,
+ NoDefaultYarn,
+ NoDefaultPnpm,
+ NpmLinkMissingPackage {
+ package: String,
+ },
+ NpmLinkWrongManager {
+ package: String,
+ },
+ NpmVersionNotFound {
+ matching: String,
+ },
+ NpxNotAvailable {
+ version: String,
+ },
+ PackageInstallFailed {
+ package: String,
+ },
+ PackageManifestParseError {
+ package: String,
+ },
+ PackageManifestReadError {
+ package: String,
+ },
+ PackageNotFound {
+ package: String,
+ },
+ PackageParseError {
+ file: PathBuf,
+ },
+ PackageReadError {
+ file: PathBuf,
+ },
+ PackageUnpackError,
+ PackageWriteError {
+ file: PathBuf,
+ },
+ ParseBinConfigError,
+ ParseHooksError {
+ file: PathBuf,
+ },
+ ParseNodeIndexCacheError,
+ ParseNodeIndexError {
+ from_url: String,
+ },
+ ParseNodeIndexExpiryError,
+ ParseNpmManifestError,
+ ParsePackageConfigError,
+ ParsePlatformError,
+ ParseToolSpecError {
+ tool_spec: String,
+ },
+ PersistInventoryError {
+ tool: String,
+ },
+ PnpmVersionNotFound {
+ matching: String,
+ },
+ ProjectLocalBinaryExecError {
+ command: String,
+ },
+ ProjectLocalBinaryNotFound {
+ command: String,
+ },
+ PublishHookBothUrlAndBin,
+ PublishHookNeitherUrlNorBin,
+ ReadBinConfigDirError {
+ dir: PathBuf,
+ },
+ ReadBinConfigError {
+ file: PathBuf,
+ },
+ ReadDefaultNpmError {
+ file: PathBuf,
+ },
+ ReadDirError {
+ dir: PathBuf,
+ },
+ ReadHooksError {
+ file: PathBuf,
+ },
+ ReadNodeIndexCacheError {
+ file: PathBuf,
+ },
+ ReadNodeIndexExpiryError {
+ file: PathBuf,
+ },
+ ReadNpmManifestError,
+ ReadPackageConfigError {
+ file: PathBuf,
+ },
+ ReadPlatformError {
+ file: PathBuf,
+ },
+ RegistryFetchError {
+ tool: String,
+ from_url: String,
+ },
+ RunShimDirectly,
+ SetToolExecutable {
+ tool: String,
+ },
+ SetupToolImageError {
+ tool: String,
+ version: String,
+ dir: PathBuf,
+ },
+ ShimCreateError {
+ name: String,
+ },
+ ShimRemoveError {
+ name: String,
+ },
+ StringifyBinConfigError,
+ StringifyPackageConfigError,
+ StringifyPlatformError,
+ Unimplemented {
+ feature: String,
+ },
+ UnpackArchiveError {
+ tool: String,
+ version: String,
+ },
+ UpgradePackageNotFound {
+ package: String,
+ manager: PackageManager,
+ },
+ UpgradePackageWrongManager {
+ package: String,
+ manager: PackageManager,
+ },
+ VersionParseError {
+ version: String,
+ },
+ WriteBinConfigError {
+ file: PathBuf,
+ },
+ WriteDefaultNpmError {
+ file: PathBuf,
+ },
+ WriteLauncherError {
+ tool: String,
+ },
+ WriteNodeIndexCacheError {
+ file: PathBuf,
+ },
+ WriteNodeIndexExpiryError {
+ file: PathBuf,
+ },
+ WritePackageConfigError {
+ file: PathBuf,
+ },
+ WritePlatformError {
+ file: PathBuf,
+ },
+ Yarn2NotSupported,
+ YarnLatestFetchError {
+ from_url: String,
+ },
+ YarnVersionNotFound {
+ matching: String,
+ },
+}
Thrown when package tries to install a binary that is already installed.
+Thrown when executing an external binary fails
+Thrown when a binary could not be found in the local inventory
+Thrown when building the virtual environment path fails
+Thrown when unable to launch a command with VOLTA_BYPASS set
+Thrown when a user tries to volta fetch
something other than node/yarn/npm.
Thrown when a user tries to volta pin
something other than node/yarn/npm.
Thrown when the Completions out-dir is not a directory
+Thrown when the containing directory could not be determined
+Thrown when unable to start the migration executable
+Thrown when unable to create the layout file
+Thrown when unable to create a link to the shared global library directory
+Thrown when creating a temporary directory fails
+Thrown when creating a temporary file fails
+Thrown when deleting a directory fails
+Thrown when deleting a file fails
+Thrown when unable to execute a hook command
+Thrown when volta.extends
keys result in an infinite cycle
Thrown when determining the path to an extension manifest fails
+Thrown when a hook command returns a non-zero exit code
+Thrown when a hook contains multiple fields (prefix, template, or bin)
+Thrown when a hook doesn’t contain any of the known fields (prefix, template, or bin)
+Thrown when determining the path to a hook fails
+Thrown when determining the name of a newly-installed package fails
+Thrown when output from a hook command could not be read
+Thrown when a user does e.g. volta install node 12
instead of
+volta install node@12
.
Thrown when a user does e.g. volta install 12
instead of
+volta install node@12
.
Thrown when a format other than “npm” or “github” is given for yarn.index in the hooks
+Thrown when a tool name is invalid per npm’s rules.
+Thrown when unable to acquire a lock on the Volta directory
+Thrown when pinning or installing npm@bundled and couldn’t detect the bundled version
+Thrown when pnpm is not set at the command-line
+Thrown when Yarn is not set at the command-line
+Thrown when a user tries to install a Yarn or npm version before installing a Node version.
+Thrown when there is no Node version matching a requested semver specifier.
+Thrown when the install dir could not be determined
+Thrown when a user tries to pin a npm, pnpm, or Yarn version before pinning a Node version.
+Thrown when the platform (Node version) could not be determined
+Thrown when parsing the project manifest and there is a "volta"
key without Node
Thrown when Yarn is not set in a project
+Thrown when pnpm is not set in a project
+Thrown when no shell profiles could be found
+Thrown when the user tries to pin Node or Yarn versions outside of a package.
+Thrown when default Yarn is not set
+Thrown when default pnpm is not set
+Thrown when npm link
is called with a package that isn’t available
Thrown when npm link
is called with a package that was not installed / linked with npm
Thrown when there is no npm version matching the requested Semver/Tag
+Thrown when the command to install a global package is not successful
+Thrown when parsing the package manifest fails
+Thrown when reading the package manifest fails
+Thrown when a specified package could not be found on the npm registry
+Thrown when parsing a package manifest fails
+Thrown when reading a package manifest fails
+Thrown when a package has been unpacked but is not formed correctly.
+Thrown when writing a package manifest fails
+Thrown when unable to parse a bin config file
+Thrown when unable to parse a hooks.json file
+Thrown when unable to parse the node index cache
+Thrown when unable to parse the node index
+Thrown when unable to parse the node index cache expiration
+Thrown when unable to parse the npm manifest file from a node install
+Thrown when unable to parse a package configuration
+Thrown when unable to parse the platform.json file
+Thrown when unable to parse a tool spec (<tool>[@<version>]
)
Thrown when persisting an archive to the inventory fails
+Thrown when there is no pnpm version matching a requested semver specifier.
+Thrown when executing a project-local binary fails
+Thrown when a project-local binary could not be found
+Thrown when a publish hook contains both the url and bin fields
+Thrown when a publish hook contains neither url nor bin fields
+Thrown when there was an error reading the user bin directory
+Thrown when there was an error reading the config for a binary
+Thrown when unable to read the default npm version file
+Thrown when unable to read the contents of a directory
+Thrown when there was an error opening a hooks.json file
+Thrown when there was an error reading the Node Index Cache
+Thrown when there was an error reading the Node Index Cache Expiration
+Thrown when there was an error reading the npm manifest file
+Thrown when there was an error reading a package configuration file
+Thrown when there was an error opening the user platform file
+Thrown when the public registry for Node or Yarn could not be downloaded.
+Thrown when the shim binary is called directly, not through a symlink
+Thrown when there was an error setting a tool to executable
+Thrown when there was an error copying an unpacked tool to the image directory
+Thrown when Volta is unable to create a shim
+Thrown when Volta is unable to remove a shim
+Thrown when serializing a bin config to JSON fails
+Thrown when serializing a package config to JSON fails
+Thrown when serializing the platform to JSON fails
+Thrown when a given feature has not yet been implemented
+Thrown when unpacking an archive (tarball or zip) fails
+Thrown when a package to upgrade was not found
+Thrown when a package to upgrade was installed with a different package manager
+Thrown when there was an error writing a bin config file
+Thrown when there was an error writing the default npm to file
+Thrown when there was an error writing the npm launcher
+Thrown when there was an error writing the node index cache
+Thrown when there was an error writing the node index expiration
+Thrown when there was an error writing a package config
+Thrown when writing the platform.json file fails
+Thrown when a user attempts to install a version of Yarn2
+Thrown when there is an error fetching the latest version of Yarn
+Thrown when there is no Yarn version matching a requested semver specifier.
+pub enum ExitCode {
+ Success = 0,
+ UnknownError = 1,
+ InvalidArguments = 3,
+ NoVersionMatch = 4,
+ NetworkError = 5,
+ EnvironmentError = 6,
+ FileSystemError = 7,
+ ConfigurationError = 8,
+ NotYetImplemented = 9,
+ ExecutionFailure = 126,
+ ExecutableNotFound = 127,
+}
Exit codes supported by Volta Errors
+No error occurred.
+An unknown error occurred.
+An invalid combination of command-line arguments was supplied.
+No match could be found for the requested version string.
+A network error occurred.
+A required environment variable was unset or invalid.
+A file could not be read or written.
+Package configuration is missing or incorrect.
+The command or feature is not yet implemented.
+The requested executable could not be run.
+The requested executable is not available.
+pub fn report_error(volta_version: &str, err: &VoltaError)
Report an error, both to the console and to error logs
+const PERMISSIONS_CTA: &str = "Please ensure you have correct permissions to the Volta directory.";
const REPORT_BUG_CTA: &str = "Please rerun the command that triggered this error with the environment
+variable `VOLTA_LOGLEVEL` set to `debug` and open an issue at
+https://github.com/volta-cli/volta/issues with the details!";
pub enum ErrorKind {
+Show 114 variants
BinaryAlreadyInstalled {
+ bin_name: String,
+ existing_package: String,
+ new_package: String,
+ },
+ BinaryExecError,
+ BinaryNotFound {
+ name: String,
+ },
+ BuildPathError,
+ BypassError {
+ command: String,
+ },
+ CannotFetchPackage {
+ package: String,
+ },
+ CannotPinPackage {
+ package: String,
+ },
+ CompletionsOutFileError {
+ path: PathBuf,
+ },
+ ContainingDirError {
+ path: PathBuf,
+ },
+ CouldNotDetermineTool,
+ CouldNotStartMigration,
+ CreateDirError {
+ dir: PathBuf,
+ },
+ CreateLayoutFileError {
+ file: PathBuf,
+ },
+ CreateSharedLinkError {
+ name: String,
+ },
+ CreateTempDirError {
+ in_dir: PathBuf,
+ },
+ CreateTempFileError {
+ in_dir: PathBuf,
+ },
+ CurrentDirError,
+ DeleteDirectoryError {
+ directory: PathBuf,
+ },
+ DeleteFileError {
+ file: PathBuf,
+ },
+ DeprecatedCommandError {
+ command: String,
+ advice: String,
+ },
+ DownloadToolNetworkError {
+ tool: Spec,
+ from_url: String,
+ },
+ ExecuteHookError {
+ command: String,
+ },
+ ExtensionCycleError {
+ paths: Vec<PathBuf>,
+ duplicate: PathBuf,
+ },
+ ExtensionPathError {
+ path: PathBuf,
+ },
+ HookCommandFailed {
+ command: String,
+ },
+ HookMultipleFieldsSpecified,
+ HookNoFieldsSpecified,
+ HookPathError {
+ command: String,
+ },
+ InstalledPackageNameError,
+ InvalidHookCommand {
+ command: String,
+ },
+ InvalidHookOutput {
+ command: String,
+ },
+ InvalidInvocation {
+ action: String,
+ name: String,
+ version: String,
+ },
+ InvalidInvocationOfBareVersion {
+ action: String,
+ version: String,
+ },
+ InvalidRegistryFormat {
+ format: String,
+ },
+ InvalidToolName {
+ name: String,
+ errors: Vec<String>,
+ },
+ LockAcquireError,
+ NoBundledNpm {
+ command: String,
+ },
+ NoCommandLinePnpm,
+ NoCommandLineYarn,
+ NoDefaultNodeVersion {
+ tool: String,
+ },
+ NodeVersionNotFound {
+ matching: String,
+ },
+ NoHomeEnvironmentVar,
+ NoInstallDir,
+ NoLocalDataDir,
+ NoPinnedNodeVersion {
+ tool: String,
+ },
+ NoPlatform,
+ NoProjectNodeInManifest,
+ NoProjectYarn,
+ NoProjectPnpm,
+ NoShellProfile {
+ env_profile: String,
+ bin_dir: PathBuf,
+ },
+ NotInPackage,
+ NoDefaultYarn,
+ NoDefaultPnpm,
+ NpmLinkMissingPackage {
+ package: String,
+ },
+ NpmLinkWrongManager {
+ package: String,
+ },
+ NpmVersionNotFound {
+ matching: String,
+ },
+ NpxNotAvailable {
+ version: String,
+ },
+ PackageInstallFailed {
+ package: String,
+ },
+ PackageManifestParseError {
+ package: String,
+ },
+ PackageManifestReadError {
+ package: String,
+ },
+ PackageNotFound {
+ package: String,
+ },
+ PackageParseError {
+ file: PathBuf,
+ },
+ PackageReadError {
+ file: PathBuf,
+ },
+ PackageUnpackError,
+ PackageWriteError {
+ file: PathBuf,
+ },
+ ParseBinConfigError,
+ ParseHooksError {
+ file: PathBuf,
+ },
+ ParseNodeIndexCacheError,
+ ParseNodeIndexError {
+ from_url: String,
+ },
+ ParseNodeIndexExpiryError,
+ ParseNpmManifestError,
+ ParsePackageConfigError,
+ ParsePlatformError,
+ ParseToolSpecError {
+ tool_spec: String,
+ },
+ PersistInventoryError {
+ tool: String,
+ },
+ PnpmVersionNotFound {
+ matching: String,
+ },
+ ProjectLocalBinaryExecError {
+ command: String,
+ },
+ ProjectLocalBinaryNotFound {
+ command: String,
+ },
+ PublishHookBothUrlAndBin,
+ PublishHookNeitherUrlNorBin,
+ ReadBinConfigDirError {
+ dir: PathBuf,
+ },
+ ReadBinConfigError {
+ file: PathBuf,
+ },
+ ReadDefaultNpmError {
+ file: PathBuf,
+ },
+ ReadDirError {
+ dir: PathBuf,
+ },
+ ReadHooksError {
+ file: PathBuf,
+ },
+ ReadNodeIndexCacheError {
+ file: PathBuf,
+ },
+ ReadNodeIndexExpiryError {
+ file: PathBuf,
+ },
+ ReadNpmManifestError,
+ ReadPackageConfigError {
+ file: PathBuf,
+ },
+ ReadPlatformError {
+ file: PathBuf,
+ },
+ RegistryFetchError {
+ tool: String,
+ from_url: String,
+ },
+ RunShimDirectly,
+ SetToolExecutable {
+ tool: String,
+ },
+ SetupToolImageError {
+ tool: String,
+ version: String,
+ dir: PathBuf,
+ },
+ ShimCreateError {
+ name: String,
+ },
+ ShimRemoveError {
+ name: String,
+ },
+ StringifyBinConfigError,
+ StringifyPackageConfigError,
+ StringifyPlatformError,
+ Unimplemented {
+ feature: String,
+ },
+ UnpackArchiveError {
+ tool: String,
+ version: String,
+ },
+ UpgradePackageNotFound {
+ package: String,
+ manager: PackageManager,
+ },
+ UpgradePackageWrongManager {
+ package: String,
+ manager: PackageManager,
+ },
+ VersionParseError {
+ version: String,
+ },
+ WriteBinConfigError {
+ file: PathBuf,
+ },
+ WriteDefaultNpmError {
+ file: PathBuf,
+ },
+ WriteLauncherError {
+ tool: String,
+ },
+ WriteNodeIndexCacheError {
+ file: PathBuf,
+ },
+ WriteNodeIndexExpiryError {
+ file: PathBuf,
+ },
+ WritePackageConfigError {
+ file: PathBuf,
+ },
+ WritePlatformError {
+ file: PathBuf,
+ },
+ Yarn2NotSupported,
+ YarnLatestFetchError {
+ from_url: String,
+ },
+ YarnVersionNotFound {
+ matching: String,
+ },
+}
Thrown when package tries to install a binary that is already installed.
+Thrown when executing an external binary fails
+Thrown when a binary could not be found in the local inventory
+Thrown when building the virtual environment path fails
+Thrown when unable to launch a command with VOLTA_BYPASS set
+Thrown when a user tries to volta fetch
something other than node/yarn/npm.
Thrown when a user tries to volta pin
something other than node/yarn/npm.
Thrown when the Completions out-dir is not a directory
+Thrown when the containing directory could not be determined
+Thrown when unable to start the migration executable
+Thrown when unable to create the layout file
+Thrown when unable to create a link to the shared global library directory
+Thrown when creating a temporary directory fails
+Thrown when creating a temporary file fails
+Thrown when deleting a directory fails
+Thrown when deleting a file fails
+Thrown when unable to execute a hook command
+Thrown when volta.extends
keys result in an infinite cycle
Thrown when determining the path to an extension manifest fails
+Thrown when a hook command returns a non-zero exit code
+Thrown when a hook contains multiple fields (prefix, template, or bin)
+Thrown when a hook doesn’t contain any of the known fields (prefix, template, or bin)
+Thrown when determining the path to a hook fails
+Thrown when determining the name of a newly-installed package fails
+Thrown when output from a hook command could not be read
+Thrown when a user does e.g. volta install node 12
instead of
+volta install node@12
.
Thrown when a user does e.g. volta install 12
instead of
+volta install node@12
.
Thrown when a format other than “npm” or “github” is given for yarn.index in the hooks
+Thrown when a tool name is invalid per npm’s rules.
+Thrown when unable to acquire a lock on the Volta directory
+Thrown when pinning or installing npm@bundled and couldn’t detect the bundled version
+Thrown when pnpm is not set at the command-line
+Thrown when Yarn is not set at the command-line
+Thrown when a user tries to install a Yarn or npm version before installing a Node version.
+Thrown when there is no Node version matching a requested semver specifier.
+Thrown when the install dir could not be determined
+Thrown when a user tries to pin a npm, pnpm, or Yarn version before pinning a Node version.
+Thrown when the platform (Node version) could not be determined
+Thrown when parsing the project manifest and there is a "volta"
key without Node
Thrown when Yarn is not set in a project
+Thrown when pnpm is not set in a project
+Thrown when no shell profiles could be found
+Thrown when the user tries to pin Node or Yarn versions outside of a package.
+Thrown when default Yarn is not set
+Thrown when default pnpm is not set
+Thrown when npm link
is called with a package that isn’t available
Thrown when npm link
is called with a package that was not installed / linked with npm
Thrown when there is no npm version matching the requested Semver/Tag
+Thrown when the command to install a global package is not successful
+Thrown when parsing the package manifest fails
+Thrown when reading the package manifest fails
+Thrown when a specified package could not be found on the npm registry
+Thrown when parsing a package manifest fails
+Thrown when reading a package manifest fails
+Thrown when a package has been unpacked but is not formed correctly.
+Thrown when writing a package manifest fails
+Thrown when unable to parse a bin config file
+Thrown when unable to parse a hooks.json file
+Thrown when unable to parse the node index cache
+Thrown when unable to parse the node index
+Thrown when unable to parse the node index cache expiration
+Thrown when unable to parse the npm manifest file from a node install
+Thrown when unable to parse a package configuration
+Thrown when unable to parse the platform.json file
+Thrown when unable to parse a tool spec (<tool>[@<version>]
)
Thrown when persisting an archive to the inventory fails
+Thrown when there is no pnpm version matching a requested semver specifier.
+Thrown when executing a project-local binary fails
+Thrown when a project-local binary could not be found
+Thrown when a publish hook contains both the url and bin fields
+Thrown when a publish hook contains neither url nor bin fields
+Thrown when there was an error reading the user bin directory
+Thrown when there was an error reading the config for a binary
+Thrown when unable to read the default npm version file
+Thrown when unable to read the contents of a directory
+Thrown when there was an error opening a hooks.json file
+Thrown when there was an error reading the Node Index Cache
+Thrown when there was an error reading the Node Index Cache Expiration
+Thrown when there was an error reading the npm manifest file
+Thrown when there was an error reading a package configuration file
+Thrown when there was an error opening the user platform file
+Thrown when the public registry for Node or Yarn could not be downloaded.
+Thrown when the shim binary is called directly, not through a symlink
+Thrown when there was an error setting a tool to executable
+Thrown when there was an error copying an unpacked tool to the image directory
+Thrown when Volta is unable to create a shim
+Thrown when Volta is unable to remove a shim
+Thrown when serializing a bin config to JSON fails
+Thrown when serializing a package config to JSON fails
+Thrown when serializing the platform to JSON fails
+Thrown when a given feature has not yet been implemented
+Thrown when unpacking an archive (tarball or zip) fails
+Thrown when a package to upgrade was not found
+Thrown when a package to upgrade was installed with a different package manager
+Thrown when there was an error writing a bin config file
+Thrown when there was an error writing the default npm to file
+Thrown when there was an error writing the npm launcher
+Thrown when there was an error writing the node index cache
+Thrown when there was an error writing the node index expiration
+Thrown when there was an error writing a package config
+Thrown when writing the platform.json file fails
+Thrown when a user attempts to install a version of Yarn2
+Thrown when there is an error fetching the latest version of Yarn
+Thrown when there is no Yarn version matching a requested semver specifier.
+fn collect_arguments() -> String
Combines all the arguments into a single String
+fn compose_error_details(err: &VoltaError) -> Option<String>
pub fn report_error(volta_version: &str, err: &VoltaError)
Report an error, both to the console and to error logs
+struct Inner {
+ kind: ErrorKind,
+ source: Option<Box<dyn Error>>,
+}
kind: ErrorKind
§source: Option<Box<dyn Error>>
pub struct VoltaError {
+ inner: Box<Inner>,
+}
Error type for Volta
+inner: Box<Inner>
pub trait Context<T> {
+ // Required method
+ fn with_context<F>(self, f: F) -> Fallible<T>
+ where F: FnOnce() -> ErrorKind;
+}
Trait providing the with_context method to easily convert any Result error into a VoltaError
+pub type Fallible<T> = Result<T, VoltaError>;
enum Fallible<T> {
+ Ok(T),
+ Err(VoltaError),
+}
pub enum EventKind {
+ Start,
+ End {
+ exit_code: i32,
+ },
+ Error {
+ exit_code: i32,
+ error: String,
+ env: ErrorEnv,
+ },
+ ToolEnd {
+ exit_code: i32,
+ },
+ Args {
+ argv: String,
+ },
+}
key
and return true
if they are equal.key
and return true
if they are equal.fn get_error_env() -> ErrorEnv
fn unix_timestamp() -> u64
pub struct ErrorEnv {
+ argv: String,
+ exec_path: String,
+ path: String,
+ platform: String,
+ platform_version: String,
+}
argv: String
§exec_path: String
§path: String
§platform: String
§platform_version: String
key
and return true
if they are equal.key
and return true
if they are equal.pub struct Event {
+ timestamp: u64,
+ pub name: String,
+ pub event: EventKind,
+}
timestamp: u64
§name: String
§event: EventKind
pub struct EventLog {
+ events: Vec<Event>,
+}
events: Vec<Event>
pub fn create_staging_dir() -> Fallible<TempDir>
Creates a staging directory in the Volta tmp directory
+pub fn create_staging_file() -> Fallible<NamedTempFile>
Creates a NamedTempFile in the Volta tmp directory
+pub fn dir_entry_match<T, F>(dir: &Path, f: F) -> Result<Vec<T>>where
+ F: FnMut(&DirEntry) -> Option<T>,
Reads the contents of a directory and returns a Vec of the matched results +from the input function
+pub fn ok_if_not_found<T: Default>(err: Error) -> Result<T>
Converts a failure because of file not found into a success.
+Handling the error is preferred over checking if a file exists before removing it, since +that avoids a potential race condition between the check and the removal.
+pub fn read_dir_eager(
+ dir: &Path
+) -> Result<impl Iterator<Item = (DirEntry, Metadata)>>
Reads the full contents of a directory, eagerly extracting each directory entry
+and its metadata and returning an iterator over them. Returns Error
if any of
+these steps fails.
This function makes it easier to write high level logic for manipulating the +contents of directories (map, filter, etc).
+Note that this function allocates an intermediate vector of directory entries to +construct the iterator from, so if a directory is expected to be very large, it +will allocate temporary data proportional to the number of entries.
+pub fn remove_dir_if_exists<P: AsRef<Path>>(path: P) -> Fallible<()>
Removes the target directory, if it exists. If the directory doesn’t exist, that is treated as +success.
+pub fn remove_file_if_exists<P: AsRef<Path>>(path: P) -> Fallible<()>
Removes the target file, if it exists. If the file doesn’t exist, that is treated as success.
+pub fn rename<F, T>(from: F, to: T) -> Result<()>where
+ F: AsRef<Path>,
+ T: AsRef<Path>,
Rename a file or directory to a new name, retrying if the operation fails because of permissions
+Will retry for ~30 seconds with longer and longer delays between each, to allow for virus scan +and other automated operations to complete.
+pub fn set_executable(bin: &Path) -> Result<()>
Ensure that a given file has ‘executable’ permissions, otherwise we won’t be able to call it
+pub fn symlink_dir<S, D>(src: S, dest: D) -> Result<()>where
+ S: AsRef<Path>,
+ D: AsRef<Path>,
Create a directory symlink. The dst
path will be a symbolic link pointing to the src
path
pub fn symlink_file<S, D>(src: S, dest: D) -> Result<()>where
+ S: AsRef<Path>,
+ D: AsRef<Path>,
Create a file symlink. The dst
path will be a symbolic link pointing to the src
path.
Provides utilities for operating on the filesystem.
+Error
if any of
+these steps fails.dst
path will be a symbolic link pointing to the src
pathdst
path will be a symbolic link pointing to the src
path.pub enum Publish {
+ Url(String),
+ Bin(String),
+}
A hook for publishing Volta events.
+Reports an event by sending a POST request to a URL.
+Reports an event by forking a process and sending the event by IPC.
+key
and return true
if they are equal.key
and return true
if they are equal.pub enum RegistryFormat {
+ Npm,
+ Github,
+}
Format of the registry used for Yarn (Npm or Github)
+self
and other
values to be equal, and is used
+by ==
.key
and return true
if they are equal.key
and return true
if they are equal.Provides types for working with Volta hooks.
+Redirecting to macro.merge_hooks.html...
+ + + \ No newline at end of file diff --git a/main/volta_core/hook/macro.merge_hooks.html b/main/volta_core/hook/macro.merge_hooks.html new file mode 100644 index 000000000..b9781e850 --- /dev/null +++ b/main/volta_core/hook/macro.merge_hooks.html @@ -0,0 +1,3 @@ +macro_rules! merge_hooks { + ($self:ident, $other:ident, $field:ident) => { ... }; +}
pub struct RawEventHooks {
+ pub publish: Option<RawPublishHook>,
+}
publish: Option<RawPublishHook>
pub struct RawHookConfig {
+ pub node: Option<RawToolHooks<Node>>,
+ pub npm: Option<RawToolHooks<Npm>>,
+ pub pnpm: Option<RawToolHooks<Pnpm>>,
+ pub yarn: Option<RawYarnHooks>,
+ pub events: Option<RawEventHooks>,
+}
node: Option<RawToolHooks<Node>>
§npm: Option<RawToolHooks<Npm>>
§pnpm: Option<RawToolHooks<Pnpm>>
§yarn: Option<RawYarnHooks>
§events: Option<RawEventHooks>
pub struct RawIndexHook {
+ prefix: Option<String>,
+ template: Option<String>,
+ bin: Option<String>,
+ format: Option<String>,
+}
prefix: Option<String>
§template: Option<String>
§bin: Option<String>
§format: Option<String>
pub struct RawPublishHook {
+ url: Option<String>,
+ bin: Option<String>,
+}
url: Option<String>
§bin: Option<String>
pub struct RawResolveHook {
+ prefix: Option<String>,
+ template: Option<String>,
+ bin: Option<String>,
+}
prefix: Option<String>
§template: Option<String>
§bin: Option<String>
pub struct RawToolHooks<T: Tool> {
+ pub distro: Option<RawResolveHook>,
+ pub latest: Option<RawResolveHook>,
+ pub index: Option<RawResolveHook>,
+ phantom: PhantomData<T>,
+}
distro: Option<RawResolveHook>
§latest: Option<RawResolveHook>
§index: Option<RawResolveHook>
§phantom: PhantomData<T>
pub struct RawYarnHooks {
+ pub distro: Option<RawResolveHook>,
+ pub latest: Option<RawResolveHook>,
+ pub index: Option<RawIndexHook>,
+}
distro: Option<RawResolveHook>
§latest: Option<RawResolveHook>
§index: Option<RawIndexHook>
pub struct EventHooks {
+ pub publish: Option<Publish>,
+}
Volta hooks related to events.
+publish: Option<Publish>
The hook for publishing events, if any.
+pub struct HookConfig {
+ node: Option<ToolHooks<Node>>,
+ npm: Option<ToolHooks<Npm>>,
+ pnpm: Option<ToolHooks<Pnpm>>,
+ yarn: Option<YarnHooks>,
+ events: Option<EventHooks>,
+}
Volta hook configuration
+node: Option<ToolHooks<Node>>
§npm: Option<ToolHooks<Npm>>
§pnpm: Option<ToolHooks<Pnpm>>
§yarn: Option<YarnHooks>
§events: Option<EventHooks>
Returns the current hooks, which are a merge between the user hooks and +the project hooks (if any).
+Returns the merged hooks loaded from an iterator of potential hook files
+paths
should be sorted in order of descending precedence.
pub struct LazyHookConfig {
+ settings: OnceCell<HookConfig>,
+}
Lazily loaded Volta hook configuration
+settings: OnceCell<HookConfig>
Constructs a new LazyHookConfig
pub struct ToolHooks<T: Tool> {
+ pub distro: Option<DistroHook>,
+ pub latest: Option<MetadataHook>,
+ pub index: Option<MetadataHook>,
+ phantom: PhantomData<T>,
+}
Volta hooks for an individual tool
+distro: Option<DistroHook>
The hook for resolving the URL for a distro version
+latest: Option<MetadataHook>
The hook for resolving the URL for the latest version
+index: Option<MetadataHook>
The hook for resolving the Tool Index URL
+phantom: PhantomData<T>
pub struct YarnHooks {
+ pub distro: Option<DistroHook>,
+ pub latest: Option<MetadataHook>,
+ pub index: Option<YarnIndexHook>,
+}
Volta hooks for Yarn
+distro: Option<DistroHook>
The hook for resolving the URL for a distro version
+latest: Option<MetadataHook>
The hook for resolving the URL for the latest version
+index: Option<YarnIndexHook>
The hook for resolving the Tool Index URL
+const ARCH_TEMPLATE: &str = "{{arch}}";
const EXTENSION_TEMPLATE: &str = "{{ext}}";
const FILENAME_TEMPLATE: &str = "{{filename}}";
const OS_TEMPLATE: &str = "{{os}}";
const VERSION_TEMPLATE: &str = "{{version}}";
pub enum DistroHook {
+ Prefix(String),
+ Template(String),
+ Bin {
+ bin: String,
+ base_path: PathBuf,
+ },
+}
A hook for resolving the distro URL for a given tool version
+self
and other
values to be equal, and is used
+by ==
.key
and return true
if they are equal.key
and return true
if they are equal.pub enum MetadataHook {
+ Prefix(String),
+ Template(String),
+ Bin {
+ bin: String,
+ base_path: PathBuf,
+ },
+}
A hook for resolving the URL for metadata about a tool
+self
and other
values to be equal, and is used
+by ==
.key
and return true
if they are equal.key
and return true
if they are equal.fn calculate_extension(filename: &str) -> Option<&str>
Use the expected filename to determine the extension for this hook
+This will include the multi-part tar.gz
extension if it is present, otherwise it will use
+the standard extension.
Types representing Volta Tool Hooks.
+static REL_PATH_PARENT: Lazy<String>
pub struct YarnIndexHook {
+ pub format: RegistryFormat,
+ pub metadata: MetadataHook,
+}
A hook for resolving the URL for the Yarn index
+format: RegistryFormat
§metadata: MetadataHook
self
and other
values to be equal, and is used
+by ==
.key
and return true
if they are equal.key
and return true
if they are equal.The main implementation crate for the core of Volta.
+log
crateProject
type, which represents a Node project tree in
+the filesystem.Session
type, which represents the user’s state during an
+execution of a Volta tool, including their current directory, Volta
+hook configuration, and the state of the local inventory.pub fn node_available(version: &Version) -> Fallible<bool>
Checks if a given Node version image is available on the local machine
+pub fn node_versions() -> Fallible<BTreeSet<Version>>
Collects a set of all Node versions fetched on the local machine
+pub fn npm_available(version: &Version) -> Fallible<bool>
Checks if a given npm version image is available on the local machine
+pub fn npm_versions() -> Fallible<BTreeSet<Version>>
Collects a set of all npm versions fetched on the local machine
+pub fn package_configs() -> Fallible<BTreeSet<PackageConfig>>
Collects a set of all Package Configs on the local machine
+pub fn pnpm_available(version: &Version) -> Fallible<bool>
Checks if a given pnpm version image is available on the local machine
+pub fn pnpm_versions() -> Fallible<BTreeSet<Version>>
Collects a set of all pnpm versions fetched on the local machine
+fn read_versions(dir: &Path) -> Fallible<BTreeSet<Version>>
Reads the contents of a directory and returns the set of all versions found +in the directory’s listing by parsing the directory names as semantic versions
+pub fn yarn_available(version: &Version) -> Fallible<bool>
Checks if a given Yarn version image is available on the local machine
+pub fn yarn_versions() -> Fallible<BTreeSet<Version>>
Collects a set of all Yarn versions fetched on the local machine
+Provides types for working with Volta’s inventory, the local repository +of available tool versions.
+fn default_install_dir() -> Fallible<PathBuf>
Determine the binary install directory from the currently running executable
+The volta-shim and volta binaries will be installed in the same location, so we can use the +currently running executable to find the binary install directory. Note that we need to +canonicalize the path we get from current_exe to make sure we resolve symlinks and find the +actual binary files
+pub fn volta_home<'a>() -> Fallible<&'a VoltaHome>
pub fn volta_install<'a>() -> Fallible<&'a VoltaInstall>
static VOLTA_HOME: OnceCell<VoltaHome>
static VOLTA_INSTALL: OnceCell<VoltaInstall>
pub(super) fn default_home_dir() -> Fallible<PathBuf>
const ALLOWED_PREFIXES: [&str; 5];
const ERROR_PREFIX: &str = "error:";
const MIGRATION_ERROR_PREFIX: &str = "Volta update error:";
const MIGRATION_WARNING_PREFIX: &str = "Volta update warning:";
const SHIM_ERROR_PREFIX: &str = "Volta error:";
const SHIM_WARNING_PREFIX: &str = "Volta warning:";
const VOLTA_LOGLEVEL: &str = "VOLTA_LOGLEVEL";
const WARNING_PREFIX: &str = "warning:";
const WRAP_INDENT: &str = " ";
pub enum LogContext {
+ Volta,
+ Shim,
+ Migration,
+}
Represents the context from which the logger was created
+Log messages from the volta
executable
Log messages from one of the shims
+Log messages from the migration
+pub enum LogVerbosity {
+ Quiet,
+ Default,
+ Verbose,
+ VeryVerbose,
+}
Represents the level of verbosity that was requested by the user
+source
. Read morefn level_from_env() -> LevelFilter
Determines the correct logging level based on the environment +If VOLTA_LOGLEVEL is set to a valid level, we use that +If not, we check the current stdout to determine whether it is a TTY or not +If it is a TTY, we use Info +If it is NOT a TTY, we use Error as we don’t want to show warnings when running as a script
+fn wrap_content<D>(prefix: &str, content: &D) -> Stringwhere
+ D: Display,
Wraps the supplied content to the terminal width, if we are in a terminal. +If not, returns the content as a String
+Note: Uses the supplied prefix to calculate the terminal width, but then removes +it so that it can be styled (style characters are counted against the wrapped width)
+This module provides a custom Logger implementation for use with the log
crate
pub struct Logger {
+ context: LogContext,
+ level: LevelFilter,
+}
context: LogContext
§level: LevelFilter
Initialize the global logger with a Logger instance +Will use the requested level of Verbosity +If set to Default, will use the environment to determine the level of verbosity
+pub fn send_events(command: &str, events: &[Event])
Send event to the spawned command process
+fn spawn_process(command: &str, tempfile_path: Option<PathBuf>) -> Option<Child>
fn write_events_file(events_json: String) -> Option<PathBuf>
pub enum InheritOption<T> {
+ Some(T),
+ None,
+ Inherit,
+}
Represents 3 possible states: Having a value, not having a value, and inheriting a value
+source
. Read morepub enum Source {
+ Default,
+ Project,
+ Binary,
+ CommandLine,
+}
The source with which a version is associated
+Represents a version from the user default platform
+Represents a version from a project manifest
+Represents a version from a pinned Binary platform
+Represents a version from the command line (via volta run
)
fn build_path_error() -> ErrorKind
pub struct Image {
+ pub node: Sourced<Version>,
+ pub npm: Option<Sourced<Version>>,
+ pub pnpm: Option<Sourced<Version>>,
+ pub yarn: Option<Sourced<Version>>,
+}
A platform image.
+node: Sourced<Version>
The pinned version of Node.
+npm: Option<Sourced<Version>>
The custom version of npm, if any. None
represents using the npm that is bundled with Node
pnpm: Option<Sourced<Version>>
The pinned version of pnpm, if any.
+yarn: Option<Sourced<Version>>
The pinned version of Yarn, if any.
+Produces a modified version of the current PATH
environment variable that
+will find toolchain executables (Node, npm, pnpm, Yarn) in the installation directories
+for the given versions instead of in the Volta shim directory.
Determines the sourced version of npm that will be available, resolving the version bundled with Node, if needed
+PlatformSpec
spub struct CliPlatform {
+ pub node: Option<Version>,
+ pub npm: InheritOption<Version>,
+ pub pnpm: InheritOption<Version>,
+ pub yarn: InheritOption<Version>,
+}
Represents a (maybe) platform with values from the command line
+node: Option<Version>
§npm: InheritOption<Version>
§pnpm: InheritOption<Version>
§yarn: InheritOption<Version>
source
. Read morepub struct Image {
+ pub node: Sourced<Version>,
+ pub npm: Option<Sourced<Version>>,
+ pub pnpm: Option<Sourced<Version>>,
+ pub yarn: Option<Sourced<Version>>,
+}
A platform image.
+node: Sourced<Version>
The pinned version of Node.
+npm: Option<Sourced<Version>>
The custom version of npm, if any. None
represents using the npm that is bundled with Node
pnpm: Option<Sourced<Version>>
The pinned version of pnpm, if any.
+yarn: Option<Sourced<Version>>
The pinned version of Yarn, if any.
+Produces a modified version of the current PATH
environment variable that
+will find toolchain executables (Node, npm, pnpm, Yarn) in the installation directories
+for the given versions instead of in the Volta shim directory.
Determines the sourced version of npm that will be available, resolving the version bundled with Node, if needed
+pub struct Platform {
+ pub node: Sourced<Version>,
+ pub npm: Option<Sourced<Version>>,
+ pub pnpm: Option<Sourced<Version>>,
+ pub yarn: Option<Sourced<Version>>,
+}
Represents a real Platform, with Versions pulled from one or more PlatformSpec
s
node: Sourced<Version>
§npm: Option<Sourced<Version>>
§pnpm: Option<Sourced<Version>>
§yarn: Option<Sourced<Version>>
Returns the user’s currently active platform, if any
+Active platform is determined by first looking at the Project Platform
+pub struct PlatformSpec {
+ pub node: Version,
+ pub npm: Option<Version>,
+ pub pnpm: Option<Version>,
+ pub yarn: Option<Version>,
+}
Represents the specification of a single Platform, regardless of the source
+node: Version
§npm: Option<Version>
§pnpm: Option<Version>
§yarn: Option<Version>
Convert this PlatformSpec into a Platform with all sources set to Default
Convert this PlatformSpec into a Platform with all sources set to Project
source
. Read moreself
and other
values to be equal, and is used
+by ==
.self
and other
) and is used by the <=
+operator. Read morekey
and return true
if they are equal.key
and return true
if they are equal.pub struct Sourced<T> {
+ pub value: T,
+ pub source: Source,
+}
value: T
§source: Source
pub struct System;
A lightweight namespace type representing the system environment, i.e. the environment +with Volta removed.
+pub struct System;
A lightweight namespace type representing the system environment, i.e. the environment +with Volta removed.
+pub(crate) fn find_closest_root(dir: PathBuf) -> Option<PathBuf>
Starts at base_dir
and walks up the directory tree until a package.json file is found
fn is_dependency(dir: &Path) -> bool
fn is_node_modules(dir: &Path) -> bool
fn is_node_root(dir: &Path) -> bool
fn is_project_root(dir: &Path) -> bool
Provides the Project
type, which represents a Node project tree in
+the filesystem.
base_dir
and walks up the directory tree until a package.json file is foundpub(super) enum ManifestKey {
+ Node,
+ Npm,
+ Pnpm,
+ Yarn,
+}
pub(super) fn update_manifest(
+ file: &Path,
+ key: ManifestKey,
+ value: Option<&Version>
+) -> Fallible<()>
Updates the volta
hash in the specified manifest with the given key and value
Will create the volta
hash if it isn’t already present
If the value is None
, will remove the key from the hash
volta
hash in the specified manifest with the given key and valuepub(super) struct Manifest {
+ pub dependency_maps: Chain<IntoIter<HashMap<String, String>>, IntoIter<HashMap<String, String>>>,
+ pub platform: Option<PartialPlatform>,
+ pub extends: Option<PathBuf>,
+}
dependency_maps: Chain<IntoIter<HashMap<String, String>>, IntoIter<HashMap<String, String>>>
§platform: Option<PartialPlatform>
§extends: Option<PathBuf>
struct RawManifest {
+ dependencies: Option<HashMap<String, String>>,
+ dev_dependencies: Option<HashMap<String, String>>,
+ volta: Option<ToolchainSpec>,
+}
dependencies: Option<HashMap<String, String>>
§dev_dependencies: Option<HashMap<String, String>>
§volta: Option<ToolchainSpec>
struct ToolchainSpec {
+ node: Option<String>,
+ npm: Option<String>,
+ pnpm: Option<String>,
+ yarn: Option<String>,
+ extends: Option<PathBuf>,
+}
node: Option<String>
§npm: Option<String>
§pnpm: Option<String>
§yarn: Option<String>
§extends: Option<PathBuf>
Moves the tool versions into a PartialPlatform
and returns that along with the extends
value
pub type DependencyMapIterator = Chain<IntoIter<HashMap<String, String>>, IntoIter<HashMap<String, String>>>;
struct DependencyMapIterator {
+ a: Option<IntoIter<HashMap<String, String>>>,
+ b: Option<IntoIter<HashMap<String, String>>>,
+}
a: Option<IntoIter<HashMap<String, String>>>
§b: Option<IntoIter<HashMap<String, String>>>
pub struct LazyProject {
+ project: OnceCell<Option<Project>>,
+}
A lazily loaded Project
+project: OnceCell<Option<Project>>
struct PartialPlatform {
+ node: Option<Version>,
+ npm: Option<Version>,
+ pnpm: Option<Version>,
+ yarn: Option<Version>,
+}
node: Option<Version>
§npm: Option<Version>
§pnpm: Option<Version>
§yarn: Option<Version>
pub struct Project {
+ manifest_file: PathBuf,
+ workspace_manifests: IndexSet<PathBuf>,
+ dependencies: ChainMap<String, String>,
+ platform: Option<PlatformSpec>,
+}
A Node project workspace in the filesystem
+manifest_file: PathBuf
§workspace_manifests: IndexSet<PathBuf>
§dependencies: ChainMap<String, String>
§platform: Option<PlatformSpec>
Creates an optional Project instance from the current directory
+Creates an optional Project instance from the specified directory
+Will search ancestors to find a package.json
and use that as the root of the project
Creates a Project instance from the given package manifest file (package.json
)
Returns a reference to the manifest file for the current project
+Returns an iterator of paths to all of the workspace roots
+Returns a reference to the Project’s PlatformSpec
, if available
Returns true if the project dependency map contains the specified dependency
+Returns true if the input binary name is a direct dependency of the input project
+Searches the project roots to find the path to a project-local binary file
+Yarn projects that are using PnP or pnpm linker need to use yarn run.
+Pins the Node version in this project’s manifest file
+Pins the npm version in this project’s manifest file
+pub(super) fn command(
+ exe: &OsStr,
+ args: &[OsString],
+ session: &mut Session
+) -> Fallible<Executor>
Determine the correct command to run for a 3rd-party binary
+Will detect if we should delegate to the project-local version or use the default version
+pub(super) fn default_execution_context(
+ tool: String,
+ platform: Option<Platform>,
+ session: &mut Session
+) -> Fallible<(OsString, ErrorKind)>
Determine the execution context (PATH and failure error message) for a default binary
+pub(super) fn local_execution_context(
+ tool: String,
+ platform: Option<Platform>,
+ session: &mut Session
+) -> Fallible<(OsString, ErrorKind)>
Determine the execution context (PATH and failure error message) for a project-local binary
+fn shared_module_path() -> Fallible<OsString>
Determine the value for NODE_PATH, with the shared lib directory prepended
+This will ensure that global bins can require
other global libs
pub struct DefaultBinary {
+ pub bin_path: PathBuf,
+ pub platform: Platform,
+}
Information about the location and execution context of default binaries
+Fetched from the config files in the Volta directory, represents the binary that is executed +when the user is outside of a project that has the given bin as a dependency.
+bin_path: PathBuf
§platform: Platform
Load information about a default binary by name, if available
+A None
response here means that the tool information couldn’t be found. Either the tool
+name is not a valid UTF-8 string, or the tool config doesn’t exist.
const RECURSION_ENV_VAR: &str = "_VOLTA_TOOL_RECURSION";
Environment variable set internally when a shim has been executed and the context evaluated
+This is set when executing a shim command. If this is already, then the built-in shims (Node, +npm, npx, pnpm and Yarn) will assume that the context has already been evaluated & the PATH has +already been modified, so they will use the pass-through behavior.
+Shims should only be called recursively when the environment is misconfigured, so this will +prevent infinite recursion as the pass-through logic removes the shim directory from the PATH.
+Note: This is explicitly removed when calling a command through volta run
, as that will
+never happen due to the Volta environment.
const VOLTA_BYPASS: &str = "VOLTA_BYPASS";
pub enum Executor {
+ Tool(Box<ToolCommand>),
+ PackageInstall(Box<PackageInstallCommand>),
+ PackageLink(Box<PackageLinkCommand>),
+ PackageUpgrade(Box<PackageUpgradeCommand>),
+ InternalInstall(Box<InternalInstallCommand>),
+ Uninstall(Box<UninstallCommand>),
+ Multiple(Vec<Executor>),
+}
pub enum ToolKind {
+ Node,
+ Npm,
+ Npx,
+ Pnpm,
+ Yarn,
+ ProjectLocalBinary(String),
+ DefaultBinary(String),
+ Bypass(String),
+}
The kind of tool being executed, used to determine the correct execution context
+volta install
logic)npm install --global
)npm link <package>
commandnpm update -g
)pub struct InternalInstallCommand {
+ tool: Spec,
+}
Executor for running an internal install (installing Node, npm, pnpm or Yarn using the volta install
logic)
Note: This is not intended to be used for Package installs. Those should go through the
+PackageInstallCommand
above, to more seamlessly integrate with the package manager
tool: Spec
pub struct PackageInstallCommand {
+ command: Command,
+ installer: DirectInstall,
+ platform: Platform,
+}
Process builder for launching a package install command (e.g. npm install --global
)
This will use a DirectInstall
instance to modify the command before running to point it to
+the Volta directory. It will also complete the install, writing config files and shims
command: Command
The command that will ultimately be executed
+installer: DirectInstall
The installer that modifies the command as necessary and provides the completion method
+platform: Platform
The platform to use when running the command.
+Adds or updates environment variables that the command will use
+Updates the Platform for the command to include values from the command-line
+Runs the install command, applying the necessary modifications to install into the Volta +data directory
+pub struct PackageLinkCommand {
+ command: Command,
+ tool: String,
+ platform: Platform,
+}
Process builder for launching a npm link <package>
command
This will set the appropriate environment variables to ensure that the linked package can be +found.
+command: Command
The command that will ultimately be executed
+tool: String
The tool the user wants to link
+platform: Platform
The platform to use when running the command
+Adds or updates environment variables that the command will use
+Updates the Platform for the command to include values from the command-line
+Runs the link command, applying the necessary modifications to pull from the Volta data +directory.
+This will also check for some common failure cases and alert the user
+Check for possible failure cases with the linked package: +- The package is not found as a global +- The package exists, but was linked using a different package manager +- The package is using a different version of Node than the current project (warning)
+pub struct PackageUpgradeCommand {
+ command: Command,
+ upgrader: InPlaceUpgrade,
+ platform: Platform,
+}
Process builder for launching a global package upgrade command (e.g. npm update -g
)
This will use an InPlaceUpgrade
instance to modify the command and point at the appropriate
+image directory. It will also complete the install, writing any updated configs and shims
command: Command
The command that will ultimately be executed
+upgrader: InPlaceUpgrade
Helper utility to modify the command and provide the completion method
+platform: Platform
The platform to run the command under
+Adds or updates environment variables that the command will use
+Updates the Platform for the command to include values from the command-line
+Runs the upgrade command, applying the necessary modifications to point at the Volta image +directory
+Will also check for common failure cases, such as non-existant package or wrong package +manager
+pub struct ToolCommand {
+ command: Command,
+ platform: Option<Platform>,
+ kind: ToolKind,
+}
Process builder for launching a Volta-managed tool
+Tracks the Platform as well as what kind of tool is being executed, to allow individual tools +to customize the behavior before execution.
+command: Command
§platform: Option<Platform>
§kind: ToolKind
Adds or updates environment variables that the command will use
+Adds or updates a single environment variable that the command will use
+Updates the Platform for the command to include values from the command-line
+Runs the command, returning the ExitStatus
if it successfully launches
pub struct UninstallCommand {
+ tool: Spec,
+}
Executor for running a tool uninstall command.
+This will use the volta uninstall
logic to correctly ensure that the package is fully
+uninstalled
tool: Spec
fn debug_active_image(image: &Image)
Write a debug message with the full image that will be used to execute a command
+fn debug_no_platform()
Write a debug message that there is no platform available
+pub fn execute_shim(session: &mut Session) -> Fallible<ExitStatus>
Execute a shim command, based on the command-line arguments to the current process
+pub fn execute_tool<K, V, S>(
+ exe: &OsStr,
+ args: &[OsString],
+ envs: &HashMap<K, V, S>,
+ cli: CliPlatform,
+ session: &mut Session
+) -> Fallible<ExitStatus>where
+ K: AsRef<OsStr>,
+ V: AsRef<OsStr>,
Execute a tool with the provided arguments
+fn format_tool_version(version: &Sourced<Version>) -> String
fn get_executor(
+ exe: &OsStr,
+ args: &[OsString],
+ session: &mut Session
+) -> Fallible<Executor>
Get the appropriate Tool command, based on the requested executable and arguments
+fn get_tool_name(args: &mut ArgsOs) -> Fallible<OsString>
Determine the name of the command to run by inspecting the first argument to the active process
+fn tool_name_from_file_name(file_name: &OsStr) -> OsString
pub(super) fn command(
+ args: &[OsString],
+ session: &mut Session
+) -> Fallible<Executor>
Build an Executor
for npm
If the command is a global install or uninstall and we have a default platform available, then +we will use custom logic to ensure that the package is correctly installed / uninstalled in the +Volta directory.
+If the command is not a global install / uninstall or we don’t have a default platform, then +we will allow npm to execute the command as usual.
+fn current_project_name(session: &mut Session) -> Option<String>
Determine the name of the current project, if possible
+static REQUIRED_NPM_VERSION: Lazy<Version>
const NPM_INSTALL_ALIASES: [&str; 12];
Aliases that npm supports for the ‘install’ command
+const NPM_LINK_ALIASES: [&str; 2];
Aliases that npm supports for the ‘link’ command
+const NPM_UNINSTALL_ALIASES: [&str; 5];
Aliases that npm supports for the ‘uninstall’ command
+const NPM_UPDATE_ALIASES: [&str; 4];
Aliases that npm supports for the update
command
const PNPM_LINK_ALIASES: [&str; 2];
Aliases that pnpm supports for the ‘link’ command +see: https://pnpm.io/cli/link
+const PNPM_UNINSTALL_ALIASES: [&str; 4];
Aliases that pnpm supports for the ‘remove’ command, +see: https://pnpm.io/cli/remove
+const PNPM_UPDATE_ALIASES: [&str; 3];
Aliases that pnpm supports for the ‘update’ command, +see: https://pnpm.io/cli/update
+const UNSAFE_GLOBAL: &str = "VOLTA_UNSAFE_GLOBAL";
pub enum CommandArg<'a> {
+ Global(GlobalCommand<'a>),
+ Intercepted(InterceptedCommand<'a>),
+ Standard,
+}
Parse the given set of arguments to see if they correspond to an intercepted npm command
+pub enum GlobalCommand<'a> {
+ Install(InstallArgs<'a>),
+ Uninstall(UninstallArgs<'a>),
+ Upgrade(UpgradeArgs<'a>),
+}
pub enum InterceptedCommand<'a> {
+ Link(LinkArgs<'a>),
+ Unlink,
+}
An intercepted local command
+fn has_global_without_prefix<A>(args: &[A]) -> boolwhere
+ A: AsRef<OsStr>,
Check if the provided argument list includes a global flag and doesn’t have a prefix setting
+For our interception, we only want to intercept global commands. Additionally, if the user +passes a prefix setting, that will override the logic we use to redirect the install, so our +process won’t work and will cause an error. We should avoid intercepting in those cases since +a command with an explicit prefix is something beyond the “standard” global install anyway.
+fn is_positional<A>(arg: &A) -> boolwhere
+ A: AsRef<OsStr>,
npm link
commandupdate
commandpub struct InstallArgs<'a> {
+ manager: PackageManager,
+ common_args: Vec<&'a OsStr>,
+ tools: Vec<&'a OsStr>,
+}
The arguments passed to a global install command
+manager: PackageManager
The package manager being used
+common_args: Vec<&'a OsStr>
Common arguments that apply to each tool (e.g. flags)
+tools: Vec<&'a OsStr>
The individual tool arguments
+Convert these global install arguments into an executor for the command
+If there are multiple packages specified to install, then they will be broken out into +individual commands and run separately. That allows us to keep Volta’s sandboxing for each +package while still supporting the ability to install multiple packages at once.
+pub struct LinkArgs<'a> {
+ common_args: Vec<&'a OsStr>,
+ tools: Vec<&'a OsStr>,
+}
The arguments passed to an npm link
command
common_args: Vec<&'a OsStr>
The common arguments that apply to each tool
+tools: Vec<&'a OsStr>
The list of tools to link (if any)
+pub struct UninstallArgs<'a> {
+ tools: Vec<&'a OsStr>,
+}
The list of tools passed to an uninstall command
+tools: Vec<&'a OsStr>
pub struct UpgradeArgs<'a> {
+ manager: PackageManager,
+ common_args: Vec<&'a OsStr>,
+ tools: Vec<&'a OsStr>,
+}
The list of tools passed to an upgrade command
+manager: PackageManager
The package manager being used
+common_args: Vec<&'a OsStr>
Common arguments that apply to each tool (e.g. flags)
+tools: Vec<&'a OsStr>
The individual tool arguments
+Convert these global upgrade arguments into an executor for the command
+If there are multiple packages specified to upgrade, then they will be broken out into +individual commands and run separately. If no packages are specified, then we will upgrade +all installed packages that were installed with the same package manager.
+Build an executor to upgrade all global packages that were installed with the same +package manager as we are currently running.
+fn validate_platform_pnpm(platform: &Platform) -> Fallible<()>
pub(super) fn command(
+ args: &[OsString],
+ session: &mut Session
+) -> Fallible<Executor>
Build an Executor
for Yarn
If the command is a global add or remove and we have a default platform available, then we will +use custom logic to ensure that the package is correctly installed / uninstalled in the Volta +directory.
+If the command is not a global add / remove or we don’t have a default platform, then +we will allow Yarn to execute the command as usual.
+fn validate_platform_yarn(platform: &Platform) -> Fallible<()>
pub enum ActivityKind {
+Show 23 variants
Fetch,
+ Install,
+ Uninstall,
+ List,
+ Current,
+ Default,
+ Pin,
+ Node,
+ Npm,
+ Npx,
+ Pnpm,
+ Yarn,
+ Volta,
+ Tool,
+ Help,
+ Version,
+ Binary,
+ Shim,
+ Completions,
+ Which,
+ Setup,
+ Run,
+ Args,
+}
source
. Read moreself
and other
values to be equal, and is used
+by ==
.self
and other
) and is used by the <=
+operator. Read morekey
and return true
if they are equal.key
and return true
if they are equal.Provides the Session
type, which represents the user’s state during an
+execution of a Volta tool, including their current directory, Volta
+hook configuration, and the state of the local inventory.
pub struct Session {
+ hooks: LazyHookConfig,
+ toolchain: LazyToolchain,
+ project: LazyProject,
+ event_log: EventLog,
+}
Represents the user’s state during an execution of a Volta tool. The session +encapsulates a number of aspects of the environment in which the tool was +invoked, including:
+hooks: LazyHookConfig
§toolchain: LazyToolchain
§project: LazyProject
§event_log: EventLog
Produces a reference to the current Node project, if any.
+Produces a mutable reference to the current Node project, if any.
+Returns the user’s default platform, if any
+Returns the current project’s pinned platform image, if any.
+Produces a reference to the current toolchain (default platform specification)
+Produces a mutable reference to the current toolchain
+Produces a reference to the hook configuration
+pub enum ShimResult {
+ Created,
+ AlreadyExists,
+ Deleted,
+ DoesntExist,
+}
self
and other
values to be equal, and is used
+by ==
.key
and return true
if they are equal.key
and return true
if they are equal.pub fn create(shim_name: &str) -> Fallible<ShimResult>
pub fn delete(shim_name: &str) -> Fallible<ShimResult>
fn get_shim_list_deduped(dir: &Path) -> Fallible<HashSet<String>>
pub fn regenerate_shims_for_dir(dir: &Path) -> Fallible<()>
pub fn create(shim_name: &str) -> Fallible<ShimResult>
pub fn entry_to_shim_name(
+ (entry, metadata): (DirEntry, Metadata)
+) -> Option<String>
Unix-specific shim utilities
+On macOS and Linux, creating a shim involves creating a symlink to the volta-shim
+executable. Additionally, filtering the shims from directory entries means looking
+for symlinks and ignoring the actual binaries
const INTERRUPTED_EXIT_CODE: i32 = 130;
pub fn pass_control_to_shim()
pub fn setup_signal_handler()
static SHIM_HAS_CONTROL: AtomicBool
const MAX_PROGRESS_WIDTH: usize = 40;
pub const MAX_WIDTH: usize = 100;
fn action_str(origin: Origin) -> &'static str
Determines the string to display based on the Origin of the operation.
+pub(crate) fn format_error_cause(inner: &dyn Error) -> String
Format the underlying cause of an error
+pub fn note_prefix() -> StyledObject<&'static str>
Generate the styled prefix for a note
+pub fn progress_bar(origin: Origin, details: &str, len: u64) -> ProgressBar
Constructs a command-line progress bar based on the specified Origin enum
+(e.g., Origin::Remote
), details string (e.g., "v1.23.4"
), and logical
+length (i.e., the number of logical progress steps in the process being
+visualized by the progress bar).
pub fn progress_spinner<S>(message: S) -> ProgressBarwhere
+ S: Into<Cow<'static, str>>,
Constructs a command-line progress spinner with the specified “message” +string. The spinner is ticked by default every 50ms.
+pub fn success_prefix() -> StyledObject<&'static str>
Generate the styled prefix for a success message
+pub fn text_width() -> Option<usize>
Get the width of the terminal, limited to a maximum of MAX_WIDTH
+pub fn tool_version<N, V>(name: N, version: V) -> Stringwhere
+ N: Display + Sized,
+ V: Display + Sized,
The view layer of Volta, with utilities for styling command-line output.
+Origin::Remote
), details string (e.g., "v1.23.4"
), and logical
+length (i.e., the number of logical progress steps in the process being
+visualized by the progress bar).const LOCK_FILE: &str = "volta.lock";
Inter-process locking on the Volta directory
+To avoid issues where multiple separate invocations of Volta modify the +data directory simultaneously, we provide a locking mechanism that only +allows a single process to modify the directory at a time.
+However, within a single process, we may attempt to lock the directory in +different code paths. For example, when installing a package we require a +lock, however we also may need to install Node, which requires a lock as +well. To avoid deadlocks in those situations, we track the state of the +lock globally:
+volta.lock
file and initialize the state with a count of 1This allows multiple code paths to request a lock and not worry about +potential deadlocks, while still preventing multiple processes from making +concurrent changes.
+static LOCK_STATE: Lazy<Mutex<Option<LockState>>>
struct LockState {
+ file: File,
+ count: usize,
+}
The current state of locks for this process.
+Note: To ensure thread safety within this process, we enclose the +state in a Mutex. This Mutex and it’s associated locks are separate +from the overall process lock and are only used to ensure the count +is accurately maintained within a given process.
+file: File
§count: usize
pub struct VoltaLock {
+ _private: PhantomData<()>,
+}
An RAII implementation of a process lock on the Volta directory. A given Volta process can have +multiple active locks, but only one process can have any locks at a time.
+Once all of the VoltaLock
objects go out of scope, the lock will be released to other
+processes.
_private: PhantomData<()>
const PATH_VAR_NAME: &str = "PATH";
enum FetchStatus {
+ AlreadyFetched,
+ FetchNeeded(Option<VoltaLock>),
+}
Represents the result of checking if a tool is available locally or not
+If a fetch is required, will include an exclusive lock on the Volta directory where possible
+pub enum Spec {
+ Node(VersionSpec),
+ Npm(VersionSpec),
+ Pnpm(VersionSpec),
+ Yarn(VersionSpec),
+ Package(String, VersionSpec),
+}
Specification for a tool and its associated version.
+Methods for parsing a Spec out of string values
+Try to parse a tool and version from a string like `
Get a valid, sorted Vec<Spec>
given a Vec<String>
.
Accounts for the following error conditions:
+volta install node 12
, where the user intended to install node@12
+but used syntax like in nodenv or nvmReturns a listed sorted so that if node
is included in the list, it is
+always first.
Check the args for the bad patterns of
+volta install <number>
volta install <tool> <number>
Compare Spec
s for sorting when converting from strings
We want to preserve the original order as much as possible, so we treat tools in +the same tool category as equal. We still need to pull Node to the front of the +list, followed by Npm, pnpm, Yarn, and then Packages last.
+Resolve a tool spec into a fully realized Tool that can be fetched
+fn check_fetched<F>(already_fetched: F) -> Fallible<FetchStatus>where
+ F: Fn() -> Fallible<bool>,
Uses the supplied already_fetched
predicate to determine if a tool is available or not.
This uses double-checking logic, to correctly handle concurrent fetch requests:
+already_fetched
indicates that a fetch is needed, we acquire an exclusive lock on the Volta directoryNote: If acquiring the lock fails, we proceed anyway, since the fetch is still necessary.
+pub fn check_shim_reachable(shim_name: &str)
Check if a newly-installed shim is first on the PATH. If it isn’t, we want to inform the user +that they’ll want to move it to the start of PATH to make sure things work as expected.
+fn debug_already_fetched<T: Display>(tool: T)
fn download_tool_error(
+ tool: Spec,
+ from_url: impl AsRef<str>
+) -> impl FnOnce() -> ErrorKind
fn find_expected_shim_dir(_shim_name: &str) -> Option<PathBuf>
Locate the base directory for the relevant shim in the Volta directories.
+On Unix, all of the shims, including the default ones, are installed in VoltaHome::shim_dir
fn info_fetched<T: Display>(tool: T)
fn info_installed<T: Display>(tool: T)
fn info_pinned<T: Display>(tool: T)
fn info_project_version<P, D>(project_version: P, default_version: D)where
+ P: Display,
+ D: Display,
fn registry_fetch_error(
+ tool: impl AsRef<str>,
+ from_url: impl AsRef<str>
+) -> impl FnOnce() -> ErrorKind
pub use node::load_default_npm_version;
pub use node::Node;
pub use node::NODE_DISTRO_ARCH;
pub use node::NODE_DISTRO_EXTENSION;
pub use node::NODE_DISTRO_OS;
pub use npm::BundledNpm;
pub use npm::Npm;
pub use package::BinConfig;
pub use package::Package;
pub use package::PackageConfig;
pub use package::PackageManifest;
pub use pnpm::Pnpm;
pub use yarn::Yarn;
already_fetched
predicate to determine if a tool is available or not.pub const NODE_DISTRO_ARCH: &str = "x64";
The architecture component of a Node distro filename
+pub const NODE_DISTRO_EXTENSION: &str = "tar.gz";
The extension for Node distro files
+pub const NODE_DISTRO_IDENTIFIER: &str = "linux-x64";
The file identifier in the Node index files
array
pub const NODE_DISTRO_OS: &str = "linux";
The OS component of a Node distro filename
+fn fetch_remote_distro(
+ version: &Version,
+ url: &str,
+ staging_path: &Path
+) -> Fallible<Box<dyn Archive>>
Fetch the distro archive from the internet
+fn load_cached_distro(file: &Path) -> Option<Box<dyn Archive>>
Return the archive if it is valid. It may have been corrupted or interrupted in the middle of +downloading.
+pub fn load_default_npm_version(node: &Version) -> Fallible<Version>
Load the local npm version file to determine the default npm version for a given version of Node
+fn npm_manifest_path(version: &Version) -> PathBuf
fn public_node_server_root() -> String
fn save_default_npm_version(node: &Version, npm: &Version) -> Fallible<()>
Save the default npm version to the filesystem for a given version of Node
+fn unpack_archive(
+ archive: Box<dyn Archive>,
+ version: &Version
+) -> Fallible<NodeVersion>
Unpack the node archive into the image directory so that it is ready for use
+Provides fetcher for Node distributions
+package.json
file that we care aboutstruct Manifest {
+ version: String,
+}
The portion of npm’s package.json
file that we care about
version: String
pub fn load_default_npm_version(node: &Version) -> Fallible<Version>
Load the local npm version file to determine the default npm version for a given version of Node
+pub fn resolve(
+ matching: VersionSpec,
+ session: &mut Session
+) -> Fallible<Version>
files
arrayfn lts_version_serde<'de, D>(deserializer: D) -> Result<bool, D::Error>where
+ D: Deserializer<'de>,
pub struct NodeEntry {
+ pub version: Version,
+ pub lts: bool,
+}
version: Version
§lts: bool
pub struct NodeIndex {
+ pub(super) entries: Vec<NodeEntry>,
+}
The index of the public Node server.
+entries: Vec<NodeEntry>
pub struct RawNodeEntry {
+ version: Version,
+ npm: Option<Version>,
+ files: HashSet<String>,
+ lts: bool,
+}
version: Version
§npm: Option<Version>
§files: HashSet<String>
§lts: bool
pub struct RawNodeIndex(Vec<RawNodeEntry>);
0: Vec<RawNodeEntry>
fn public_node_version_index() -> String
Returns the URL of the index of available Node versions on the public Node server.
+fn read_cached_opt(url: &str) -> Fallible<Option<RawNodeIndex>>
Reads a public index from the Node cache, if it exists and hasn’t expired.
+pub fn resolve(
+ matching: VersionSpec,
+ session: &mut Session
+) -> Fallible<Version>
fn resolve_latest(hooks: Option<&ToolHooks<Node>>) -> Fallible<Version>
fn resolve_lts(hooks: Option<&ToolHooks<Node>>) -> Fallible<Version>
fn resolve_node_versions(url: &str) -> Fallible<RawNodeIndex>
fn resolve_semver(
+ matching: Range,
+ hooks: Option<&ToolHooks<Node>>
+) -> Fallible<Version>
Provides resolution of Node requirements into specific versions, using the NodeJS index
+pub struct Node {
+ pub(super) version: Version,
+}
The Tool implementation for fetching and installing Node
+version: Version
pub struct NodeVersion {
+ pub runtime: Version,
+ pub npm: Version,
+}
A full Node version including not just the version of Node itself +but also the specific version of npm installed globally with that +Node installation.
+runtime: Version
The version of Node itself.
+npm: Version
The npm version globally installed with the Node distro.
+source
. Read morefn fetch_remote_distro(
+ version: &Version,
+ url: &str,
+ staging_path: &Path
+) -> Fallible<Box<dyn Archive>>
Fetch the distro archive from the internet
+fn load_cached_distro(file: &Path) -> Option<Box<dyn Archive>>
Return the archive if it is valid. It may have been corrupted or interrupted in the middle of +downloading. +ISSUE(#134) - verify checksum
+fn overwrite_launcher(base_path: &Path, tool: &str) -> Fallible<()>
Overwrite the launcher script
+fn unpack_archive(archive: Box<dyn Archive>, version: &Version) -> Fallible<()>
Unpack the npm archive into the image directory so that it is ready for use
+Provides fetcher for npm distributions
+pub fn resolve(
+ matching: VersionSpec,
+ session: &mut Session
+) -> Fallible<Option<Version>>
fn fetch_npm_index(
+ hooks: Option<&ToolHooks<Npm>>
+) -> Fallible<(String, PackageIndex)>
pub fn resolve(
+ matching: VersionSpec,
+ session: &mut Session
+) -> Fallible<Option<Version>>
fn resolve_semver(
+ matching: Range,
+ hooks: Option<&ToolHooks<Npm>>
+) -> Fallible<Version>
pub struct BundledNpm;
The Tool implementation for setting npm to the version bundled with Node
+pub struct Npm {
+ pub(super) version: Version,
+}
The Tool implementation for fetching and installing npm
+version: Version
pub(super) fn parse_manifest(
+ package_name: &str,
+ staging_dir: PathBuf,
+ manager: PackageManager
+) -> Fallible<PackageManifest>
Read the manifest for the package being installed
+fn validate_bins(package_name: &str, manifest: &PackageManifest) -> Fallible<()>
Validate that we aren’t attempting to install a bin that is already installed by +another package.
+pub(super) fn write_config_and_shims(
+ name: &str,
+ manifest: &PackageManifest,
+ image: &Image,
+ manager: PackageManager
+) -> Fallible<()>
Generate configuration files and shims for the package and each of its bins
+enum NeedsScope {
+ Yes,
+ No,
+}
source
. Read moreself
and other
values to be equal, and is used
+by ==
.pub enum PackageManager {
+ Npm,
+ Pnpm,
+ Yarn,
+}
The package manager used to install a given package
+Given the package_root
, returns the directory where the source is stored for this
+package manager. This will include the top-level node_modules
, where appropriate.
Given the package_root
, returns the root of the source directory. This directory will
+contain the top-level node-modules
Given the package_root
, returns the directory where binaries are stored for this package
+manager.
Modify a given Command
to be set up for global installs, given the package root
Determine the name of the package that was installed into the package_root
If there are none or more than one package installed, then we return None
+source
. Read moreself
and other
values to be equal, and is used
+by ==
.self
and other
) and is used by the <=
+operator. Read morekey
and return true
if they are equal.key
and return true
if they are equal.fn link_package_to_shared_dir(
+ package_name: &str,
+ manager: PackageManager
+) -> Fallible<()>
fn persist_install<V>(
+ package_name: &str,
+ package_version: V,
+ staging_dir: &Path
+) -> Fallible<()>where
+ V: Display,
fn setup_staging_directory(
+ manager: PackageManager,
+ needs_scope: NeedsScope
+) -> Fallible<TempDir>
Create the temporary staging directory we will use to install and ensure expected +subdirectories exist within it
+npm i -g
or yarn global add
npm update -g
or yarn global upgrade
package.json
filepub(super) fn run_global_install(
+ package: String,
+ staging_dir: PathBuf,
+ platform_image: &Image
+) -> Fallible<()>
Use npm install --global
to install the package
Sets the environment variable npm_config_prefix
to redirect the install to the Volta
+data directory, taking advantage of the standard global install behavior with a custom
+location
pub enum PackageManager {
+ Npm,
+ Pnpm,
+ Yarn,
+}
The package manager used to install a given package
+Given the package_root
, returns the directory where the source is stored for this
+package manager. This will include the top-level node_modules
, where appropriate.
Given the package_root
, returns the root of the source directory. This directory will
+contain the top-level node-modules
Given the package_root
, returns the directory where binaries are stored for this package
+manager.
Modify a given Command
to be set up for global installs, given the package root
Determine the name of the package that was installed into the package_root
If there are none or more than one package installed, then we return None
+source
. Read moreself
and other
values to be equal, and is used
+by ==
.self
and other
) and is used by the <=
+operator. Read morekey
and return true
if they are equal.key
and return true
if they are equal.fn get_npm_package_name(source_dir: PathBuf) -> Option<String>
Determine the package name for an npm global install
+npm doesn’t hoist the packages inside of node_modules
, so the only directory will be the
+globally installed package.
fn get_pnpm_or_yarn_package_name(source_root: PathBuf) -> Option<String>
Determine the package name for a pnpm or Yarn global install
+pnpm/Yarn creates a package.json
file with the globally installed package as a dependency
fn get_single_directory_name(parent_dir: &Path) -> Option<String>
Return the name of the single subdirectory (if any) to the given parent_dir
If there are more than one subdirectory, then this will return None
parent_dir
fn default_binary_name(package_name: &str) -> String
Determine the default binary name from the package name
+For non-scoped packages, this is just the package name
+For scoped packages, to match the behavior of the package managers, we remove the scope and use
+only the package part, e.g. @microsoft/rush
would have a default name of rush
dependencies
out of Yarn’s global manifest.package.json
filepub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>where
+ D: Deserializer<'de>,
struct BinMapVisitor;
i8
. Read morei16
. Read morei32
. Read morei64
. Read morei128
. Read moreu8
. Read moreu16
. Read moreu32
. Read moreu64
. Read moreu128
. Read moref32
. Read moref64
. Read morechar
. Read moreDeserializer
. Read moreVisitor
. Read moreDeserializer
. Read moreVisitor
. Read more()
. Read morepub struct BinConfig {
+ pub name: String,
+ pub package: String,
+ pub version: Version,
+ pub platform: PlatformSpec,
+ pub manager: PackageManager,
+}
Configuration information about a single installed binary from a package
+Will be stored in <VOLTA_HOME>/tools/user/bins/
name: String
The binary name
+package: String
The package that installed the binary
+version: Version
The package version
+platform: PlatformSpec
The platform used to install this binary
+manager: PackageManager
The package manager used to install this binary
+pub(super) struct GlobalYarnManifest {
+ pub dependencies: HashMap<String, String>,
+}
Struct to read the dependencies
out of Yarn’s global manifest.
For global installs, yarn creates a package.json
file in the global-folder
and installs
+global packages as dependencies of that pseudo-package
dependencies: HashMap<String, String>
pub struct PackageConfig {
+ pub name: String,
+ pub version: Version,
+ pub platform: PlatformSpec,
+ pub bins: Vec<String>,
+ pub manager: PackageManager,
+}
Configuration information about an installed package
+Will be stored in <VOLTA_HOME>/tools/user/packages/<package>.json
name: String
The package name
+version: Version
The package version
+platform: PlatformSpec
The platform used to install this package
+bins: Vec<String>
The binaries installed by this package
+manager: PackageManager
The package manager that was used to install this package
+self
and other
values to be equal, and is used
+by ==
.self
and other
) and is used by the <=
+operator. Read morekey
and return true
if they are equal.key
and return true
if they are equal.pub struct PackageManifest {
+ pub name: String,
+ pub version: Version,
+ pub bin: Vec<String>,
+}
The relevant information we need out of a package’s package.json
file
This includes the exact Version (since we can install using a range) +and the list of bins provided by the package.
+name: String
The name of the package
+version: Version
The version of the package
+bin: Vec<String>
The bin
section, containing a map of binary names to locations
struct RawPlatformSpec {
+ node: Version,
+ npm: Option<Version>,
+ pnpm: Option<Version>,
+ yarn: Option<Version>,
+}
node: Version
§npm: Option<Version>
§pnpm: Option<Version>
§yarn: Option<Version>
pub struct BinConfig {
+ pub name: String,
+ pub package: String,
+ pub version: Version,
+ pub platform: PlatformSpec,
+ pub manager: PackageManager,
+}
Configuration information about a single installed binary from a package
+Will be stored in <VOLTA_HOME>/tools/user/bins/
name: String
The binary name
+package: String
The package that installed the binary
+version: Version
The package version
+platform: PlatformSpec
The platform used to install this binary
+manager: PackageManager
The package manager used to install this binary
+pub struct DirectInstall {
+ staging: TempDir,
+ manager: PackageManager,
+ name: Option<String>,
+}
Helper struct for direct installs through npm i -g
or yarn global add
Provides methods to simplify installing into a staging directory and then moving that install +into the proper location after it is complete.
+Note: We don’t always know the name of the package up-front, as the install could be from a +tarball or a git coordinate. If we do know ahead of time, then we can skip looking it up
+staging: TempDir
§manager: PackageManager
§name: Option<String>
pub struct InPlaceUpgrade {
+ package: String,
+ directory: PathBuf,
+ manager: PackageManager,
+}
Helper struct for direct in-place upgrades using npm update -g
or yarn global upgrade
Upgrades the requested package directly in the image directory
+package: String
§directory: PathBuf
§manager: PackageManager
Check for possible failure cases with the package to be upgraded +- The package is not installed as a global +- The package exists, but was installed with a different package manager
+pub struct Package {
+ name: String,
+ version: VersionSpec,
+ staging: TempDir,
+}
The Tool implementation for installing 3rd-party global packages
+name: String
§version: VersionSpec
§staging: TempDir
pub struct PackageConfig {
+ pub name: String,
+ pub version: Version,
+ pub platform: PlatformSpec,
+ pub bins: Vec<String>,
+ pub manager: PackageManager,
+}
Configuration information about an installed package
+Will be stored in <VOLTA_HOME>/tools/user/packages/<package>.json
name: String
The package name
+version: Version
The package version
+platform: PlatformSpec
The platform used to install this package
+bins: Vec<String>
The binaries installed by this package
+manager: PackageManager
The package manager that was used to install this package
+self
and other
values to be equal, and is used
+by ==
.self
and other
) and is used by the <=
+operator. Read morekey
and return true
if they are equal.key
and return true
if they are equal.pub struct PackageManifest {
+ pub name: String,
+ pub version: Version,
+ pub bin: Vec<String>,
+}
The relevant information we need out of a package’s package.json
file
This includes the exact Version (since we can install using a range) +and the list of bins provided by the package.
+name: String
The name of the package
+version: Version
The version of the package
+bin: Vec<String>
The bin
section, containing a map of binary names to locations
fn binaries_from_package(package: &str) -> Fallible<Vec<String>>
Reads the contents of a directory and returns a Vec containing the names of +all the binaries installed by the given package.
+fn remove_config_and_shim(bin_name: &str, pkg_name: &str) -> Fallible<()>
Remove a shim and its associated configuration file
+fn remove_shared_link_dir(name: &str) -> Fallible<()>
Remove the link to the package in the shared lib directory
+For scoped packages, if the scope directory is now empty, it will also be removed
+fn fetch_remote_distro(
+ version: &Version,
+ url: &str,
+ staging_path: &Path
+) -> Fallible<Box<dyn Archive>>
Fetch the distro archive from the internet
+fn load_cached_distro(file: &Path) -> Option<Box<dyn Archive>>
Return the archive if it is valid. It may have been corrupted or interrupted in the middle of +downloading.
+fn unpack_archive(archive: Box<dyn Archive>, version: &Version) -> Fallible<()>
Unpack the pnpm archive into the image directory so that it is ready for use
+fn write_launcher(base_path: &Path, tool: &str) -> Fallible<()>
Create executable launchers for the pnpm and pnpx binaries
+Provides fetcher for pnpm distributions
+pub fn resolve(
+ matching: VersionSpec,
+ session: &mut Session
+) -> Fallible<Version>
fn fetch_pnpm_index(
+ hooks: Option<&ToolHooks<Pnpm>>
+) -> Fallible<(String, PackageIndex)>
Fetch the index of available pnpm versions from the npm registry
+pub fn resolve(
+ matching: VersionSpec,
+ session: &mut Session
+) -> Fallible<Version>
fn resolve_semver(
+ matching: Range,
+ hooks: Option<&ToolHooks<Pnpm>>
+) -> Fallible<Version>
pub struct Pnpm {
+ pub(super) version: Version,
+}
The Tool implementation for fetching and installing pnpm
+version: Version
pub const NPM_ABBREVIATED_ACCEPT_HEADER: &str = "application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*";
pub fn fetch_npm_registry(
+ url: String,
+ name: &str
+) -> Fallible<(String, PackageIndex)>
pub fn find_unpack_dir(in_dir: &Path) -> Fallible<PathBuf>
Figure out the unpacked package directory name dynamically
+Packages typically extract to a “package” directory, but not always
+pub fn public_registry_index(package: &str) -> String
pub fn public_registry_package(package: &str, version: &str) -> String
pub fn scoped_public_registry_package(
+ scope: &str,
+ package: &str,
+ version: &str
+) -> String
pub struct PackageDetails {
+ pub(crate) version: Version,
+}
Details about a package in the npm Registry
+version: Version
pub struct PackageIndex {
+ pub tags: HashMap<String, Version>,
+ pub entries: Vec<PackageDetails>,
+}
Index of versions of a specific package from the npm Registry
+entries: Vec<PackageDetails>
pub struct RawDistInfo {
+ pub shasum: String,
+ pub tarball: String,
+}
shasum: String
§tarball: String
source
. Read morepub struct RawPackageMetadata {
+ pub name: String,
+ pub versions: HashMap<String, RawPackageVersionInfo>,
+ pub dist_tags: HashMap<String, Version>,
+}
Package Metadata Response
+See npm registry API doc: +https://github.com/npm/registry/blob/master/docs/REGISTRY-API.md
+name: String
§versions: HashMap<String, RawPackageVersionInfo>
pub struct RawPackageVersionInfo {
+ pub version: Version,
+ pub dist: RawDistInfo,
+}
version: Version
§dist: RawDistInfo
fn is_version_like(value: &str) -> bool
Determine if a given string is “version-like”.
+This means it is either ‘latest’, ‘lts’, a Version, or a Version Range.
+static HAS_VERSION: Lazy<Regex>
static TOOL_SPEC_PATTERN: Lazy<Regex>
pub struct PackageDetails {
+ pub(crate) version: Version,
+}
Details about a package in the npm Registry
+version: Version
pub trait Tool: Display {
+ // Required methods
+ fn fetch(self: Box<Self>, session: &mut Session) -> Fallible<()>;
+ fn install(self: Box<Self>, session: &mut Session) -> Fallible<()>;
+ fn pin(self: Box<Self>, session: &mut Session) -> Fallible<()>;
+}
Trait representing all of the actions that can be taken with a tool
+Fetch a Tool into the local inventory
+fn determine_remote_url(
+ version: &Version,
+ hooks: Option<&YarnHooks>
+) -> Fallible<String>
Determine the remote URL to download from, using the hooks if available
+fn ensure_bin_is_executable(unpack_dir: &Path, tool: &str) -> Fallible<()>
fn fetch_remote_distro(
+ version: &Version,
+ url: &str,
+ staging_path: &Path
+) -> Fallible<Box<dyn Archive>>
Fetch the distro archive from the internet
+fn load_cached_distro(file: &Path) -> Option<Box<dyn Archive>>
Return the archive if it is valid. It may have been corrupted or interrupted in the middle of +downloading.
+fn unpack_archive(archive: Box<dyn Archive>, version: &Version) -> Fallible<()>
Unpack the yarn archive into the image directory so that it is ready for use
+Provides fetcher for Yarn distributions
+pub fn resolve(
+ matching: VersionSpec,
+ session: &mut Session
+) -> Fallible<Version>
pub struct RawYarnAsset {
+ pub name: String,
+}
name: String
The filename of an asset included in a Yarn GitHub release.
+pub struct RawYarnEntry {
+ pub tag_name: Version,
+ pub assets: Vec<RawYarnAsset>,
+}
tag_name: Version
Yarn releases are given a tag name of the form “v$version” where $version +is the release’s version string.
+assets: Vec<RawYarnAsset>
The GitHub API provides a list of assets. Some Yarn releases don’t include +a tarball, so we don’t support them and remove them from the set of available +Yarn versions.
+Is this entry a full release, i.e., does this entry’s asset list include a +proper release tarball?
+pub struct RawYarnIndex(Vec<RawYarnEntry>);
0: Vec<RawYarnEntry>
pub struct YarnIndex {
+ pub(super) entries: BTreeSet<Version>,
+}
The public Yarn index.
+entries: BTreeSet<Version>
fn fetch_yarn_index(package: &str) -> Fallible<(String, PackageIndex)>
pub fn resolve(
+ matching: VersionSpec,
+ session: &mut Session
+) -> Fallible<Version>
fn resolve_custom_tag(tag: String) -> Fallible<Version>
fn resolve_latest_legacy(url: String) -> Fallible<Version>
fn resolve_semver(
+ matching: Range,
+ hooks: Option<&YarnHooks>
+) -> Fallible<Version>
fn resolve_semver_from_registry(matching: Range) -> Fallible<Version>
fn resolve_semver_legacy(matching: Range, url: String) -> Fallible<Version>
fn resolve_semver_npm(matching: Range, url: String) -> Fallible<Version>
fn resolve_tag(tag: VersionTag, hooks: Option<&YarnHooks>) -> Fallible<Version>
pub struct Yarn {
+ pub(super) version: Version,
+}
The Tool implementation for fetching and installing Yarn
+version: Version
pub struct NodeVersion {
+ pub runtime: Version,
+ pub npm: Option<Version>,
+}
runtime: Version
§npm: Option<Version>
self
and other
values to be equal, and is used
+by ==
.key
and return true
if they are equal.key
and return true
if they are equal.pub struct Platform {
+ pub node: Option<NodeVersion>,
+ pub pnpm: Option<Version>,
+ pub yarn: Option<Version>,
+}
node: Option<NodeVersion>
§pnpm: Option<Version>
§yarn: Option<Version>
key
and return true
if they are equal.key
and return true
if they are equal.pub struct LazyToolchain {
+ toolchain: OnceCell<Toolchain>,
+}
Lazily loaded toolchain
+toolchain: OnceCell<Toolchain>
pub struct Toolchain {
+ platform: Option<PlatformSpec>,
+}
platform: Option<PlatformSpec>
Set the active Node version in the default platform file.
+Set the active Yarn version in the default platform file.
+Set the active pnpm version in the default platform file.
+Set the active Npm version in the default platform file.
+pub enum VersionSpec {
+ None,
+ Semver(Range),
+ Exact(Version),
+ Tag(VersionTag),
+}
No version specified (default)
+SemVer Range
+Exact Version
+Arbitrary Version Tag
+pub enum VersionTag {
+ Latest,
+ Lts,
+ Custom(String),
+}
The ‘latest’ tag, a special case that exists for all packages
+The ‘lts’ tag, a special case for Node
+An arbitrary tag version
+pub fn parse_requirements(s: impl AsRef<str>) -> Fallible<Range>
pub fn parse_version(s: impl AsRef<str>) -> Fallible<Version>
fn trim_version(s: &str) -> &str
pub fn deserialize<'de, D>(
+ deserializer: D
+) -> Result<HashMap<String, Version>, D::Error>where
+ D: Deserializer<'de>,
struct Wrapper(Version);
0: Version
pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<Version>, D::Error>where
+ D: Deserializer<'de>,
pub fn serialize<S>(version: &Option<Version>, s: S) -> Result<S::Ok, S::Error>where
+ S: Serializer,
pub fn parse_requirements(src: &str) -> Result<Range, SemverError>
pub fn deserialize<'de, D>(deserializer: D) -> Result<Version, D::Error>where
+ D: Deserializer<'de>,
pub fn serialize<S>(version: &Version, s: S) -> Result<S::Ok, S::Error>where
+ S: Serializer,
struct VersionVisitor;
i8
. Read morei16
. Read morei32
. Read morei64
. Read morei128
. Read moreu8
. Read moreu16
. Read moreu32
. Read moreu64
. Read moreu128
. Read moref32
. Read moref64
. Read morechar
. Read moreDeserializer
. Read moreVisitor
. Read moreDeserializer
. Read moreVisitor
. Read more()
. Read morepub(crate) fn executable(name: &str) -> String
Redirecting to macro.path_buf.html...
+ + + \ No newline at end of file diff --git a/main/volta_layout/macros/macro.path_buf.html b/main/volta_layout/macros/macro.path_buf.html new file mode 100644 index 000000000..62a55b454 --- /dev/null +++ b/main/volta_layout/macros/macro.path_buf.html @@ -0,0 +1,3 @@ +macro_rules! path_buf { + ($base:expr, $( $x:expr ), *) => { ... }; +}
pub struct VoltaHome {Show 22 fields
+ cache_dir: PathBuf,
+ node_cache_dir: PathBuf,
+ shim_dir: PathBuf,
+ log_dir: PathBuf,
+ tools_dir: PathBuf,
+ inventory_dir: PathBuf,
+ node_inventory_dir: PathBuf,
+ package_inventory_dir: PathBuf,
+ yarn_inventory_dir: PathBuf,
+ image_dir: PathBuf,
+ node_image_root_dir: PathBuf,
+ yarn_image_root_dir: PathBuf,
+ package_image_root_dir: PathBuf,
+ default_toolchain_dir: PathBuf,
+ default_bin_dir: PathBuf,
+ default_package_dir: PathBuf,
+ tmp_dir: PathBuf,
+ node_index_file: PathBuf,
+ node_index_expiry_file: PathBuf,
+ default_platform_file: PathBuf,
+ default_hooks_file: PathBuf,
+ root: PathBuf,
+}
cache_dir: PathBuf
§node_cache_dir: PathBuf
§shim_dir: PathBuf
§log_dir: PathBuf
§tools_dir: PathBuf
§inventory_dir: PathBuf
§node_inventory_dir: PathBuf
§package_inventory_dir: PathBuf
§yarn_inventory_dir: PathBuf
§image_dir: PathBuf
§node_image_root_dir: PathBuf
§yarn_image_root_dir: PathBuf
§package_image_root_dir: PathBuf
§default_toolchain_dir: PathBuf
§default_bin_dir: PathBuf
§default_package_dir: PathBuf
§tmp_dir: PathBuf
§node_index_file: PathBuf
§node_index_expiry_file: PathBuf
§default_platform_file: PathBuf
§default_hooks_file: PathBuf
§root: PathBuf
Returns the
+node_cache_dir
+path.
Returns the
+inventory_dir
+path.
Returns the
+node_inventory_dir
+path.
Returns the
+package_inventory_dir
+path.
Returns the
+yarn_inventory_dir
+path.
Returns the
+node_image_root_dir
+path.
Returns the
+yarn_image_root_dir
+path.
Returns the
+package_image_root_dir
+path.
Returns the
+default_toolchain_dir
+path.
Returns the
+default_bin_dir
+path.
Returns the
+default_package_dir
+path.
Returns the
+node_index_file
+path.
Returns the
+node_index_expiry_file
+path.
Returns the
+default_platform_file
+path.
Returns the
+default_hooks_file
+path.
pub struct VoltaInstall {
+ shim_executable: PathBuf,
+ root: PathBuf,
+}
shim_executable: PathBuf
§root: PathBuf
pub struct VoltaHome {Show 23 fields
+ cache_dir: PathBuf,
+ node_cache_dir: PathBuf,
+ shim_dir: PathBuf,
+ log_dir: PathBuf,
+ tools_dir: PathBuf,
+ inventory_dir: PathBuf,
+ node_inventory_dir: PathBuf,
+ package_inventory_dir: PathBuf,
+ yarn_inventory_dir: PathBuf,
+ image_dir: PathBuf,
+ node_image_root_dir: PathBuf,
+ yarn_image_root_dir: PathBuf,
+ package_image_root_dir: PathBuf,
+ default_toolchain_dir: PathBuf,
+ default_bin_dir: PathBuf,
+ default_package_dir: PathBuf,
+ tmp_dir: PathBuf,
+ node_index_file: PathBuf,
+ node_index_expiry_file: PathBuf,
+ default_platform_file: PathBuf,
+ default_hooks_file: PathBuf,
+ layout_file: PathBuf,
+ root: PathBuf,
+}
cache_dir: PathBuf
§node_cache_dir: PathBuf
§shim_dir: PathBuf
§log_dir: PathBuf
§tools_dir: PathBuf
§inventory_dir: PathBuf
§node_inventory_dir: PathBuf
§package_inventory_dir: PathBuf
§yarn_inventory_dir: PathBuf
§image_dir: PathBuf
§node_image_root_dir: PathBuf
§yarn_image_root_dir: PathBuf
§package_image_root_dir: PathBuf
§default_toolchain_dir: PathBuf
§default_bin_dir: PathBuf
§default_package_dir: PathBuf
§tmp_dir: PathBuf
§node_index_file: PathBuf
§node_index_expiry_file: PathBuf
§default_platform_file: PathBuf
§default_hooks_file: PathBuf
§layout_file: PathBuf
§root: PathBuf
Returns the
+node_cache_dir
+path.
Returns the
+inventory_dir
+path.
Returns the
+node_inventory_dir
+path.
Returns the
+package_inventory_dir
+path.
Returns the
+yarn_inventory_dir
+path.
Returns the
+node_image_root_dir
+path.
Returns the
+yarn_image_root_dir
+path.
Returns the
+package_image_root_dir
+path.
Returns the
+default_toolchain_dir
+path.
Returns the
+default_bin_dir
+path.
Returns the
+default_package_dir
+path.
Returns the
+node_index_file
+path.
Returns the
+node_index_expiry_file
+path.
Returns the
+default_platform_file
+path.
Returns the
+default_hooks_file
+path.
Returns the
+layout_file
+path.
pub struct VoltaInstall {
+ shim_executable: PathBuf,
+ main_executable: PathBuf,
+ migrate_executable: PathBuf,
+ root: PathBuf,
+}
shim_executable: PathBuf
§main_executable: PathBuf
§migrate_executable: PathBuf
§root: PathBuf
Returns the
+shim_executable
+path.
Returns the
+main_executable
+path.
Returns the
+migrate_executable
+path.
pub use crate::v1::VoltaInstall;
pub struct VoltaHome {Show 25 fields
+ cache_dir: PathBuf,
+ node_cache_dir: PathBuf,
+ shim_dir: PathBuf,
+ log_dir: PathBuf,
+ tools_dir: PathBuf,
+ inventory_dir: PathBuf,
+ node_inventory_dir: PathBuf,
+ npm_inventory_dir: PathBuf,
+ package_inventory_dir: PathBuf,
+ yarn_inventory_dir: PathBuf,
+ image_dir: PathBuf,
+ node_image_root_dir: PathBuf,
+ npm_image_root_dir: PathBuf,
+ yarn_image_root_dir: PathBuf,
+ package_image_root_dir: PathBuf,
+ default_toolchain_dir: PathBuf,
+ default_bin_dir: PathBuf,
+ default_package_dir: PathBuf,
+ tmp_dir: PathBuf,
+ node_index_file: PathBuf,
+ node_index_expiry_file: PathBuf,
+ default_platform_file: PathBuf,
+ default_hooks_file: PathBuf,
+ layout_file: PathBuf,
+ root: PathBuf,
+}
cache_dir: PathBuf
§node_cache_dir: PathBuf
§shim_dir: PathBuf
§log_dir: PathBuf
§tools_dir: PathBuf
§inventory_dir: PathBuf
§node_inventory_dir: PathBuf
§npm_inventory_dir: PathBuf
§package_inventory_dir: PathBuf
§yarn_inventory_dir: PathBuf
§image_dir: PathBuf
§node_image_root_dir: PathBuf
§npm_image_root_dir: PathBuf
§yarn_image_root_dir: PathBuf
§package_image_root_dir: PathBuf
§default_toolchain_dir: PathBuf
§default_bin_dir: PathBuf
§default_package_dir: PathBuf
§tmp_dir: PathBuf
§node_index_file: PathBuf
§node_index_expiry_file: PathBuf
§default_platform_file: PathBuf
§default_hooks_file: PathBuf
§layout_file: PathBuf
§root: PathBuf
Returns the
+node_cache_dir
+path.
Returns the
+inventory_dir
+path.
Returns the
+node_inventory_dir
+path.
Returns the
+npm_inventory_dir
+path.
Returns the
+package_inventory_dir
+path.
Returns the
+yarn_inventory_dir
+path.
Returns the
+node_image_root_dir
+path.
Returns the
+npm_image_root_dir
+path.
Returns the
+yarn_image_root_dir
+path.
Returns the
+package_image_root_dir
+path.
Returns the
+default_toolchain_dir
+path.
Returns the
+default_bin_dir
+path.
Returns the
+default_package_dir
+path.
Returns the
+node_index_file
+path.
Returns the
+node_index_expiry_file
+path.
Returns the
+default_platform_file
+path.
Returns the
+default_hooks_file
+path.
Returns the
+layout_file
+path.
pub use crate::v1::VoltaInstall;
pub struct VoltaHome {Show 27 fields
+ cache_dir: PathBuf,
+ node_cache_dir: PathBuf,
+ shim_dir: PathBuf,
+ log_dir: PathBuf,
+ tools_dir: PathBuf,
+ inventory_dir: PathBuf,
+ node_inventory_dir: PathBuf,
+ npm_inventory_dir: PathBuf,
+ pnpm_inventory_dir: PathBuf,
+ yarn_inventory_dir: PathBuf,
+ image_dir: PathBuf,
+ node_image_root_dir: PathBuf,
+ npm_image_root_dir: PathBuf,
+ pnpm_image_root_dir: PathBuf,
+ yarn_image_root_dir: PathBuf,
+ package_image_root_dir: PathBuf,
+ shared_lib_root: PathBuf,
+ default_toolchain_dir: PathBuf,
+ default_bin_dir: PathBuf,
+ default_package_dir: PathBuf,
+ tmp_dir: PathBuf,
+ node_index_file: PathBuf,
+ node_index_expiry_file: PathBuf,
+ default_platform_file: PathBuf,
+ default_hooks_file: PathBuf,
+ layout_file: PathBuf,
+ root: PathBuf,
+}
cache_dir: PathBuf
§node_cache_dir: PathBuf
§shim_dir: PathBuf
§log_dir: PathBuf
§tools_dir: PathBuf
§inventory_dir: PathBuf
§node_inventory_dir: PathBuf
§npm_inventory_dir: PathBuf
§pnpm_inventory_dir: PathBuf
§yarn_inventory_dir: PathBuf
§image_dir: PathBuf
§node_image_root_dir: PathBuf
§npm_image_root_dir: PathBuf
§pnpm_image_root_dir: PathBuf
§yarn_image_root_dir: PathBuf
§package_image_root_dir: PathBuf
§default_toolchain_dir: PathBuf
§default_bin_dir: PathBuf
§default_package_dir: PathBuf
§tmp_dir: PathBuf
§node_index_file: PathBuf
§node_index_expiry_file: PathBuf
§default_platform_file: PathBuf
§default_hooks_file: PathBuf
§layout_file: PathBuf
§root: PathBuf
Returns the
+node_cache_dir
+path.
Returns the
+inventory_dir
+path.
Returns the
+node_inventory_dir
+path.
Returns the
+npm_inventory_dir
+path.
Returns the
+pnpm_inventory_dir
+path.
Returns the
+yarn_inventory_dir
+path.
Returns the
+node_image_root_dir
+path.
Returns the
+npm_image_root_dir
+path.
Returns the
+pnpm_image_root_dir
+path.
Returns the
+yarn_image_root_dir
+path.
Returns the
+package_image_root_dir
+path.
Returns the
+shared_lib_root
+path.
Returns the
+default_toolchain_dir
+path.
Returns the
+default_bin_dir
+path.
Returns the
+default_package_dir
+path.
Returns the
+node_index_file
+path.
Returns the
+node_index_expiry_file
+path.
Returns the
+default_platform_file
+path.
Returns the
+default_hooks_file
+path.
Returns the
+layout_file
+path.
pub use crate::v1::VoltaInstall;
pub struct VoltaHome {Show 27 fields
+ cache_dir: PathBuf,
+ node_cache_dir: PathBuf,
+ shim_dir: PathBuf,
+ log_dir: PathBuf,
+ tools_dir: PathBuf,
+ inventory_dir: PathBuf,
+ node_inventory_dir: PathBuf,
+ npm_inventory_dir: PathBuf,
+ pnpm_inventory_dir: PathBuf,
+ yarn_inventory_dir: PathBuf,
+ image_dir: PathBuf,
+ node_image_root_dir: PathBuf,
+ npm_image_root_dir: PathBuf,
+ pnpm_image_root_dir: PathBuf,
+ yarn_image_root_dir: PathBuf,
+ package_image_root_dir: PathBuf,
+ shared_lib_root: PathBuf,
+ default_toolchain_dir: PathBuf,
+ default_bin_dir: PathBuf,
+ default_package_dir: PathBuf,
+ tmp_dir: PathBuf,
+ node_index_file: PathBuf,
+ node_index_expiry_file: PathBuf,
+ default_platform_file: PathBuf,
+ default_hooks_file: PathBuf,
+ layout_file: PathBuf,
+ root: PathBuf,
+}
cache_dir: PathBuf
§node_cache_dir: PathBuf
§shim_dir: PathBuf
§log_dir: PathBuf
§tools_dir: PathBuf
§inventory_dir: PathBuf
§node_inventory_dir: PathBuf
§npm_inventory_dir: PathBuf
§pnpm_inventory_dir: PathBuf
§yarn_inventory_dir: PathBuf
§image_dir: PathBuf
§node_image_root_dir: PathBuf
§npm_image_root_dir: PathBuf
§pnpm_image_root_dir: PathBuf
§yarn_image_root_dir: PathBuf
§package_image_root_dir: PathBuf
§default_toolchain_dir: PathBuf
§default_bin_dir: PathBuf
§default_package_dir: PathBuf
§tmp_dir: PathBuf
§node_index_file: PathBuf
§node_index_expiry_file: PathBuf
§default_platform_file: PathBuf
§default_hooks_file: PathBuf
§layout_file: PathBuf
§root: PathBuf
Returns the
+node_cache_dir
+path.
Returns the
+inventory_dir
+path.
Returns the
+node_inventory_dir
+path.
Returns the
+npm_inventory_dir
+path.
Returns the
+pnpm_inventory_dir
+path.
Returns the
+yarn_inventory_dir
+path.
Returns the
+node_image_root_dir
+path.
Returns the
+npm_image_root_dir
+path.
Returns the
+pnpm_image_root_dir
+path.
Returns the
+yarn_image_root_dir
+path.
Returns the
+package_image_root_dir
+path.
Returns the
+shared_lib_root
+path.
Returns the
+default_toolchain_dir
+path.
Returns the
+default_bin_dir
+path.
Returns the
+default_package_dir
+path.
Returns the
+node_index_file
+path.
Returns the
+node_index_expiry_file
+path.
Returns the
+default_platform_file
+path.
Returns the
+default_hooks_file
+path.
Returns the
+layout_file
+path.
enum EntryKind {
+ Exe,
+ File,
+ Dir,
+}
enum FieldContents {
+ File(Semi),
+ Dir(Directory),
+}
AST for the suffix of a field in a layout!
struct declaration.
A file field suffix, which consists of a single semicolon (;
).
A directory field suffix, which consists of a braced directory.
+layout!
macro.layout!
struct declaration,
+which is of the form:layout!
struct declaration.pub(crate) struct Ast {
+ decls: Vec<LayoutStruct>,
+}
Abstract syntax tree (AST) for the surface syntax of the layout!
macro.
The surface syntax of the layout!
macro takes the form:
Attribute* Visibility "struct" Ident Directory
+
This AST gets lowered by the flatten
method to a vector of intermediate
+representation (IR) trees. See the Ir
type for details.
decls: Vec<LayoutStruct>
struct Directory {
+ entries: Punctuated<FieldPrefix, FieldContents>,
+}
Represents a directory entry in the AST, which can recursively contain +more entries.
+The surface syntax of a directory takes the form:
+{
+ (FieldPrefix)FieldContents*
+}
+
entries: Punctuated<FieldPrefix, FieldContents>
struct FieldPrefix {
+ filename: LitStr,
+ name: Ident,
+}
AST for the common prefix of a single field in a layout!
struct declaration,
+which is of the form:
LitStr ":" Ident
+
This is followed either by a semicolon (;
), indicating that the field is a
+file, or a braced directory entry, indicating that the field is a directory.
If the LitStr
contains the suffix "[.exe]"
it is treated specially as an
+executable file, whose suffix (or lack thereof) is determined by the current
+operating system (using the std::env::consts::EXE_SUFFIX
constant).
filename: LitStr
§name: Ident
pub(crate) struct LayoutStruct {
+ attrs: Vec<Attribute>,
+ visibility: Visibility,
+ name: Ident,
+ directory: Directory,
+}
Represents a single type LayoutStruct in the AST, which takes the form:
+Attribute* Visibility "struct" Ident Directory
+
This AST gets lowered by the flatten
method to a flat list of entries,
+organized by entry type. See the Ir
type for details.
attrs: Vec<Attribute>
§visibility: Visibility
§name: Ident
§directory: Directory
Lowers the AST to a flattened intermediate representation.
+pub(crate) type Result<T> = Result<T, TokenStream>;
enum Result<T> {
+ Ok(T),
+ Err(TokenStream),
+}
pub(crate) struct Entry {
+ pub(crate) name: Ident,
+ pub(crate) context: Vec<LitStr>,
+ pub(crate) filename: LitStr,
+}
name: Ident
§context: Vec<LitStr>
§filename: LitStr
pub(crate) struct Ir {
+ pub(crate) name: Ident,
+ pub(crate) attrs: Vec<Attribute>,
+ pub(crate) visibility: Visibility,
+ pub(crate) dirs: Vec<Entry>,
+ pub(crate) files: Vec<Entry>,
+ pub(crate) exes: Vec<Entry>,
+}
The intermediate representation (IR) of a struct type defined by the layout!
+macro, which contains the flattened directory entries, organized into three
+categories:
name: Ident
§attrs: Vec<Attribute>
§visibility: Visibility
§dirs: Vec<Entry>
§files: Vec<Entry>
§exes: Vec<Entry>
Redirecting to macro.layout.html...
+ + + \ No newline at end of file diff --git a/main/volta_layout_macro/macro.layout.html b/main/volta_layout_macro/macro.layout.html new file mode 100644 index 000000000..9a06cbd99 --- /dev/null +++ b/main/volta_layout_macro/macro.layout.html @@ -0,0 +1,23 @@ +layout!() { /* proc-macro */ }
A macro for defining Volta directory layout hierarchies.
+The syntax of layout!
takes the form:
layout! {
+ LayoutStruct*
+}
+
The syntax of a LayoutStruct
takes the form:
Attribute* Visibility "struct" Ident Directory
+
The syntax of a Directory
takes the form:
{
+ (FieldPrefix)FieldContents*
+}
+
The syntax of a FieldPrefix
takes the form:
LitStr ":" Ident
+
The syntax of a FieldContents
is either:
";"
+
or:
+Directory
+
pub fn main()
pub enum Error {
+ Volta(VoltaError),
+ Tool(i32),
+}
pub fn ensure_layout() -> Result<(), Error>
pub trait IntoResult<T> {
+ // Required method
+ fn into_result(self) -> Result<T, Error>;
+}
pub fn main()