reups_lib/setup.rs
1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 * Copyright Nate Lust 2018*/
5
6/*!
7 Setup is the subcommand responsible for adding products to a users
8 environment based on the options provided.
9*/
10
11use fnv::FnvHashMap;
12
13use std::env;
14use std::fs;
15use std::path::PathBuf;
16
17use crate::argparse;
18use crate::db;
19use crate::logger;
20use crate::table;
21
22// Determine the system on which this comand is run. In eups past there used to be
23// more flavors (i.e. just linux) but these systems are almost never used and are
24// dropped from consideration in reups.
25#[cfg(target_os = "macos")]
26static SYSTEM_OS: &str = "Darwin64";
27#[cfg(target_os = "linux")]
28static SYSTEM_OS: &str = "Linux64";
29
30/// Given a product's version and table file, this function creates all the appropriate
31/// environment variable entries given the supplied options.
32///
33/// * product_version: The version of the product being setup
34/// * product_table: The table file object for the product being setup
35/// * env_vars: HashMap of environment variables with keys equal to the variable name, and values
36/// equal to the value of the variable.
37/// * keep: bool that controls if this should overwirte a product which already exists in the environment or not
38fn setup_table(
39 product_version: &String,
40 product_table: &table::Table,
41 env_vars: &mut FnvHashMap<String, String>,
42 keep: bool,
43 flavor: &String,
44 db_path: PathBuf,
45) {
46 // set the setup env var
47 let mut setup_var = String::from("SETUP_");
48 // set the product directory
49 let mut prod_dir_label = product_table.name.clone();
50 prod_dir_label = prod_dir_label.replace(" ", "_");
51 prod_dir_label = prod_dir_label.to_uppercase();
52 setup_var.push_str(prod_dir_label.as_str());
53 prod_dir_label.push_str("_DIR");
54
55 // get the current env var correspoinding to this prod dir
56 let prod_dir_env = env::var(&prod_dir_label);
57
58 // If told to keep existing products, and those products are in the env in some fashion return
59 // immediately
60 if keep && (env_vars.contains_key(&prod_dir_label) || prod_dir_env.is_ok()) {
61 return;
62 }
63
64 // add this product in to the environment map that is to be setup
65 let mut setup_string_vec = vec![product_table.name.clone(), product_version.clone()];
66
67 // if there is no flavor use the system os as platform
68 setup_string_vec.push("-f".to_string());
69 if flavor.is_empty() {
70 setup_string_vec.push(SYSTEM_OS.to_string());
71 } else {
72 setup_string_vec.push(flavor.clone());
73 }
74
75 // Set db dir to none if there is no db dir (local setup)
76 setup_string_vec.push("-Z".to_string());
77 if db_path.to_str().unwrap().is_empty() {
78 setup_string_vec.push("\\(none\\)".to_string());
79 } else {
80 setup_string_vec.push(db_path.to_str().unwrap().to_string().replace("ups_db", ""));
81 }
82 crate::info!(
83 "Setting up: {:<25}Version: {}",
84 product_table.name,
85 product_version
86 );
87 env_vars.insert(
88 prod_dir_label,
89 String::from(product_table.product_dir.to_str().unwrap()),
90 );
91 env_vars.insert(setup_var, setup_string_vec.join("\\ "));
92
93 // iterate over all environment variables, values in the supplied table
94 for (k, v) in product_table.env_var.iter() {
95 // look up the specific env var specified in the table in the env_vars hashmap passed into
96 // this function. If there is no existing variable in the hash map, check the environment
97 // that was present when the program was executed. If it is found no where, return None
98 // to mark there is no existing variables.
99 let mut existing_var = match env_vars.get(k) {
100 Some(existing) => existing.clone(),
101 None => match env::var(k) {
102 Ok(r) => r,
103 Err(_) => String::from(""),
104 },
105 };
106
107 // if the prod_dir_env is not none, then the value of this variable should be removed from all
108 // existing env var values before being set again, to prevent the variable from growing out
109 // of control
110 //
111 // Variables to mark the start and end position of where the prod_dir_env value is found in
112 // the value of the environment variable (k). AKA LD_LIBRARY_PATH is a long string, find
113 // the location of the substring corresponding to the value of prod_dir_env
114 let mut start_pos = 0;
115 let mut end_pos = 0;
116 // Check if there was a current value set in the environment
117 if let Ok(prod_text) = prod_dir_env.as_ref() {
118 // Find the start position of the text
119 let start_pos_option = existing_var.find(prod_text.as_str());
120 // check if a start position was found
121 if let Some(tmp_start) = start_pos_option {
122 start_pos = tmp_start;
123 // iterate character by character until either a : or the end of the string is
124 // encountered. If one is found, get the end point plus one (+1 so that the
125 // character is encluded in the subsiquent removal, as the end point in that
126 // function call is not inclusive)
127 for (i, character) in existing_var[tmp_start..].chars().enumerate() {
128 let glob_index = tmp_start + i;
129 if character == ':' || glob_index == existing_var.len() {
130 end_pos = glob_index + 1;
131 break;
132 }
133 }
134 }
135 // If an end point was found that means the string was found and has bounds.
136 // Replace the range of the string with an empty str
137 if end_pos != 0 {
138 existing_var.replace_range(start_pos..end_pos, "");
139 }
140 }
141
142 // check the action type and appropriately add the new value onto the env variable
143 // under investigation in this loop
144 let output_var = match v {
145 (table::EnvActionType::Prepend, var) => [var.clone(), existing_var].join(":"),
146 (table::EnvActionType::Append, var) => [existing_var, var.clone()].join(":"),
147 };
148
149 // Add the altered string back into the hash map of all env vars
150 env_vars.insert(k.clone(), output_var);
151 }
152}
153
154/**
155 * If tables are specified as a filesystem path, this function attempts to load and return the
156 * table file.
157 *
158 * Valid input paths are the table file exactly, the path to the ups directory containing the
159 * table, or the path to the directory containing the ups directory
160 */
161fn get_table_path_from_input(input_path: &str) -> Option<table::Table> {
162 let mut input_pathbuf = PathBuf::from(input_path);
163 // check if the full path to the table file was given
164 let mut table_path: Option<PathBuf> = None;
165 let mut prod_dir: Option<PathBuf> = None;
166 if input_pathbuf.is_file() {
167 if let Some(extension) = input_pathbuf.extension() {
168 // if this is true, then the input path is the table path
169 if extension.to_str().unwrap() == "table" {
170 table_path = Some(input_pathbuf.clone());
171 // assumes this is {prod_dir}/ups/something.table
172 let mut tmp_prod_dir = input_pathbuf.clone();
173 tmp_prod_dir.pop();
174 tmp_prod_dir.pop();
175 prod_dir = Some(tmp_prod_dir);
176 }
177 }
178 } else if input_pathbuf.is_dir() {
179 // The supplied path is a directory, it should be checked if it is an ups directory
180 // or a directory containing an ups directory
181 let mut search_path: Option<PathBuf> = None;
182 if input_pathbuf.ends_with("ups") {
183 search_path = Some(input_pathbuf.clone());
184 }
185 input_pathbuf.push("ups");
186 if input_pathbuf.is_dir() {
187 search_path = Some(input_pathbuf);
188 }
189 // need to scan the search dir for the table file
190 if !search_path.is_none() {
191 for entry in fs::read_dir(&search_path.unwrap()).unwrap() {
192 let entry = entry.unwrap();
193 if let Some(extension) = entry.path().extension() {
194 if extension.to_str().unwrap() == "table" {
195 table_path = Some(entry.path());
196 let mut tmp_prod_dir = entry.path();
197 tmp_prod_dir.pop();
198 tmp_prod_dir.pop();
199 prod_dir = Some(tmp_prod_dir);
200 }
201 }
202 }
203 }
204 }
205 if let Some(table_file) = table_path {
206 let table_file = table_file.canonicalize().unwrap();
207 let prod_dir = prod_dir.unwrap().canonicalize().unwrap();
208 let name = String::from(table_file.file_stem().unwrap().to_str().unwrap());
209 Some(table::Table::new(name, table_file, prod_dir).unwrap())
210 } else {
211 return None;
212 }
213}
214
215/** Function to ensure a supplied path is not a relative path
216 *
217 * * input - A string that represents a path to be normalized
218 *
219 * Returns a normalized path, or an error if the supplied input does not correspond to a file
220 * system path, or there was some issue interacting with the file system.
221 **/
222fn normalize_path(input: String) -> Result<String, std::io::Error> {
223 let tmp_path = PathBuf::from(input).canonicalize()?;
224 let err = std::io::Error::new(std::io::ErrorKind::Other, "Problem normalizing Path");
225 let tmp_string = tmp_path.to_str().ok_or(err)?;
226 Ok(String::from(tmp_string))
227}
228
229/**
230 * Gets the arguments used to invoke this subcommand from the command line, ensures all paths are
231 * normalized, and formats these arguments into a single string
232 **/
233fn get_command_string() -> String {
234 // marker to indicate the next argument is a path that should be normalized
235 let mut marker = false;
236 // String to accumulate the input arguments into
237 let mut command_arg = String::new();
238 // Make the switches to check a vector, so future switches can be added easily
239 // This represents a switch where the following argument will be a path to be
240 // normalized
241 let switches = vec!["-r"];
242 for arg in env::args() {
243 let next_string: String = match marker {
244 true => {
245 // if the marker is set, normalize the current arg and return it, setting
246 // marker to false
247 marker = false;
248 normalize_path(arg).unwrap()
249 }
250 false => {
251 // The marker is not set, check if the current argument is a desired
252 // switch and if so set marker so the next argument will be normalzied
253 if switches.contains(&arg.as_str()) {
254 marker = true;
255 }
256 // return argument
257 arg
258 }
259 };
260 // push the current argument onto our accumulated string
261 command_arg.push_str(format!("{} ", next_string.as_str()).as_str());
262 }
263 // pop off the trailing white space
264 command_arg.pop();
265 command_arg
266}
267
268/**
269 * This function takes in arguments parsed from the command line, parses them for products to setup
270 * and options to use during the setup, and sets up the specified product in the
271 * environment.
272 *
273 * Because of the way environments work, this function itself actually only returns a string
274 * containing all the environment variables to be setup. To actually have the variables added to
275 * the environment, this command must be used in combination with the rsetup shell function.
276 */
277pub fn setup_command(sub_args: &argparse::ArgMatches, _main_args: &argparse::ArgMatches) {
278 // Here we will process any of the global arguments in the future but for now there is
279 // nothing so we do nothing but create the database. The global arguments might affect
280 // construction in the future
281 logger::build_logger(sub_args, true);
282 let db = db::DB::new(None, None, None, None);
283
284 // We process local arguments here to set the state that will be used to setup a product
285 // Create a vector for the tags to consider
286 let current = String::from("current");
287 let mut tags_str = vec![];
288 let mut tags = vec![];
289 if sub_args.is_present("tag") {
290 for t in sub_args.values_of("tag").unwrap() {
291 tags_str.push(t.to_string());
292 }
293 for t in tags_str.iter() {
294 tags.push(t);
295 }
296 }
297 crate::info!("Using tags: {:?}", tags);
298 // Always put the current tag
299 tags.push(¤t);
300
301 let product = sub_args.value_of("product");
302 // Get if the command should be run in exact or inexact mode
303 let mut mode = table::VersionType::Exact;
304 if sub_args.is_present("inexact") {
305 mode = table::VersionType::Inexact;
306 }
307
308 // Match to determine if a product or relative path was given by the user
309 let table_option = match (product, sub_args.value_of("relative")) {
310 (Some(name), _) => {
311 if !db.has_product(&name.to_string()) {
312 exit_with_message!(format!("Cannot find product `{}` to setup", name));
313 }
314 let local_table = db.get_table_from_tag(&name.to_string(), tags.clone());
315 let versions = db.get_versions_from_tag(&name.to_string(), tags.clone());
316 let mut version = String::from("");
317 match versions.last() {
318 Some(v) => {
319 version = v.clone();
320 }
321 None => (),
322 }
323 (local_table, version)
324 }
325 (None, Some(path)) => {
326 // specifying a directory of table file to setup manually implies that version type
327 // should be set to Inexact
328 let table = get_table_path_from_input(path);
329 let mut version = String::from("");
330 if table.is_some() {
331 let mut tmp = String::from("LOCAL:");
332 tmp.push_str(table.as_ref().unwrap().path.to_str().unwrap());
333 version = tmp
334 }
335 mode = table::VersionType::Inexact;
336 (table, version)
337 }
338 _ => (None, String::from("")),
339 };
340
341 // Determine if the user wants existing dependencies to be kept in the environment
342 // or replaced
343 let keep = sub_args.is_present("keep");
344
345 // If there is a valid table and version found, determine dependencies and setup
346 // the product
347 if let (Some(table), version) = table_option {
348 // If someone specified the just flag, don't look up any dependencies
349 let mut deps: Option<db::graph::Graph> = None;
350 if !sub_args.is_present("just") {
351 let mut dep_graph = db::graph::Graph::new(&db);
352 dep_graph.add_table(
353 &table,
354 mode,
355 db::graph::NodeType::Required,
356 Some(&tags),
357 true,
358 );
359
360 deps = Some(dep_graph);
361 }
362 // create a hashmap to hold all the environment variables to set
363 let mut env_vars: FnvHashMap<String, String> = FnvHashMap::default();
364 let flavors = db.get_flavors_from_version(&table.name, &version);
365 let flavor = match flavors.last() {
366 Some(flav) => flav.clone(),
367 None => String::from(""),
368 };
369
370 // Determine the path to the database that contains this product
371 let db_dirs = db.get_db_directories();
372 // This works because there are 2 dbs, the first is always the system one
373 // the second is always the user db. and we always want to take the entry from
374 // the end if it exists, in this case that is the flavor entry
375 let db_path = match flavors.len() {
376 1 => db_dirs[0].clone(),
377 2 => db_dirs[1].clone(),
378 _ => PathBuf::from(""), // Needed to satisfy rust matching
379 };
380
381 // Keep should always be false for the first product to setup, as this is the
382 // directory the user specified, so clearly they want to set it up.
383 setup_table(&version, &table, &mut env_vars, false, &flavor, db_path);
384
385 // If there are dependencies, then set them up as well
386 if let Some(dependencies) = deps {
387 // Skip the root node, as it is what is setup
388 for node in dependencies.iter().skip(1) {
389 let name = dependencies.get_name(node);
390 let versions = dependencies.product_versions(&name);
391 // right now we find the largest version from the graph and set that up, as it is
392 // easiest, but it could be wrong and this code should be thought through more.
393 // FINDME
394 let mut largest_version = versions.iter().max().unwrap().clone().clone();
395 let node_table_option: Option<table::Table>;
396 if largest_version.as_str() != "" {
397 node_table_option = db.get_table_from_version(&name, &largest_version);
398 } else {
399 node_table_option = db.get_table_from_tag(&name, tags.clone());
400 let versions = db.get_versions_from_tag(&name, tags.clone());
401 match versions.last() {
402 Some(v) => {
403 largest_version = v.clone();
404 }
405 None => (),
406 }
407 }
408 match (node_table_option, dependencies.is_optional(&name)) {
409 (Some(node_table), _) => {
410 let flavors =
411 db.get_flavors_from_version(&node_table.name, &largest_version);
412 let flavor = match flavors.last() {
413 Some(flav) => flav.clone(),
414 None => String::from(""),
415 };
416 // This works because there are 2 dbs, the first is always the system one
417 // the second is always the user db. and we always want to take the entry from
418 // the end if it exists, in this case that is the flavor entry
419 let db_path = match flavors.len() {
420 1 => db_dirs[0].clone(),
421 2 => db_dirs[1].clone(),
422 _ => PathBuf::from(""), // Needed to satisfy rust matching
423 };
424 setup_table(
425 &largest_version,
426 &node_table,
427 &mut env_vars,
428 keep,
429 &flavor,
430 db_path,
431 )
432 }
433 (None, true) => continue,
434 (None, false) => {
435 if env::var(String::from("SETUP_") + &name.to_uppercase()).is_ok() {
436 crate::warn!("Product {} could not be found in the database, resolving dependency using setup version", &name);
437 continue;
438 } else {
439 exit_with_message!(format!(
440 "Cannot find any acceptable table for {}",
441 &name
442 ));
443 }
444 }
445 }
446 }
447 }
448
449 // Add or update env var for reups history
450 let current_reups_command = get_command_string();
451 // If there is an existing reups history environment variable append to it
452 // separating with a pipe character. else return a new string for the env
453 // var. Both make sure the string to be set as an environment variable are
454 // quoted so that all spaces are preserved
455 let reups_history_string = match env::var("REUPS_HISTORY") {
456 Ok(existing) => format!("\"{}|{}\"", existing, current_reups_command),
457 _ => format!("\"{}\"", current_reups_command),
458 };
459 let reups_history_key = String::from("REUPS_HISTORY");
460 // insert into the in memory map of environment variables to values
461 env_vars.insert(reups_history_key, reups_history_string);
462 // Process all the environment variables into a string to return
463 let mut return_string = String::from("export ");
464 for (k, v) in env_vars {
465 return_string.push_str([k, v].join("=").as_str());
466 return_string.push_str(" ");
467 }
468 println!("{}", return_string);
469 } else {
470 exit_with_message!(
471 "Error, no product to setup, please specify product or path to table with -r"
472 );
473 }
474}