├── .gitignore ├── .travis.yml ├── CHANGELOG ├── Cargo.toml ├── LICENSE ├── README.md └── src ├── config.rs ├── dependencies.rs ├── dirs.rs ├── main.rs ├── output.rs ├── rt_result.rs ├── tags.rs └── types.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | rust: 3 | - stable 4 | - beta 5 | - nightly 6 | - 1.41.1 7 | 8 | notifications: 9 | email: 10 | on_success: never 11 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | 3.11.0 2 | ------ 3 | * Support format change of ids in the subcomand metadata of cargo 1.78 4 | 5 | 3.10.0 6 | ------ 7 | * add async support to regex for excuberant ctags 8 | 9 | 3.9.0 10 | ----- 11 | * optimize tags creation for huge dependency tree 12 | 13 | 3.8.1 14 | ----- 15 | * Beautify error format 16 | 17 | 3.8.0 18 | ----- 19 | * Fix standard library tags creation for rustc >= 1.47.0 20 | 21 | 3.7.0 22 | ----- 23 | * Fix '--omit-deps' option 24 | 25 | 3.6.0 26 | ----- 27 | * Reduce the number of open files 28 | * Fix the order in which tags for sources are created 29 | 30 | 3.5.1 31 | ----- 32 | * Better handling of empty ctags_exe config, by using the default ctag exe names 33 | 34 | 3.5.0 35 | ----- 36 | * Add option -O/--output for naming tags files 37 | * Increased minimum rustc version to 1.24.1 38 | 39 | 3.4.0 40 | ----- 41 | * Fix temporary file creation errors on Windows 10 42 | * Create tags for crates of kind 'test' 43 | * Increased minimum rustc version to 1.22.0 44 | 45 | 3.3.0 46 | ----- 47 | * Add indexing of 'unsafe Trait' for 'excuberant ctags' 48 | * Use 'home_dir' of 'dirs' crate instead of 'env::home_dir' 49 | * Don't expect a 'Cargo.toml' in an ancestor directory of the source 50 | 51 | 3.2.0 52 | ----- 53 | * Further optimizations for dependency heavy cargo projects 54 | * Now really - hopefully for all times - fix handling of cyclic dependencies 55 | * Ensure support to at least rustc 1.20.0 56 | 57 | 3.1.0 58 | ----- 59 | * Further optimizations for dependency heavy cargo projects 60 | * Correctly update emacs style tags with missing included tag files 61 | * Handling of multiple versions of the same library 62 | 63 | 3.0.0 64 | ----- 65 | * Several optimizations for dependency heavy cargo projects 66 | 67 | 2.11.0 68 | ------ 69 | * Extend verbose output 70 | 71 | 2.10.0 72 | ------ 73 | * Add config option for ctags executable path 74 | * Add ctags executable names used by FreeBSD 75 | 76 | 2.9.0 77 | ----- 78 | * Create tags for any kind of lib: lib, dylib, staticlib, cdylib or rlib 79 | 80 | 2.8.0 81 | ----- 82 | * Support target kind 'staticlib' as tags root 83 | 84 | 2.7.0 85 | ----- 86 | * Upgrading dependencies 87 | 88 | 2.6.0 89 | ----- 90 | * Detect universal ctags and call it with just "--languages=Rust" and without any regexes, 91 | because it already supports Rust and the regexes only slow down the tags creation. 92 | 93 | Universal ctags supports the creation of tags for struct fields and enum variants out 94 | of the box, which isn't possible with the regex based approach. 95 | 96 | * Add configuration option 'ctags_options' in '~/.rusty-tags/config.toml'. The 'ctags_options' 97 | are given as options to the ctags executable. 98 | 99 | E.g. I'm using universal ctags but don't like tags for impls, so I've set 'ctags_options = "--Rust-kinds=-c"'. 100 | 101 | 2.5.1 102 | ----- 103 | * Only README updates 104 | 105 | 2.5.0 106 | ----- 107 | * Ensure that the cached tags of local dependencies - which are developed in conjunction 108 | with the cargo project - get updated on source changes. 109 | 110 | 2.4.0 111 | ----- 112 | * Multi threaded creation of tags (--num-threads) 113 | * Add a lock file during the tags creation of a cargo project 114 | 115 | 2.3.0 116 | ----- 117 | * Add option to omit building tags for dependencies (--omit-deps) 118 | 119 | 2.2.0 120 | ----- 121 | * Support the creation of tags for procedural macro crates 122 | 123 | 2.1.0 124 | ----- 125 | * Ensure that files aren't moved between devices 126 | 127 | 2.0.0 128 | ----- 129 | * Complete rewrite of the dependency source path resolution 130 | 131 | 1.3.0 132 | ----- 133 | * Fix cargo search path for git dependencies 134 | * Don't stop the tags creation on missing sources 135 | 136 | 1.2.0 137 | ----- 138 | * Only create tags for rust files under the 'src' directory 139 | 140 | 1.1.1 141 | ----- 142 | * Try to support cargo workspaces 143 | * Better handling of missing 'cargo' executable 144 | * Better handling of missing '~/.cargo' directory 145 | 146 | 1.1.0 147 | ----- 148 | * Fix issues with moving of tag files across filesystem/partition boundaries 149 | 150 | 1.0.1 151 | ----- 152 | * Handle missing platform specific dependencies 153 | 154 | 1.0.0 155 | ----- 156 | * Always handle reexports of dependencies correctly, not only for the direct dependencies 157 | 158 | * Make tags file creation safe, which allows the running of multiple 159 | rusty-tags processes at once without interfering with each other 160 | 161 | 0.10.0 162 | ------ 163 | * Support configuration of tags file name with '~/.rusty-tags/config.toml' 164 | 165 | 0.9.3 166 | ----- 167 | * Update dependencies 168 | 169 | 0.9.2 170 | ----- 171 | * Better error messages for failed executions of ctags/git 172 | 173 | 0.9.1 174 | ----- 175 | * Better handling of failed ctags execution 176 | 177 | 0.9.0 178 | ----- 179 | * Now needs at least rust 1.5 for building 180 | 181 | 0.8.2 182 | ----- 183 | * Support CARGO_HOME environment variable 184 | 185 | 0.8.1 186 | ----- 187 | * Only create tags for module definitions 188 | 189 | 0.8.0 190 | ----- 191 | * Build tags for rust standard library 192 | * Support 'rustup' beta 193 | * Remove tags creation for 'impl' 194 | * Only create tags for files under 'src' directory 195 | 196 | 0.7.0 197 | ----- 198 | * Now really handle empty 'dependencies' in 'Cargo.toml' 199 | 200 | 0.6.8 201 | ----- 202 | * Add '--start-dir' option 203 | 204 | 0.6.6 205 | ----- 206 | * Support dev/build dependencies 207 | * More complete support of dependency definitions in 'Cargo.toml' 208 | 209 | 0.6.5 210 | ----- 211 | * Add '--verbose' and '--quiet' options 212 | 213 | 0.6.4 214 | ----- 215 | * Add option --force-recreate 216 | * Support first level local path dependencies 217 | 218 | 0.6.2 219 | ----- 220 | * Determine rusty-tags and cargo directories only once 221 | 222 | 0.6.1 223 | ----- 224 | * Handle empty 'dependencies' entry in 'Cargo.toml' 225 | 226 | 0.6.0 227 | ----- 228 | * Support multirust 229 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | 3 | name = "rusty-tags" 4 | version = "3.11.0" 5 | authors = ["Daniel Trstenjak "] 6 | license = "BSD-3-Clause" 7 | description = "Create ctags/etags for a cargo project and all of its dependencies" 8 | repository = "https://github.com/dan-t/rusty-tags" 9 | readme = "README.md" 10 | 11 | [dependencies] 12 | toml = "0.5" 13 | clap = "2.32.0" 14 | lazy_static = "1.2.0" 15 | tempfile = "3.0.6" 16 | scoped_threadpool = "0.1.9" 17 | num_cpus = "1.10.0" 18 | serde = "1.0.87" 19 | serde_derive = "1.0.87" 20 | serde_json = "1.0.38" 21 | fnv = "1.0.6" 22 | semver = "0.9.0" 23 | dirs = "2.0" 24 | 25 | [profile.release] 26 | lto = "fat" 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011, Daniel Trstenjak 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL DANIEL TRSTENJAK BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/dan-t/rusty-tags.svg?branch=master)](https://travis-ci.org/dan-t/rusty-tags) 2 | [![](http://meritbadge.herokuapp.com/rusty-tags)](https://crates.io/crates/rusty-tags) 3 | 4 | rusty-tags 5 | ========== 6 | 7 | A command line tool that creates [tags](https://en.wikipedia.org/wiki/Ctags) - for source code navigation by 8 | using [ctags]() - for a [cargo]() project, all 9 | of its direct and indirect dependencies and the rust standard library. 10 | 11 | Prerequisites 12 | ============= 13 | 14 | * [ctags]() installed, needs a version with the `--recurse` flag 15 | 16 | On a linux system the package is most likely called `exuberant-ctags`. 17 | 18 | Otherwise you can get the sources directly from [here](http://ctags.sourceforge.net/) or use the newer and alternative 19 | [universal-ctags](https://github.com/universal-ctags/ctags). 20 | 21 | Only `universal-ctags` will add tags for struct fields and enum variants. 22 | 23 | Installation 24 | ============ 25 | 26 | $ cargo install rusty-tags 27 | 28 | The build binary will be located at `~/.cargo/bin/rusty-tags`. 29 | 30 | Usage 31 | ===== 32 | 33 | Just calling `rusty-tags vi` or `rusty-tags emacs` anywhere inside 34 | of the cargo project should just work. 35 | 36 | After its run a `rusty-tags.vi / rusty-tags.emacs` file should be beside of the 37 | `Cargo.toml` file. 38 | 39 | Additionally every dependency gets a tags file at its source directory, so 40 | jumping further to its dependencies is possible. 41 | 42 | Rust Standard Library Support 43 | ============================= 44 | 45 | Tags for the standard library are created if the rust source is supplied by 46 | defining the environment variable `RUST_SRC_PATH`. 47 | 48 | These tags aren't automatically added to the tags of the cargo project and have 49 | to be added manually with the path `$RUST_SRC_PATH/rusty-tags.vi` or 50 | `$RUST_SRC_PATH/rusty-tags.emacs`. 51 | 52 | If you're using [rustup]() you can get the 53 | rust source of the currently used compiler version by calling: 54 | 55 | $ rustup component add rust-src 56 | 57 | And then setting `RUST_SRC_PATH` inside of e.g. `~/.bashrc`. 58 | 59 | For `rustc >= 1.47.0`: 60 | 61 | $ export RUST_SRC_PATH=$(rustc --print sysroot)/lib/rustlib/src/rust/library/ 62 | 63 | For `rustc < 1.47.0`: 64 | 65 | $ export RUST_SRC_PATH=$(rustc --print sysroot)/lib/rustlib/src/rust/src/ 66 | 67 | Configuration 68 | ============= 69 | 70 | The current supported configuration at `~/.rusty-tags/config.toml` (defaults displayed): 71 | 72 | # the file name used for vi tags 73 | vi_tags = "rusty-tags.vi" 74 | 75 | # the file name used for emacs tags 76 | emacs_tags = "rusty-tags.emacs" 77 | 78 | # the name or path to the ctags executable, by default executables with names 79 | # are searched in the following order: "ctags", "exuberant-ctags", "exctags", "universal-ctags", "uctags" 80 | ctags_exe = "" 81 | 82 | # options given to the ctags executable 83 | ctags_options = "" 84 | 85 | Vim Configuration 86 | ================= 87 | 88 | Put this into your `~/.vimrc` file: 89 | 90 | autocmd BufRead *.rs :setlocal tags=./rusty-tags.vi;/ 91 | 92 | Or if you've supplied the rust source code by defining `RUST_SRC_PATH`: 93 | 94 | autocmd BufRead *.rs :setlocal tags=./rusty-tags.vi;/,$RUST_SRC_PATH/rusty-tags.vi 95 | 96 | And: 97 | 98 | autocmd BufWritePost *.rs :silent! exec "!rusty-tags vi --quiet --start-dir=" . expand('%:p:h') . "&" | redraw! 99 | 100 | Emacs Configuration 101 | =================== 102 | 103 | Install [counsel-etags](https://github.com/redguardtoo/counsel-etags). 104 | 105 | Create file `.dir-locals.el` in rust project root (please note the line to set `counsel-etags-extra-tags-files` is optional): 106 | 107 | ((nil . ((counsel-etags-update-tags-backend . (lambda (src-dir) (shell-command "rusty-tags emacs"))) 108 | (counsel-etags-extra-tags-files . ("~/third-party-lib/rusty-tags.emacs" "$RUST_SRC_PATH/rusty-tags.emacs")) 109 | (counsel-etags-tags-file-name . "rusty-tags.emacs")))) 110 | 111 | Use `M-x counsel-etags-find-tag-at-point` for code navigation. 112 | 113 | `counsel-etags` will automatically detect and update tags file in project root. So no extra setup is required. 114 | 115 | Sublime Configuration 116 | ===================== 117 | 118 | The plugin [CTags](https://github.com/SublimeText/CTags) uses vi style tags, so 119 | calling `rusty-tags vi` should work. 120 | 121 | By default it expects tag files with the name `.tags`, which can be set 122 | in `~/.rusty-tags/config.toml`: 123 | 124 | vi_tags = ".tags" 125 | 126 | Or by calling `rusty-tags vi --output=".tags"`. 127 | 128 | MacOS Issues 129 | ============ 130 | 131 | Mac OS users may encounter problems with the execution of `ctags` because the shipped version 132 | of this program does not support the recursive flag. See [this posting]() 133 | for how to install a working version with homebrew. 134 | 135 | Cygwin/Msys Issues 136 | ================== 137 | 138 | If you're running [Cygwin]() or [Msys]() under Windows, 139 | you might have to set the environment variable `$CARGO_HOME` explicitly. Otherwise you might get errors 140 | when the tags files are moved. 141 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::path::{Path, PathBuf}; 3 | use std::fs::File; 4 | use std::io::Read; 5 | use std::cmp::max; 6 | use std::process::Command; 7 | use clap::App; 8 | use types::{TagsExe, TagsKind, TagsSpec}; 9 | use rt_result::RtResult; 10 | use dirs; 11 | use tempfile::TempDir; 12 | 13 | /// the configuration used to run rusty-tags 14 | pub struct Config { 15 | /// the tags that should be created 16 | pub tags_spec: TagsSpec, 17 | 18 | /// start directory for the search of the 'Cargo.toml' 19 | pub start_dir: PathBuf, 20 | 21 | /// output directory for the tags for the standard library 22 | pub output_dir_std: Option, 23 | 24 | /// do not generate tags for dependencies 25 | pub omit_deps: bool, 26 | 27 | /// forces the recreation of cached tags 28 | pub force_recreate: bool, 29 | 30 | /// verbose output about all operations 31 | pub verbose: bool, 32 | 33 | /// don't output anything but errors 34 | pub quiet: bool, 35 | 36 | /// num threads used for the tags creation 37 | pub num_threads: u32, 38 | 39 | /// temporary directory for created tags 40 | temp_dir: TempDir 41 | } 42 | 43 | impl Config { 44 | pub fn from_command_args() -> RtResult { 45 | let matches = App::new("rusty-tags") 46 | .about("Create ctags/etags for a cargo project and all of its dependencies") 47 | // Pull version from Cargo.toml 48 | .version(crate_version!()) 49 | .author("Daniel Trstenjak ") 50 | .arg_from_usage(" 'The kind of the created tags (vi, emacs)'") 51 | .arg_from_usage("-s --start-dir [DIR] 'Start directory for the search of the Cargo.toml (default: current working directory)'") 52 | .arg_from_usage("--output-dir-std [DIR] 'Set the output directory for the tags for the Rust standard library (default: $RUST_SRC_PATH)'") 53 | .arg_from_usage("-o --omit-deps 'Do not generate tags for dependencies'") 54 | .arg_from_usage("-f --force-recreate 'Forces the recreation of the tags of all dependencies and the Rust standard library'") 55 | .arg_from_usage("-v --verbose 'Verbose output about all operations'") 56 | .arg_from_usage("-q --quiet 'Don't output anything but errors'") 57 | .arg_from_usage("-n --num-threads [NUM] 'Num threads used for the tags creation (default: num available physical cpus)'") 58 | .arg_from_usage("-O --output [FILENAME] 'Name of output tags file.'") 59 | .get_matches(); 60 | 61 | let start_dir = matches.value_of("start-dir") 62 | .map(PathBuf::from) 63 | .unwrap_or(env::current_dir()?); 64 | 65 | if ! start_dir.is_dir() { 66 | return Err(format!("Invalid directory given to '--start-dir': '{}'!", start_dir.display()).into()); 67 | } 68 | 69 | let output_dir_std = matches.value_of("output-dir-std").map(PathBuf::from); 70 | 71 | if let Some(ref output_dir_std) = output_dir_std { 72 | if ! output_dir_std.is_dir() { 73 | return Err(format!("Invalid directory given to '--output-dir-std': '{}'!", output_dir_std.display()).into()); 74 | } 75 | } 76 | 77 | let kind = value_t_or_exit!(matches.value_of("TAGS_KIND"), TagsKind); 78 | 79 | let (vi_tags, emacs_tags, ctags_exe, ctags_options) = { 80 | let mut vt = "rusty-tags.vi".to_string(); 81 | let mut et = "rusty-tags.emacs".to_string(); 82 | let mut cte = None; 83 | let mut cto = "".to_string(); 84 | 85 | // Override defaults with file config 86 | if let Some(file_config) = ConfigFromFile::load()? { 87 | if let Some(fcvt) = file_config.vi_tags { vt = fcvt; } 88 | if let Some(fcet) = file_config.emacs_tags { et = fcet; } 89 | cte = file_config.ctags_exe; 90 | if let Some(fccto) = file_config.ctags_options { cto = fccto; } 91 | } 92 | 93 | // Override defaults with commandline options 94 | if let Some(cltf) = matches.value_of("output") { 95 | match kind { 96 | TagsKind::Vi => vt = cltf.to_string(), 97 | TagsKind::Emacs => et = cltf.to_string() 98 | } 99 | } 100 | 101 | (vt, et, cte, cto) 102 | }; 103 | 104 | let omit_deps = matches.is_present("omit-deps"); 105 | let force_recreate = matches.is_present("force-recreate"); 106 | let quiet = matches.is_present("quiet"); 107 | let verbose = if quiet { false } else { matches.is_present("verbose") }; 108 | 109 | let num_threads = if verbose { 110 | println!("Switching to single threaded for verbose output"); 111 | 1 112 | } else { 113 | value_t!(matches.value_of("num-threads"), u32) 114 | .map(|n| max(1, n)) 115 | .unwrap_or(num_cpus::get_physical() as u32) 116 | }; 117 | 118 | if verbose { 119 | println!("Using configuration: vi_tags='{}', emacs_tags='{}', ctags_exe='{:?}', ctags_options='{}'", 120 | vi_tags, emacs_tags, ctags_exe, ctags_options); 121 | } 122 | 123 | let ctags_exe = detect_tags_exe(&ctags_exe)?; 124 | if verbose { 125 | println!("Found ctags executable: {:?}", ctags_exe); 126 | } 127 | 128 | Ok(Config { 129 | tags_spec: TagsSpec::new(kind, ctags_exe, vi_tags, emacs_tags, ctags_options)?, 130 | start_dir: start_dir, 131 | output_dir_std: output_dir_std, 132 | omit_deps: omit_deps, 133 | force_recreate: force_recreate, 134 | verbose: verbose, 135 | quiet: quiet, 136 | num_threads: num_threads, 137 | temp_dir: TempDir::new()? 138 | }) 139 | } 140 | 141 | pub fn temp_file(&self, name: &str) -> RtResult { 142 | let file_path = self.temp_dir.path().join(name); 143 | let _ = File::create(&file_path)?; 144 | Ok(file_path) 145 | } 146 | } 147 | 148 | /// Represents the data from a `.rusty-tags/config.toml` configuration file. 149 | #[derive(Deserialize, Debug, Default)] 150 | struct ConfigFromFile { 151 | /// the file name used for vi tags 152 | vi_tags: Option, 153 | 154 | /// the file name used for emacs tags 155 | emacs_tags: Option, 156 | 157 | /// path to the ctags executable 158 | ctags_exe: Option, 159 | 160 | /// options given to the ctags executable 161 | ctags_options: Option 162 | } 163 | 164 | impl ConfigFromFile { 165 | fn load() -> RtResult> { 166 | let config_file = dirs::rusty_tags_dir().map(|p| p.join("config.toml"))?; 167 | if ! config_file.is_file() { 168 | return Ok(None); 169 | } 170 | 171 | let config = map_file(&config_file, |contents| { 172 | let config = toml::from_str(&contents)?; 173 | Ok(config) 174 | })?; 175 | 176 | Ok(Some(config)) 177 | } 178 | } 179 | 180 | /// Reads `file` into a string which is passed to the function `f` 181 | /// and its return value is returned by `map_file`. 182 | fn map_file(file: &Path, f: F) -> RtResult 183 | where F: FnOnce(String) -> RtResult 184 | { 185 | let mut file = File::open(file)?; 186 | 187 | let mut contents = String::new(); 188 | file.read_to_string(&mut contents)?; 189 | 190 | let r = f(contents)?; 191 | Ok(r) 192 | } 193 | 194 | fn detect_tags_exe(ctags_exe: &Option) -> RtResult { 195 | let exes = match *ctags_exe { 196 | Some(ref exe) if exe != "" => vec![exe.as_str()], 197 | _ => vec!["ctags", "exuberant-ctags", "exctags", "universal-ctags", "uctags"] 198 | }; 199 | 200 | for exe in &exes { 201 | let mut cmd = Command::new(exe); 202 | cmd.arg("--version"); 203 | 204 | if let Ok(output) = cmd.output() { 205 | if output.status.success() { 206 | let stdout = String::from_utf8_lossy(&output.stdout); 207 | if stdout.contains("Universal Ctags") { 208 | return Ok(TagsExe::UniversalCtags(exe.to_string())); 209 | } 210 | 211 | return Ok(TagsExe::ExuberantCtags(exe.to_string())); 212 | } 213 | } 214 | } 215 | 216 | Err(format!("Couldn't find 'ctags' executable! Searched for executables with names: {:?}. Is 'ctags' correctly installed?", &exes).into()) 217 | } 218 | -------------------------------------------------------------------------------- /src/dependencies.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | use semver::Version; 4 | use fnv::FnvHashMap; 5 | 6 | use rt_result::RtResult; 7 | use types::{DepTree, Source, SourceId}; 8 | use config::Config; 9 | 10 | type JsonValue = serde_json::Value; 11 | type JsonObject = serde_json::Map; 12 | 13 | /// Returns the dependency tree of the whole cargo workspace. 14 | pub fn dependency_tree(config: &Config, metadata: &JsonValue) -> RtResult { 15 | let mut dep_tree = DepTree::new(); 16 | let packages = packages(config, metadata, &mut dep_tree)?; 17 | 18 | build_dep_tree(config, metadata, &packages, &mut dep_tree)?; 19 | dep_tree.compute_depths(); 20 | 21 | Ok(dep_tree) 22 | } 23 | 24 | fn workspace_members<'a>(metadata: &'a JsonValue) -> RtResult>> { 25 | let members = as_array_from_value("workspace_members", metadata)?; 26 | let mut member_ids = Vec::with_capacity(members.len()); 27 | for member in members { 28 | let member_id = member.as_str() 29 | .ok_or(format!("Expected 'workspace_members' of type string but found: {}", to_string_pretty(member)))?; 30 | 31 | member_ids.push(member_id); 32 | } 33 | 34 | Ok(member_ids) 35 | } 36 | 37 | type PackageId<'a> = &'a str; 38 | 39 | struct Package<'a> { 40 | pub name: &'a str, 41 | pub version: Version, 42 | pub source_id: SourceId, 43 | pub source_path: &'a Path 44 | } 45 | 46 | type Packages<'a> = FnvHashMap, Package<'a>>; 47 | 48 | fn packages<'a>(config: &Config, 49 | metadata: &'a JsonValue, 50 | dep_tree: &mut DepTree) 51 | -> RtResult> { 52 | let packages = as_array_from_value("packages", metadata)?; 53 | dep_tree.reserve_num_sources(packages.len()); 54 | let mut package_map = FnvHashMap::default(); 55 | for package in packages { 56 | let id = as_str_from_value("id", package)?; 57 | let name = as_str_from_value("name", package)?; 58 | let version = Version::parse(as_str_from_value("version", package)?)?; 59 | let source_path = { 60 | let path = source_path(config, package)?; 61 | if path == None { 62 | continue; 63 | } 64 | 65 | path.unwrap() 66 | }; 67 | 68 | verbose!(config, "Found package of {} {} with source at '{}'", name, version, source_path.display()); 69 | 70 | let source_id = dep_tree.new_source(); 71 | package_map.insert(id, Package { name, version, source_id, source_path }); 72 | } 73 | 74 | Ok(package_map) 75 | } 76 | 77 | fn build_dep_tree(config: &Config, 78 | metadata: &JsonValue, 79 | packages: &Packages, 80 | dep_tree: &mut DepTree) 81 | -> RtResult<()> { 82 | let root_ids = { 83 | let members_ids = workspace_members(metadata)?; 84 | verbose!(config, "Found workspace members: {:?}", members_ids); 85 | 86 | let mut source_ids = Vec::with_capacity(members_ids.len()); 87 | for member_id in &members_ids { 88 | let member_package = package(&member_id, packages)?; 89 | source_ids.push(member_package.source_id); 90 | if config.omit_deps { 91 | let is_root = true; 92 | let source = Source::new(member_package.source_id, member_package.name, &member_package.version, 93 | member_package.source_path, is_root, config)?; 94 | dep_tree.set_source(source, vec![]); 95 | } 96 | } 97 | 98 | source_ids 99 | }; 100 | 101 | dep_tree.set_roots(root_ids.clone()); 102 | if config.omit_deps { 103 | return Ok(()); 104 | } 105 | 106 | let nodes = { 107 | let resolve = as_object_from_value("resolve", metadata)?; 108 | as_array_from_object("nodes", resolve)? 109 | }; 110 | 111 | for node in nodes { 112 | let node_id = as_str_from_value("id", node)?; 113 | let node_package = package(&node_id, packages)?; 114 | 115 | let dep_src_ids = { 116 | let dependencies = as_array_from_value("dependencies", node)?; 117 | let dep_pkg_ids = { 118 | let mut pkg_ids = Vec::with_capacity(dependencies.len()); 119 | for dep in dependencies { 120 | let pkg_id = dep.as_str() 121 | .ok_or(format!("Couldn't find string in dependency:\n{}", to_string_pretty(dep)))?; 122 | 123 | pkg_ids.push(pkg_id); 124 | } 125 | 126 | pkg_ids 127 | }; 128 | 129 | if ! dep_pkg_ids.is_empty() { 130 | verbose!(config, "Found dependencies of {} {}: {:?}", node_package.name, node_package.version, dep_pkg_ids); 131 | } 132 | 133 | let mut src_ids = Vec::with_capacity(dep_pkg_ids.len()); 134 | for pkg_id in &dep_pkg_ids { 135 | src_ids.push(package(&pkg_id, packages)?.source_id); 136 | } 137 | 138 | src_ids 139 | }; 140 | 141 | verbose!(config, "Building tree for {} {}", node_package.name, node_package.version); 142 | 143 | let is_root = root_ids.iter().find(|id| **id == node_package.source_id) != None; 144 | let source = Source::new(node_package.source_id, node_package.name, &node_package.version, 145 | node_package.source_path, is_root, config)?; 146 | dep_tree.set_source(source, dep_src_ids); 147 | } 148 | 149 | Ok(()) 150 | } 151 | 152 | fn package<'a>(package_id: &PackageId<'a>, packages: &'a Packages) -> RtResult<&'a Package<'a>> { 153 | packages.get(package_id) 154 | .ok_or(format!("Couldn't find package for id '{}'", package_id).into()) 155 | } 156 | 157 | fn source_path<'a>(config: &Config, package: &'a JsonValue) -> RtResult> { 158 | let targets = as_array_from_value("targets", package)?; 159 | 160 | let manifest_dir = { 161 | let manifest_path = as_str_from_value("manifest_path", package).map(Path::new)?; 162 | 163 | manifest_path.parent() 164 | .ok_or(format!("Couldn't get directory of path '{:?}'", manifest_path.display()))? 165 | }; 166 | 167 | for target in targets { 168 | let kinds = as_array_from_value("kind", target)?; 169 | 170 | for kind in kinds { 171 | let kind_str = kind.as_str() 172 | .ok_or(format!("Expected 'kind' of type string but found: {}", to_string_pretty(kind)))?; 173 | 174 | if kind_str != "bin" && ! kind_str.contains("lib") && kind_str != "proc-macro" && kind_str != "test" { 175 | verbose!(config, "Unsupported target kind: {}", kind_str); 176 | continue; 177 | } 178 | 179 | let mut src_path = as_str_from_value("src_path", target).map(Path::new)?; 180 | if src_path.is_absolute() && src_path.is_file() { 181 | src_path = src_path.parent() 182 | .ok_or(format!("Couldn't get directory of path '{:?}' in target:\n{}\nof package:\n{}", 183 | src_path.display(), to_string_pretty(target), to_string_pretty(package)))?; 184 | } 185 | 186 | if src_path.is_relative() { 187 | src_path = manifest_dir; 188 | } 189 | 190 | if ! src_path.is_dir() { 191 | return Err(format!("Invalid source path directory '{:?}' in target:\n{}\nof package:\n{}", 192 | src_path.display(), to_string_pretty(target), to_string_pretty(package)).into()); 193 | } 194 | 195 | return Ok(Some(src_path)); 196 | } 197 | } 198 | 199 | Ok(None) 200 | } 201 | 202 | fn to_string_pretty(value: &JsonValue) -> String { 203 | serde_json::to_string_pretty(value).unwrap_or(String::new()) 204 | } 205 | 206 | fn as_array_from_value<'a>(entry: &str, value: &'a JsonValue) -> RtResult<&'a Vec> { 207 | value.get(entry) 208 | .and_then(JsonValue::as_array) 209 | .ok_or(format!("Couldn't find array entry '{}' in:\n{}", entry, to_string_pretty(value)).into()) 210 | } 211 | 212 | fn as_str_from_value<'a>(entry: &str, value: &'a JsonValue) -> RtResult<&'a str> { 213 | value.get(entry) 214 | .and_then(JsonValue::as_str) 215 | .ok_or(format!("Couldn't find string entry '{}' in:\n{}", entry, to_string_pretty(value)).into()) 216 | } 217 | 218 | fn as_object_from_value<'a>(entry: &str, value: &'a JsonValue) -> RtResult<&'a JsonObject> { 219 | value.get(entry) 220 | .and_then(JsonValue::as_object) 221 | .ok_or(format!("Couldn't find object entry '{}' in:\n{}", entry, to_string_pretty(value)).into()) 222 | } 223 | 224 | fn as_array_from_object<'a>(entry: &str, object: &'a JsonObject) -> RtResult<&'a Vec> { 225 | object.get(entry) 226 | .and_then(JsonValue::as_array) 227 | .ok_or(format!("Couldn't find array entry '{}' in:\n{:?}", entry, object).into()) 228 | } 229 | -------------------------------------------------------------------------------- /src/dirs.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::path::{Path, PathBuf}; 3 | 4 | use rt_result::RtResult; 5 | 6 | lazy_static! { 7 | static ref HOME_DIR: RtResult = home_dir_internal(); 8 | static ref RUSTY_TAGS_DIR: RtResult = rusty_tags_dir_internal(); 9 | static ref RUSTY_TAGS_CACHE_DIR: RtResult = rusty_tags_cache_dir_internal(); 10 | static ref RUSTY_TAGS_LOCKS_DIR: RtResult = rusty_tags_locks_dir_internal(); 11 | } 12 | 13 | /// where rusty-tags puts all of its stuff 14 | pub fn rusty_tags_dir() -> RtResult<&'static Path> { 15 | RUSTY_TAGS_DIR 16 | .as_ref() 17 | .map(|pb| pb.as_path()) 18 | .map_err(|err| err.clone()) 19 | } 20 | 21 | /// where `rusty-tags` caches its tag files 22 | pub fn rusty_tags_cache_dir() -> RtResult<&'static Path> { 23 | RUSTY_TAGS_CACHE_DIR 24 | .as_ref() 25 | .map(|pb| pb.as_path()) 26 | .map_err(|err| err.clone()) 27 | } 28 | 29 | /// where `rusty-tags` puts its locks when updating a cargo project 30 | pub fn rusty_tags_locks_dir() -> RtResult<&'static Path> { 31 | RUSTY_TAGS_LOCKS_DIR 32 | .as_ref() 33 | .map(|pb| pb.as_path()) 34 | .map_err(|err| err.clone()) 35 | } 36 | 37 | fn home_dir() -> RtResult { 38 | HOME_DIR.clone() 39 | } 40 | 41 | fn home_dir_internal() -> RtResult { 42 | if let Some(path) = extern_dirs::home_dir() { 43 | Ok(path) 44 | } else { 45 | Err("Couldn't read home directory!".into()) 46 | } 47 | } 48 | 49 | fn rusty_tags_cache_dir_internal() -> RtResult { 50 | let dir = rusty_tags_dir()?.join("cache"); 51 | if ! dir.is_dir() { 52 | fs::create_dir_all(&dir)?; 53 | } 54 | 55 | Ok(dir) 56 | } 57 | 58 | fn rusty_tags_locks_dir_internal() -> RtResult { 59 | let dir = rusty_tags_dir()?.join("locks"); 60 | if ! dir.is_dir() { 61 | fs::create_dir_all(&dir)?; 62 | } 63 | 64 | Ok(dir) 65 | } 66 | 67 | fn rusty_tags_dir_internal() -> RtResult { 68 | let dir = home_dir()?.join(".rusty-tags"); 69 | if ! dir.is_dir() { 70 | fs::create_dir_all(&dir)?; 71 | } 72 | 73 | Ok(dir) 74 | } 75 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | //#![allow(dead_code)] 2 | //#![allow(unused_variables)] 3 | 4 | extern crate toml; 5 | extern crate tempfile; 6 | extern crate num_cpus; 7 | extern crate scoped_threadpool; 8 | extern crate serde; 9 | extern crate serde_json; 10 | extern crate fnv; 11 | extern crate semver; 12 | extern crate dirs as extern_dirs; 13 | 14 | #[macro_use] 15 | extern crate serde_derive; 16 | 17 | #[macro_use] 18 | extern crate clap; 19 | 20 | #[macro_use] 21 | extern crate lazy_static; 22 | 23 | use std::path::Path; 24 | use std::io::{self, Write}; 25 | use std::process::Command; 26 | use std::env; 27 | 28 | use tempfile::NamedTempFile; 29 | 30 | use rt_result::RtResult; 31 | use dependencies::dependency_tree; 32 | use tags::{update_tags, create_tags, move_tags}; 33 | use config::Config; 34 | use types::SourceLock; 35 | 36 | #[macro_use] 37 | mod output; 38 | 39 | mod rt_result; 40 | mod dependencies; 41 | mod dirs; 42 | mod tags; 43 | mod types; 44 | mod config; 45 | 46 | fn main() { 47 | execute().unwrap_or_else(|err| { 48 | writeln!(&mut io::stderr(), "{}", err).unwrap(); 49 | std::process::exit(1); 50 | }); 51 | } 52 | 53 | fn execute() -> RtResult<()> { 54 | let config = Config::from_command_args()?; 55 | update_all_tags(&config)?; 56 | Ok(()) 57 | } 58 | 59 | fn update_all_tags(config: &Config) -> RtResult<()> { 60 | let metadata = fetch_source_and_metadata(&config)?; 61 | update_std_lib_tags(&config)?; 62 | 63 | let mut source_locks = Vec::new(); 64 | let dep_tree = { 65 | let mut dep_tree = dependency_tree(&config, &metadata)?; 66 | let unlocked_root_ids: Vec<_> = { 67 | let mut unlocked_roots = Vec::new(); 68 | for source in dep_tree.roots() { 69 | match source.lock(&config.tags_spec)? { 70 | SourceLock::AlreadyLocked { ref path } => { 71 | info!(config, "Already creating tags for '{}', if this isn't the case remove the lock file '{}'", 72 | source.name, path.display()); 73 | continue; 74 | } 75 | 76 | sl@SourceLock::Locked { .. } => { 77 | source_locks.push(sl); 78 | unlocked_roots.push(source); 79 | } 80 | } 81 | } 82 | 83 | unlocked_roots.iter().map(|r| r.id).collect() 84 | }; 85 | 86 | if unlocked_root_ids.is_empty() { 87 | return Ok(()); 88 | } 89 | 90 | dep_tree.set_roots(unlocked_root_ids); 91 | dep_tree 92 | }; 93 | 94 | update_tags(&config, &dep_tree)?; 95 | Ok(()) 96 | } 97 | 98 | fn fetch_source_and_metadata(config: &Config) -> RtResult { 99 | info!(config, "Fetching source and metadata ..."); 100 | 101 | env::set_current_dir(&config.start_dir)?; 102 | 103 | let mut cmd = Command::new("cargo"); 104 | cmd.arg("metadata"); 105 | cmd.arg("--format-version=1"); 106 | 107 | let output = cmd.output() 108 | .map_err(|err| format!("'cargo' execution failed: {}\nIs 'cargo' correctly installed?", err))?; 109 | 110 | if ! output.status.success() { 111 | let mut msg = String::from_utf8_lossy(&output.stderr).into_owned(); 112 | if msg.is_empty() { 113 | msg = String::from_utf8_lossy(&output.stdout).into_owned(); 114 | } 115 | 116 | return Err(msg.into()); 117 | } 118 | 119 | Ok(serde_json::from_str(&String::from_utf8_lossy(&output.stdout))?) 120 | } 121 | 122 | fn update_std_lib_tags(config: &Config) -> RtResult<()> { 123 | let src_path_str = env::var("RUST_SRC_PATH"); 124 | if ! src_path_str.is_ok() { 125 | return Ok(()); 126 | } 127 | 128 | let src_path_str = src_path_str.unwrap(); 129 | let src_path = Path::new(&src_path_str); 130 | if ! src_path.is_dir() { 131 | return Err(format!("Missing rust source code at '{}'!", src_path.display()).into()); 132 | } 133 | 134 | let output_path = match config.output_dir_std { 135 | Some(ref path_buf) => path_buf.as_path(), 136 | None => src_path, 137 | }; 138 | let std_lib_tags = output_path.join(config.tags_spec.file_name()); 139 | if std_lib_tags.is_file() && ! config.force_recreate { 140 | return Ok(()); 141 | } 142 | 143 | let possible_src_dirs = [ 144 | // rustc >= 1.47.0 145 | "alloc", 146 | "core", 147 | "panic_abort", 148 | "panic_unwind", 149 | "proc_macro", 150 | "profiler_builtins", 151 | "rtstartup", 152 | "std", 153 | "stdarch", 154 | "term", 155 | "test", 156 | "unwind", 157 | 158 | // rustc < 1.47.0 159 | "liballoc", 160 | "libarena", 161 | "libbacktrace", 162 | "libcollections", 163 | "libcore", 164 | "libflate", 165 | "libfmt_macros", 166 | "libgetopts", 167 | "libgraphviz", 168 | "liblog", 169 | "librand", 170 | "librbml", 171 | "libserialize", 172 | "libstd", 173 | "libsyntax", 174 | "libterm" 175 | ]; 176 | 177 | let mut src_dirs = Vec::new(); 178 | for dir in &possible_src_dirs { 179 | let src_dir = src_path.join(&dir); 180 | if src_dir.is_dir() { 181 | src_dirs.push(src_dir); 182 | } 183 | } 184 | 185 | if src_dirs.is_empty() { 186 | return Err(format!(r#" 187 | No source directories found for standard library source at $RUST_SRC_PATH: 188 | '{}' 189 | 190 | Please set the standard library source path depending on your rustc version. 191 | 192 | For rustc >= 1.47.0: 193 | $ export RUST_SRC_PATH=$(rustc --print sysroot)/lib/rustlib/src/rust/library/ 194 | 195 | For rustc < 1.47.0: 196 | $ export RUST_SRC_PATH=$(rustc --print sysroot)/lib/rustlib/src/rust/src/"#, src_path.display()).into()); 197 | } 198 | 199 | info!(config, "Creating tags for the standard library ..."); 200 | 201 | let tmp_std_lib_tags = NamedTempFile::new_in(&output_path)?; 202 | create_tags(config, &src_dirs, tmp_std_lib_tags.path())?; 203 | move_tags(config, tmp_std_lib_tags.path(), &std_lib_tags)?; 204 | 205 | Ok(()) 206 | } 207 | -------------------------------------------------------------------------------- /src/output.rs: -------------------------------------------------------------------------------- 1 | macro_rules! info { 2 | ($config:ident, $fmt:expr) => {{ 3 | if ! $config.quiet { 4 | println!($fmt); 5 | } 6 | }}; 7 | 8 | ($config:ident, $fmt:expr, $($arg:tt)*) => {{ 9 | if ! $config.quiet { 10 | println!($fmt, $($arg)*); 11 | } 12 | }}; 13 | } 14 | 15 | macro_rules! verbose { 16 | ($config:ident, $fmt:expr) => {{ 17 | if $config.verbose { 18 | println!($fmt); 19 | } 20 | }}; 21 | 22 | ($config:ident, $fmt:expr, $($arg:tt)*) => {{ 23 | if $config.verbose { 24 | println!($fmt, $($arg)*); 25 | } 26 | }}; 27 | } 28 | -------------------------------------------------------------------------------- /src/rt_result.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use std::fmt::{self, Display, Formatter}; 3 | 4 | use semver::{ReqParseError, SemVerError}; 5 | 6 | /// The result used in the whole application. 7 | pub type RtResult = Result; 8 | 9 | /// The generic error used in the whole application. 10 | #[derive(Clone, Debug)] 11 | pub enum RtErr { 12 | /// generic error message 13 | Message(String), 14 | } 15 | 16 | impl Display for RtErr { 17 | fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> { 18 | match self { 19 | &RtErr::Message(ref msg) => writeln!(f, "{}", msg), 20 | } 21 | } 22 | } 23 | 24 | impl From for RtErr { 25 | fn from(err: io::Error) -> RtErr { 26 | RtErr::Message(format!("{}", err)) 27 | } 28 | } 29 | 30 | impl From for RtErr { 31 | fn from(err: toml::de::Error) -> RtErr { 32 | RtErr::Message(err.to_string()) 33 | } 34 | } 35 | 36 | impl From for RtErr { 37 | fn from(err: serde_json::Error) -> RtErr { 38 | RtErr::Message(format!("{}", err)) 39 | } 40 | } 41 | 42 | impl From for RtErr { 43 | fn from(s: String) -> RtErr { 44 | RtErr::Message(s) 45 | } 46 | } 47 | 48 | impl<'a> From<&'a str> for RtErr { 49 | fn from(s: &str) -> RtErr { 50 | RtErr::Message(s.to_owned()) 51 | } 52 | } 53 | 54 | impl From for RtErr { 55 | fn from(_: ReqParseError) -> RtErr { 56 | RtErr::Message("Invalid version requirement".to_owned()) 57 | } 58 | } 59 | 60 | impl From for RtErr { 61 | fn from(err: SemVerError) -> RtErr { 62 | match err { 63 | SemVerError::ParseError(err) => RtErr::Message(err) 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/tags.rs: -------------------------------------------------------------------------------- 1 | use std::fs::{File, OpenOptions, copy, rename}; 2 | use std::io::{Read, Write, BufWriter}; 3 | use std::path::Path; 4 | 5 | use tempfile::NamedTempFile; 6 | use scoped_threadpool::Pool; 7 | use fnv::FnvHashSet; 8 | 9 | use rt_result::RtResult; 10 | use types::{TagsKind, SourceWithTmpTags, Sources, DepTree, unique_sources}; 11 | use config::Config; 12 | use dirs::rusty_tags_cache_dir; 13 | 14 | /// Update the tags of all sources in 'dep_tree' 15 | pub fn update_tags(config: &Config, dep_tree: &DepTree) -> RtResult<()> { 16 | if ! config.quiet { 17 | let names: Vec<_> = dep_tree.roots().map(|r| &r.name).collect(); 18 | let num_names = names.len(); 19 | print!("Creating tags for: "); 20 | for i in 0..num_names { 21 | print!("{}", &names[i]); 22 | if i < (num_names - 1) { 23 | print!(", "); 24 | } 25 | } 26 | 27 | print!(" ...\n"); 28 | } 29 | 30 | let sources_to_update: Vec<_> = dep_tree.all_sources().filter(|s| { 31 | s.needs_tags_update(config) 32 | }) 33 | .collect(); 34 | 35 | // If a source with missing tags was detected (the 'sources_to_update' above), then all 36 | // dependent (ancestor) sources also have to be updated. The reason for the missing tags 37 | // might be a version change of the source - by changes in the 'Cargo.toml' - so all 38 | // dependent sources have to be rebuild to include the new version. 39 | let sources_to_update = { 40 | let mut srcs = dep_tree.ancestors(&sources_to_update); 41 | srcs.extend(&sources_to_update); 42 | unique_sources(&mut srcs); 43 | 44 | // sort the sources by their depth in the dependency tree to ensure that 45 | // the sources are processed bottom to top, that the tags of dependencies 46 | // are build before the tags of parents 47 | srcs.sort_unstable_by(|a, b| b.max_depth.cmp(&a.max_depth)); 48 | 49 | let mut srcs_with_tags = Vec::with_capacity(srcs.len()); 50 | for src in &srcs { 51 | srcs_with_tags.push(SourceWithTmpTags::new(&config, src)?); 52 | } 53 | 54 | srcs_with_tags 55 | }; 56 | 57 | if config.verbose && ! sources_to_update.is_empty() { 58 | println!("\nCreating tags for sources:"); 59 | for &SourceWithTmpTags { source, .. } in &sources_to_update { 60 | println!(" {}", source.recreate_status(config)); 61 | } 62 | } 63 | 64 | let mut thread_pool = if config.num_threads > 1 { 65 | Some(Pool::new(config.num_threads)) 66 | } else { 67 | None 68 | }; 69 | 70 | // Create the tags for each source in 'sources_to_update'. This creates 71 | // only the tags of the source without considering the dependencies. 72 | if let Some(ref mut thread_pool) = thread_pool { 73 | thread_pool.scoped(|scoped| { 74 | for &SourceWithTmpTags { ref source, ref tags_file, .. } in &sources_to_update { 75 | scoped.execute(move || { 76 | create_tags(config, &[&source.dir], tags_file.as_path()).unwrap(); 77 | }); 78 | } 79 | }); 80 | } else { 81 | for &SourceWithTmpTags { ref source, ref tags_file, .. } in &sources_to_update { 82 | create_tags(config, &[&source.dir], tags_file.as_path())?; 83 | } 84 | } 85 | 86 | // Creates the cacheable tags of each source in 'sources_to_update'. The cacheable 87 | // tags contain the tags of the source and the tags of the public exported dependencies. 88 | // Furthermore creates the final tags of each source in 'sources_to_update'. The 89 | // final tags contain the tags of the source and of all direct dependencies. 90 | if let Some(ref mut thread_pool) = thread_pool { 91 | thread_pool.scoped(|scoped| { 92 | for src in &sources_to_update { 93 | scoped.execute(move || { 94 | let deps = dep_tree.dependencies(src.source); 95 | update_tags_internal(config, src, deps).unwrap(); 96 | }); 97 | } 98 | }); 99 | } else { 100 | for src in &sources_to_update { 101 | let deps = dep_tree.dependencies(src.source); 102 | update_tags_internal(config, src, deps)?; 103 | } 104 | } 105 | 106 | return Ok(()); 107 | 108 | fn update_tags_internal<'a>(config: &Config, source_with_tags: &SourceWithTmpTags<'a>, dependencies: Sources<'a>) -> RtResult<()> { 109 | let source = source_with_tags.source; 110 | let tmp_src_tags = source_with_tags.tags_file.as_path(); 111 | 112 | // create the cached tags file of 'source' which 113 | // might also contain the tags of dependencies if they're 114 | // reexported 115 | { 116 | let reexported_crates = find_reexported_crates(&source.dir)?; 117 | 118 | if ! reexported_crates.is_empty() && config.verbose { 119 | println!("\nFound public reexports in '{}' of:", source.name); 120 | for rcrate in &reexported_crates { 121 | println!(" {}", rcrate); 122 | } 123 | 124 | println!(""); 125 | } 126 | 127 | // collect the tags files of reexported dependencies 128 | let reexported_tags_files: Vec<&Path> = dependencies.clone() 129 | .filter(|d| reexported_crates.iter().find(|c| **c == d.name) != None) 130 | .filter_map(|d| { 131 | if d.cached_tags_file.is_file() { 132 | Some(d.cached_tags_file.as_path()) 133 | } else { 134 | verbose!(config, "\nCouldn't find tags file '{}' of reexported crate. Might be a cyclic dependency?", 135 | d.cached_tags_file.display()); 136 | None 137 | } 138 | }) 139 | .collect(); 140 | 141 | let tmp_cached_tags = NamedTempFile::new_in(rusty_tags_cache_dir()?)?; 142 | if ! reexported_tags_files.is_empty() { 143 | merge_tags(config, tmp_src_tags, &reexported_tags_files, tmp_cached_tags.path())?; 144 | } else { 145 | copy_tags(config, tmp_src_tags, tmp_cached_tags.path())?; 146 | } 147 | 148 | move_tags(config, tmp_cached_tags.path(), &source.cached_tags_file)?; 149 | } 150 | 151 | // create the source tags file of 'source' by merging 152 | // the tags of 'source' and of its dependencies 153 | { 154 | let dep_tags_files: Vec<&Path> = dependencies.clone() 155 | .filter_map(|d| { 156 | if d.cached_tags_file.is_file() { 157 | Some(d.cached_tags_file.as_path()) 158 | } else { 159 | verbose!(config, "\nCouldn't find tags file '{}' of dependency. Might be a cyclic dependency?", 160 | d.cached_tags_file.display()); 161 | None 162 | } 163 | }) 164 | .collect(); 165 | 166 | let tmp_src_and_dep_tags = NamedTempFile::new_in(&source.dir)?; 167 | if ! dep_tags_files.is_empty() { 168 | merge_tags(config, tmp_src_tags, &dep_tags_files, tmp_src_and_dep_tags.path())?; 169 | } else { 170 | copy_tags(config, tmp_src_tags, tmp_src_and_dep_tags.path())?; 171 | } 172 | 173 | move_tags(config, tmp_src_and_dep_tags.path(), &source.tags_file)?; 174 | } 175 | 176 | Ok(()) 177 | } 178 | } 179 | 180 | /// creates tags recursive for the directory hierarchies starting at `src_dirs` 181 | /// and writes them to `tags_file` 182 | pub fn create_tags(config: &Config, src_dirs: &[P1], tags_file: P2) -> RtResult<()> 183 | where P1: AsRef, 184 | P2: AsRef 185 | { 186 | let mut cmd = config.tags_spec.ctags_command(); 187 | cmd.arg("-o") 188 | .arg(tags_file.as_ref()); 189 | 190 | for dir in src_dirs { 191 | cmd.arg(dir.as_ref()); 192 | } 193 | 194 | if config.verbose { 195 | println!("\nCreating tags ...\n with command: {:?}", cmd); 196 | 197 | println!("\n for source:"); 198 | for dir in src_dirs { 199 | println!(" {}", dir.as_ref().display()); 200 | } 201 | 202 | println!("\n cached at:\n {}", tags_file.as_ref().display()); 203 | } 204 | 205 | let output = cmd.output() 206 | .map_err(|err| format!("'ctags' execution failed: {}\nIs 'ctags' correctly installed?", err))?; 207 | 208 | if ! output.status.success() { 209 | let mut msg = String::from_utf8_lossy(&output.stderr).into_owned(); 210 | if msg.is_empty() { 211 | msg = String::from_utf8_lossy(&output.stdout).into_owned(); 212 | } 213 | 214 | if msg.is_empty() { 215 | msg = "ctags execution failed without any stderr or stdout output".to_string(); 216 | } 217 | 218 | return Err(msg.into()); 219 | } 220 | 221 | Ok(()) 222 | } 223 | 224 | pub fn copy_tags(config: &Config, from_tags: &Path, to_tags: &Path) -> RtResult<()> { 225 | verbose!(config, "\nCopy tags ...\n from:\n {}\n to:\n {}", 226 | from_tags.display(), to_tags.display()); 227 | 228 | let _ = copy(from_tags, to_tags)?; 229 | Ok(()) 230 | } 231 | 232 | pub fn move_tags(config: &Config, from_tags: &Path, to_tags: &Path) -> RtResult<()> { 233 | verbose!(config, "\nMove tags ...\n from:\n {}\n to:\n {}", 234 | from_tags.display(), to_tags.display()); 235 | 236 | let _ = rename(from_tags, to_tags)?; 237 | Ok(()) 238 | } 239 | 240 | /// merges the library tag file `lib_tag_file` and its dependency tag files 241 | /// `dependency_tag_files` into `into_tag_file` 242 | fn merge_tags(config: &Config, 243 | lib_tag_file: &Path, 244 | dependency_tag_files: &[&Path], 245 | into_tag_file: &Path) 246 | -> RtResult<()> { 247 | if config.verbose { 248 | println!("\nMerging ...\n tags:"); 249 | println!(" {}", lib_tag_file.display()); 250 | for file in dependency_tag_files { 251 | println!(" {}", file.display()); 252 | } 253 | println!("\n into:\n {}", into_tag_file.display()); 254 | } 255 | 256 | match config.tags_spec.kind { 257 | TagsKind::Vi => { 258 | if dependency_tag_files.is_empty() { 259 | if lib_tag_file != into_tag_file { 260 | copy_tags(config, lib_tag_file, into_tag_file)?; 261 | } 262 | 263 | return Ok(()); 264 | } 265 | 266 | let mut file_contents: Vec = Vec::with_capacity(dependency_tag_files.len() + 1); 267 | let mut num_lines: usize = 0; 268 | { 269 | let mut file = File::open(lib_tag_file)?; 270 | let mut contents = String::new(); 271 | file.read_to_string(&mut contents)?; 272 | num_lines += contents.lines().count(); 273 | file_contents.push(contents); 274 | } 275 | 276 | for file in dependency_tag_files { 277 | let mut file = File::open(file)?; 278 | let mut contents = String::new(); 279 | file.read_to_string(&mut contents)?; 280 | num_lines += contents.lines().count(); 281 | file_contents.push(contents); 282 | } 283 | 284 | let mut merged_lines: Vec<&str> = Vec::with_capacity(num_lines); 285 | for content in file_contents.iter() { 286 | for line in content.lines() { 287 | if let Some(chr) = line.chars().nth(0) { 288 | if chr != '!' { 289 | merged_lines.push(line); 290 | } 291 | } 292 | } 293 | } 294 | 295 | verbose!(config, "\nNum merged lines: {}", merged_lines.len()); 296 | 297 | merged_lines.sort_unstable(); 298 | merged_lines.dedup(); 299 | 300 | let mut tag_file = BufWriter::with_capacity(64000, OpenOptions::new() 301 | .create(true) 302 | .truncate(true) 303 | .read(true) 304 | .write(true) 305 | .open(into_tag_file)?); 306 | 307 | tag_file.write_fmt(format_args!("{}\n", "!_TAG_FILE_FORMAT 2 /extended format; --format=1 will not append ;\" to lines/"))?; 308 | tag_file.write_fmt(format_args!("{}\n", "!_TAG_FILE_SORTED 1 /0=unsorted, 1=sorted, 2=foldcase/"))?; 309 | 310 | let new_line = "\n".as_bytes(); 311 | for line in merged_lines { 312 | tag_file.write_all(line.as_bytes())?; 313 | tag_file.write_all(new_line)?; 314 | } 315 | }, 316 | 317 | TagsKind::Emacs => { 318 | if lib_tag_file != into_tag_file { 319 | copy_tags(config, lib_tag_file, into_tag_file)?; 320 | } 321 | 322 | let mut tag_file = BufWriter::with_capacity(64000, OpenOptions::new() 323 | .create(true) 324 | .append(true) 325 | .read(true) 326 | .write(true) 327 | .open(into_tag_file)?); 328 | 329 | for file in dependency_tag_files { 330 | if *file != into_tag_file { 331 | tag_file.write_fmt(format_args!("{},include\n", file.display()))?; 332 | } 333 | } 334 | } 335 | } 336 | 337 | Ok(()) 338 | } 339 | 340 | type CrateName = String; 341 | 342 | /// searches in the file `/lib.rs` for external crates 343 | /// that are reexpored and returns their names 344 | fn find_reexported_crates(src_dir: &Path) -> RtResult> { 345 | let lib_file = src_dir.join("lib.rs"); 346 | if ! lib_file.is_file() { 347 | return Ok(Vec::new()); 348 | } 349 | 350 | let contents = { 351 | let mut file = File::open(&lib_file)?; 352 | let mut contents = String::new(); 353 | file.read_to_string(&mut contents)?; 354 | contents 355 | }; 356 | 357 | let lines = contents.lines(); 358 | 359 | type ModuleName = String; 360 | let mut pub_uses = FnvHashSet::::default(); 361 | 362 | #[derive(Eq, PartialEq, Hash)] 363 | struct ExternCrate<'a> 364 | { 365 | name: &'a str, 366 | as_name: &'a str 367 | } 368 | 369 | let mut extern_crates = FnvHashSet::::default(); 370 | 371 | for line in lines { 372 | let items = line.trim_matches(';').split(' ').collect::>(); 373 | if items.len() < 3 { 374 | continue; 375 | } 376 | 377 | if items[0] == "pub" && items[1] == "use" { 378 | let mods = items[2].split("::").collect::>(); 379 | if mods.len() >= 1 { 380 | pub_uses.insert(mods[0].to_string()); 381 | } 382 | } 383 | 384 | if items[0] == "extern" && items[1] == "crate" { 385 | if items.len() == 3 { 386 | extern_crates.insert(ExternCrate { name: items[2].trim_matches('"'), as_name: items[2] }); 387 | } else if items.len() == 5 && items[3] == "as" { 388 | extern_crates.insert(ExternCrate { name: items[2].trim_matches('"'), as_name: items[4] }); 389 | } 390 | } 391 | } 392 | 393 | let mut reexp_crates = Vec::::new(); 394 | for extern_crate in extern_crates.iter() { 395 | if pub_uses.contains(extern_crate.as_name) { 396 | reexp_crates.push(extern_crate.name.to_string()); 397 | } 398 | } 399 | 400 | Ok(reexp_crates) 401 | } 402 | -------------------------------------------------------------------------------- /src/types.rs: -------------------------------------------------------------------------------- 1 | use std::path::{Path, PathBuf}; 2 | use std::fs::{self, File}; 3 | use std::collections::hash_map::DefaultHasher; 4 | use std::collections::HashSet; 5 | use std::hash::{Hash, Hasher}; 6 | use std::process::Command; 7 | use std::ops::Deref; 8 | use std::cmp; 9 | use std::mem; 10 | 11 | use semver::Version; 12 | use rt_result::RtResult; 13 | use dirs::{rusty_tags_cache_dir, rusty_tags_locks_dir}; 14 | use config::Config; 15 | 16 | /// The tree describing the dependencies of the whole cargo project. 17 | #[derive(Debug)] 18 | pub struct DepTree { 19 | /// the roots, the workspace members of the cargo project, 20 | /// the source ids are indices into 'sources' 21 | roots: Vec, 22 | 23 | /// all sources of the cargo project, the roots and all direct 24 | /// and indirect dependencies 25 | sources: Vec>, 26 | 27 | /// the dependencies of each source in 'sources', the source 28 | /// ids are indices into 'sources' 29 | dependencies: Vec>>, 30 | 31 | /// the parents - the dependent sources - of each 32 | /// source in 'sources', the source ids are indices into 33 | /// 'sources' 34 | parents: Vec>> 35 | } 36 | 37 | impl DepTree { 38 | pub fn new() -> DepTree { 39 | DepTree { 40 | roots: Vec::with_capacity(10), 41 | sources: Vec::new(), 42 | dependencies: Vec::new(), 43 | parents: Vec::new() 44 | } 45 | } 46 | 47 | pub fn reserve_num_sources(&mut self, num: usize) { 48 | self.sources.reserve(num); 49 | self.dependencies.reserve(num); 50 | self.parents.reserve(num); 51 | } 52 | 53 | pub fn roots(&self) -> Sources { 54 | Sources::new(&self.sources, Some(&self.roots)) 55 | } 56 | 57 | pub fn dependencies(&self, source: &Source) -> Sources { 58 | Sources::new(&self.sources, self.dependencies_slice(source)) 59 | } 60 | 61 | pub fn all_sources<'a>(&'a self) -> Box + 'a> { 62 | Box::new(self.sources 63 | .iter() 64 | .filter_map(|s| s.as_ref())) 65 | } 66 | 67 | /// Get all of the ancestors of 'sources' till the roots. 68 | pub fn ancestors<'a>(&'a self, sources: &[&Source]) -> Vec<&'a Source> { 69 | let mut ancestor_ids = HashSet::with_capacity(50000); 70 | for src in sources { 71 | self.ancestors_internal(src.id, &mut ancestor_ids); 72 | } 73 | 74 | let mut ancestor_srcs = Vec::with_capacity(ancestor_ids.len()); 75 | for id in &ancestor_ids { 76 | if let Some(ref src) = self.sources[**id] { 77 | ancestor_srcs.push(src); 78 | } 79 | } 80 | 81 | ancestor_srcs 82 | } 83 | 84 | /// Reserve space for a new source and return its source id. 85 | pub fn new_source(&mut self) -> SourceId { 86 | let id = self.sources.len(); 87 | self.sources.push(None); 88 | self.dependencies.push(None); 89 | self.parents.push(None); 90 | SourceId { id } 91 | } 92 | 93 | pub fn set_roots(&mut self, ids: Vec) { 94 | self.roots = ids; 95 | } 96 | 97 | pub fn set_source(&mut self, src: Source, dependencies: Vec) { 98 | let src_id = src.id; 99 | self.sources[*src_id] = Some(src); 100 | if dependencies.is_empty() { 101 | return; 102 | } 103 | 104 | for dep in &dependencies { 105 | let dep_id: usize = **dep; 106 | if self.parents[dep_id].is_none() { 107 | self.parents[dep_id] = Some(Vec::with_capacity(10)); 108 | } 109 | 110 | if let Some(ref mut parents) = self.parents[dep_id] { 111 | parents.push(src_id); 112 | } 113 | } 114 | 115 | self.dependencies[*src_id] = Some(dependencies); 116 | } 117 | 118 | pub fn compute_depths(&mut self) { 119 | let roots = mem::replace(&mut self.roots, vec![]); 120 | for id in &roots { 121 | self.compute_depths_internal(*id, 0) 122 | } 123 | 124 | self.roots = roots; 125 | } 126 | 127 | fn compute_depths_internal(&mut self, source_id: SourceId, depth: u32) { 128 | if let Some(ref mut source) = &mut self.sources[source_id.id] { 129 | // source already visited, just update 'max_depth' 130 | if let Some(max_depth) = source.max_depth { 131 | source.max_depth = Some(cmp::max(max_depth, depth)); 132 | return; 133 | } 134 | else { 135 | source.max_depth = Some(depth); 136 | } 137 | } 138 | 139 | if let Some(dep_ids) = mem::replace(&mut self.dependencies[source_id.id], None) { 140 | for id in &dep_ids { 141 | self.compute_depths_internal(*id, depth + 1); 142 | } 143 | 144 | self.dependencies[source_id.id] = Some(dep_ids); 145 | } 146 | } 147 | 148 | fn dependencies_slice(&self, source: &Source) -> Option<&[SourceId]> { 149 | self.dependencies[*source.id].as_ref().map(Vec::as_slice) 150 | } 151 | 152 | fn ancestors_internal<'a>(&'a self, source_id: SourceId, 153 | ancestors: &mut HashSet) { 154 | if let Some(ref parent_ids) = self.parents[*source_id] { 155 | for id in parent_ids { 156 | if ancestors.contains(id) { 157 | continue; 158 | } 159 | 160 | ancestors.insert(*id); 161 | self.ancestors_internal(*id, ancestors); 162 | } 163 | } 164 | } 165 | } 166 | 167 | /// An iterator over sources by their source ids. 168 | #[derive(Clone)] 169 | pub struct Sources<'a> { 170 | /// all sources 171 | sources: &'a [Option], 172 | 173 | /// the sources to iterate over, 'source_ids' 174 | /// are indices into 'sources' 175 | source_ids: Option<&'a [SourceId]>, 176 | 177 | /// the current index into 'source_ids' 178 | idx: usize 179 | } 180 | 181 | impl<'a> Sources<'a> { 182 | fn new(sources: &'a [Option], source_ids: Option<&'a [SourceId]>) -> Sources<'a> { 183 | Sources { sources, source_ids, idx: 0 } 184 | } 185 | } 186 | 187 | impl<'a> Iterator for Sources<'a> { 188 | type Item = &'a Source; 189 | 190 | fn next(&mut self) -> Option { 191 | if let Some(source_ids) = self.source_ids { 192 | if self.idx >= source_ids.len() { 193 | None 194 | } else { 195 | let id = source_ids[self.idx]; 196 | let src = self.sources[*id].as_ref(); 197 | self.idx += 1; 198 | src 199 | } 200 | } else { 201 | None 202 | } 203 | } 204 | } 205 | 206 | /// Lock a source to prevent that multiple running instances 207 | /// of 'rusty-tags' update the same source at once. 208 | /// 209 | /// This is only an optimization and not needed for correctness, 210 | /// because the tags are written to a temporary file which is then 211 | /// moved to its final place. It's ensured that the moving happens 212 | /// on the same partition/file system and therefore the move is 213 | /// an atomic operation which can't be affected by an other 214 | /// running instance of 'rusty-tags'. So multiple running 215 | /// 'rusty-tags' can't write at once to the same file. 216 | pub enum SourceLock { 217 | /// this running instance of 'rusty-tags' holds the lock 218 | Locked { 219 | path: PathBuf 220 | }, 221 | 222 | /// an other instance of 'rusty-tags' holds the lock, 223 | /// or the other instance couldn't cleanup the lock correctly 224 | AlreadyLocked { 225 | path: PathBuf 226 | } 227 | } 228 | 229 | impl SourceLock { 230 | fn new(source: &Source, tags_spec: &TagsSpec) -> RtResult { 231 | let file_name = source.unique_file_name(tags_spec); 232 | let lock_file = rusty_tags_locks_dir()?.join(file_name); 233 | if lock_file.is_file() { 234 | Ok(SourceLock::AlreadyLocked { path: lock_file }) 235 | } else { 236 | let _ = File::create(&lock_file)?; 237 | Ok(SourceLock::Locked { 238 | path: lock_file 239 | }) 240 | } 241 | } 242 | } 243 | 244 | impl Drop for SourceLock { 245 | fn drop(&mut self) { 246 | match *self { 247 | SourceLock::Locked { ref path, .. } => { 248 | if path.is_file() { 249 | let _ = fs::remove_file(&path); 250 | } 251 | } 252 | 253 | SourceLock::AlreadyLocked { .. } => {} 254 | } 255 | } 256 | } 257 | 258 | #[derive(Debug)] 259 | pub struct Source { 260 | /// rusty-tags specific internal id of the source 261 | pub id: SourceId, 262 | 263 | /// the 'Cargo.toml' name of the source 264 | pub name: String, 265 | 266 | /// the 'Cargo.toml' version of the source 267 | pub version: Version, 268 | 269 | /// the root source directory 270 | pub dir: PathBuf, 271 | 272 | /// hash of 'dir' 273 | pub hash: String, 274 | 275 | /// if the source is a root of the dependency tree, 276 | /// which means that it's a workspace member 277 | pub is_root: bool, 278 | 279 | /// the max depth of the source inside of the dependency tree, 280 | /// a source might be referenced multiple times and 'max_depth' 281 | /// contains the greatest depth of the source 282 | pub max_depth: Option, 283 | 284 | /// path to the tags file in the source directory, 285 | /// beside of the 'Cargo.toml' file, this tags file 286 | /// contains the tags of the source and of its 287 | /// dependencies 288 | pub tags_file: PathBuf, 289 | 290 | /// path to the tags file in the rusty-tags cache directory, 291 | /// this tags file contains the tags of the source and 292 | /// the tags of the dependencies that have a public 293 | /// export from the source 294 | pub cached_tags_file: PathBuf, 295 | } 296 | 297 | impl Source { 298 | pub fn new(id: SourceId, name: &str, version: &Version, dir: &Path, is_root: bool, config: &Config) -> RtResult { 299 | let tags_dir = find_dir_upwards_containing("Cargo.toml", dir).unwrap_or(dir.to_path_buf()); 300 | let tags_file = tags_dir.join(config.tags_spec.file_name()); 301 | let hash = source_hash(dir); 302 | let cached_tags_file = { 303 | let cache_dir = rusty_tags_cache_dir()?; 304 | let file_name = format!("{}-{}.{}", name, hash, config.tags_spec.file_extension()); 305 | cache_dir.join(&file_name) 306 | }; 307 | 308 | Ok(Source { 309 | id: id, 310 | max_depth: None, 311 | name: name.to_owned(), 312 | version: version.clone(), 313 | dir: dir.to_owned(), 314 | hash: hash, 315 | is_root: is_root, 316 | tags_file: tags_file, 317 | cached_tags_file: cached_tags_file 318 | }) 319 | } 320 | 321 | pub fn needs_tags_update(&self, config: &Config) -> bool { 322 | if config.force_recreate { 323 | return true; 324 | } 325 | 326 | // Tags of the root (the cargo project) should be always recreated, 327 | // because we don't know which source file has been changed and 328 | // even if we would know it, we couldn't easily just replace the 329 | // tags of the changed source file. 330 | if self.is_root { 331 | return true; 332 | } 333 | 334 | ! self.cached_tags_file.is_file() || ! self.tags_file.is_file() 335 | } 336 | 337 | pub fn recreate_status(&self, config: &Config) -> String { 338 | if config.force_recreate { 339 | format!("Forced recreating of tags for {}", self.source_version()) 340 | } else if self.is_root { 341 | format!("Recreating tags for cargo project root {}", self.source_version()) 342 | } else if ! self.cached_tags_file.is_file() { 343 | format!("Recreating tags for {}, because of missing cache file at '{:?}'", 344 | self.source_version(), self.cached_tags_file) 345 | } else if ! self.tags_file.is_file() { 346 | format!("Recreating tags for {}, because of missing tags file at '{:?}'", 347 | self.source_version(), self.tags_file) 348 | } else { 349 | format!("Recreating tags for {}, because one of its dependencies was updated", 350 | self.source_version()) 351 | } 352 | } 353 | 354 | pub fn lock(&self, tags_spec: &TagsSpec) -> RtResult { 355 | SourceLock::new(self, tags_spec) 356 | } 357 | 358 | /// create a file name that's unique for each source 359 | pub fn unique_file_name(&self, tags_spec: &TagsSpec) -> String { 360 | format!("{}-{}.{}", self.name, self.hash, tags_spec.file_extension()) 361 | } 362 | 363 | fn source_version(&self) -> String { 364 | format!("({}, {})", self.name, self.version) 365 | } 366 | } 367 | 368 | /// Temporary struct for the tags updating of the source. It's 369 | /// used to create and associate a temporary file to the source 370 | /// for its tags creation. 371 | pub struct SourceWithTmpTags<'a> { 372 | /// the source to update 373 | pub source: &'a Source, 374 | 375 | /// temporary file for the tags of the source 376 | pub tags_file: PathBuf 377 | } 378 | 379 | impl<'a> SourceWithTmpTags<'a> { 380 | pub fn new(config: &Config, source: &'a Source) -> RtResult> { 381 | let file_name = source.unique_file_name(&config.tags_spec); 382 | let tags_file = config.temp_file(&file_name)?; 383 | Ok(SourceWithTmpTags { source, tags_file }) 384 | } 385 | } 386 | 387 | /// An unique runtime specific 'rusty-tags' internal id 388 | /// of the source. 389 | #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Debug)] 390 | pub struct SourceId { 391 | id: usize 392 | } 393 | 394 | impl Deref for SourceId { 395 | type Target = usize; 396 | 397 | fn deref(&self) -> &Self::Target { 398 | &self.id 399 | } 400 | } 401 | 402 | fn source_hash(source_dir: &Path) -> String { 403 | let mut hasher = DefaultHasher::new(); 404 | source_dir.hash(&mut hasher); 405 | hasher.finish().to_string() 406 | } 407 | 408 | // which kind of tags are created 409 | arg_enum! { 410 | #[derive(Eq, PartialEq, Debug)] 411 | pub enum TagsKind { 412 | Vi, 413 | Emacs 414 | } 415 | } 416 | 417 | type ExeName = String; 418 | 419 | /// which ctags executable is used 420 | #[derive(Debug)] 421 | pub enum TagsExe { 422 | ExuberantCtags(ExeName), 423 | UniversalCtags(ExeName) 424 | } 425 | 426 | /// holds additional info for the kind of tags, which extension 427 | /// they use for caching and which user viewable file names they get 428 | pub struct TagsSpec { 429 | pub kind: TagsKind, 430 | 431 | exe: TagsExe, 432 | 433 | /// the file name for vi tags 434 | vi_tags: String, 435 | 436 | /// the file name for emacs tags 437 | emacs_tags: String, 438 | 439 | /// options given to the ctags executable 440 | ctags_options: String 441 | } 442 | 443 | impl TagsSpec { 444 | pub fn new(kind: TagsKind, exe: TagsExe, vi_tags: String, emacs_tags: String, ctags_options: String) -> RtResult { 445 | if vi_tags == emacs_tags { 446 | return Err(format!("It's not supported to use the same tags name '{}' for vi and emacs!", vi_tags).into()); 447 | } 448 | 449 | Ok(TagsSpec { 450 | kind: kind, 451 | exe: exe, 452 | vi_tags: vi_tags, 453 | emacs_tags: emacs_tags, 454 | ctags_options: ctags_options 455 | }) 456 | } 457 | 458 | pub fn file_extension(&self) -> &'static str { 459 | match self.kind { 460 | TagsKind::Vi => "vi", 461 | TagsKind::Emacs => "emacs" 462 | } 463 | } 464 | 465 | pub fn file_name(&self) -> &str { 466 | match self.kind { 467 | TagsKind::Vi => &self.vi_tags, 468 | TagsKind::Emacs => &self.emacs_tags 469 | } 470 | } 471 | 472 | pub fn ctags_command(&self) -> Command { 473 | match self.exe { 474 | TagsExe::ExuberantCtags(ref exe_name) => { 475 | let mut cmd = Command::new(&exe_name); 476 | self.generic_ctags_options(&mut cmd); 477 | cmd.arg("--languages=Rust") 478 | .arg("--langdef=Rust") 479 | .arg("--langmap=Rust:.rs") 480 | .arg("--regex-Rust=/^[ \\t]*(#\\[[^\\]]\\][ \\t]*)*(pub[ \\t]+)?(extern[ \\t]+)?(\"[^\"]+\"[ \\t]+)?(unsafe[ \\t]+)?(async[ \\t]+)?fn[ \\t]+([a-zA-Z0-9_]+)/\\7/f,functions,function definitions/") 481 | .arg("--regex-Rust=/^[ \\t]*(pub[ \\t]+)?type[ \\t]+([a-zA-Z0-9_]+)/\\2/T,types,type definitions/") 482 | .arg("--regex-Rust=/^[ \\t]*(pub[ \\t]+)?enum[ \\t]+([a-zA-Z0-9_]+)/\\2/g,enum,enumeration names/") 483 | .arg("--regex-Rust=/^[ \\t]*(pub[ \\t]+)?struct[ \\t]+([a-zA-Z0-9_]+)/\\2/s,structure names/") 484 | .arg("--regex-Rust=/^[ \\t]*(pub[ \\t]+)?mod[ \\t]+([a-zA-Z0-9_]+)\\s*\\{/\\2/m,modules,module names/") 485 | .arg("--regex-Rust=/^[ \\t]*(pub[ \\t]+)?(static|const)[ \\t]+([a-zA-Z0-9_]+)/\\3/c,consts,static constants/") 486 | .arg("--regex-Rust=/^[ \\t]*(pub[ \\t]+)?(unsafe[ \\t]+)?trait[ \\t]+([a-zA-Z0-9_]+)/\\3/t,traits,traits/") 487 | .arg("--regex-Rust=/^[ \\t]*macro_rules![ \\t]+([a-zA-Z0-9_]+)/\\1/d,macros,macro definitions/"); 488 | 489 | cmd 490 | } 491 | 492 | TagsExe::UniversalCtags(ref exe_name) => { 493 | let mut cmd = Command::new(&exe_name); 494 | self.generic_ctags_options(&mut cmd); 495 | cmd.arg("--languages=Rust"); 496 | 497 | cmd 498 | } 499 | } 500 | } 501 | 502 | fn generic_ctags_options(&self, cmd: &mut Command) { 503 | match self.kind { 504 | TagsKind::Vi => {} 505 | TagsKind::Emacs => { cmd.arg("-e"); } 506 | } 507 | 508 | cmd.arg("--recurse"); 509 | if ! self.ctags_options.is_empty() { 510 | cmd.arg(&self.ctags_options); 511 | } 512 | } 513 | } 514 | 515 | pub fn unique_sources(sources: &mut Vec<&Source>) { 516 | sources.sort_unstable_by(|a, b| a.id.cmp(&b.id)); 517 | sources.dedup_by_key(|s| &s.id); 518 | } 519 | 520 | fn find_dir_upwards_containing(file_name: &str, start_dir: &Path) -> RtResult { 521 | let mut dir = start_dir.to_path_buf(); 522 | loop { 523 | if let Ok(files) = fs::read_dir(&dir) { 524 | for path in files.map(|r| r.map(|d| d.path())) { 525 | match path { 526 | Ok(ref path) if path.is_file() => 527 | match path.file_name() { 528 | Some(name) if name.to_str() == Some(file_name) => return Ok(dir), 529 | _ => continue 530 | }, 531 | _ => continue 532 | } 533 | } 534 | } 535 | 536 | if ! dir.pop() { 537 | return Err(format!("Couldn't find '{}' starting at directory '{}'!", file_name, start_dir.display()).into()); 538 | } 539 | } 540 | } 541 | --------------------------------------------------------------------------------