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(&current);
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}