├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── src └── main.rs └── test.sh /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | tempunosolotest 3 | wtf.cpp 4 | Cargo.lock 5 | wat.txt 6 | wtf.txt 7 | **/*.rs.bk 8 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "unosolo" 3 | version = "0.1.1" 4 | authors = ["Vittorio Romeo "] 5 | 6 | [dependencies] 7 | structopt = "0.0.3" 8 | structopt-derive = "0.0.3" 9 | walkdir = "1.0.7" 10 | regex = "0.2" 11 | lazy_static = "0.2.8" 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Vittorio Romeo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # unosolo 2 | 3 | > **Work-in-progress Rust application that converts C++ header-only libraries to single self-contained headers.** 4 | 5 | [![stability][badge.stability]][stability] 6 | [![license][badge.license]][license] 7 | [![gratipay][badge.gratipay]][gratipay] 8 | ![badge.rust](https://img.shields.io/badge/rust-nightly-ff69b4.svg?style=flat-square) 9 | 10 | [badge.stability]: https://img.shields.io/badge/stability-experimental-orange.svg?style=flat-square 11 | [badge.license]: http://img.shields.io/badge/license-mit-blue.svg?style=flat-square 12 | [badge.gratipay]: https://img.shields.io/gratipay/user/SuperV1234.svg?style=flat-square 13 | 14 | [stability]: http://github.com/badges/stability-badges 15 | [license]: https://github.com/SuperV1234/unosolo/blob/master/LICENSE 16 | [gratipay]: https://gratipay.com/~SuperV1234/ 17 | 18 | 19 | ## Disclaimer 20 | 21 | This is my first Rust project, mainly created to start getting used to the language. My intention is to improve `unosolo` as I get better with Rust and the final goal is being able to successfully use it on popular libraries. *(If you need a full-fledged customizable preprocessor implementation, check out [pcpp](https://pypi.python.org/pypi/pcpp).)* 22 | 23 | I also do not encourage people to create single-header libraries and use those in their projects: they're mainly useful when dealing with very complicated build systems or when experimenting on an online compiler that doesn't allow users to easily import multiple files. 24 | 25 | *Contributions and code reviews are welcome!* 26 | 27 | 28 | 29 | ## Build instructions 30 | 31 | ```bash 32 | git clone https://github.com/SuperV1234/unosolo 33 | cd unosolo 34 | cargo build 35 | ``` 36 | 37 | * `cargo run -- args...` can be used to build&run `unosolo`. 38 | 39 | * `cargo install` can be used to install `unosolo` on the user's system. 40 | 41 | 42 | 43 | ## Overview 44 | 45 | Given a set of paths containing the C++ header-only library's header files and a "top-level include" file where the graph traversal will start from, `unosolo` outputs a self-contained single-header version of the library to `stdout`. Here's the [`clap-rs`](https://github.com/kbknapp/clap-rs) auto-generated help: 46 | 47 | ``` 48 | unosolo 0.1.1 49 | Vittorio Romeo 50 | transforms a C++ header-only library in a self-contained single header. 51 | 52 | USAGE: 53 | unosolo [FLAGS] [OPTIONS] --topinclude 54 | 55 | FLAGS: 56 | -h, --help Prints help information 57 | -V, --version Prints version information 58 | -v, --verbose Enable verbose mode 59 | 60 | OPTIONS: 61 | -p, --paths ... Include paths [default: .] 62 | -t, --topinclude Top-level include file path (entrypoint) 63 | ``` 64 | 65 | 66 | ## Use cases 67 | 68 | ### `scelta` 69 | 70 | `unosolo` is currently able to transform [**`scelta`**](https://github.com/SuperV1234/scelta), my latest C++17 header-only library, to a single-header version. In fact, I've used `unosolo` to add two badges to `scelta`'s README that allow users to try the library either [on wandbox](https://wandbox.org/permlink/wSA55OCJz17k7Jtz) or [on godbolt](https://godbolt.org/g/4sQtkM). This idea was taken from Michael Park's excellent variant implementation: [`mpark::variant`](https://github.com/mpark/variant). 71 | 72 | The command used to transform `scelta` was: 73 | 74 | ```bash 75 | unosolo -p"./scelta/include" -v -t"./scelta/include/scelta.hpp" > scelta_single_header.hpp 76 | ``` 77 | 78 | It produced [this abomination](https://gist.github.com/SuperV1234/a5af0a8b92f75d83085a8e5fccf71d6a). 79 | 80 | 81 | 82 | ### `vrm_core` and `vrm_pp` 83 | 84 | Since 0.1.1, `unosolo` supports multiple library include paths and "absolute `#include` directives". My [**`vrm_core`**](https://github.com/SuperV1234/vrm_core) library, which depends on [**`vrm_pp`**](https://github.com/SuperV1234/vrm_pp), can be transformed to a single header as follows: 85 | 86 | ```bash 87 | git clone https://github.com/SuperV1234/vrm_pp.git 88 | git clone https://github.com/SuperV1234/vrm_core.git 89 | unosolo -p"./vrm_pp/include" "./vrm_core/include" -t"./vrm_core/include/vrm/core.hpp" > vrm_core_sh.hpp 90 | ``` 91 | 92 | It produced [this beauty](https://gist.github.com/SuperV1234/4f9ae8f99da72288c73ca643b101ed20). 93 | 94 | 95 | 96 | ### `boost::hana` 97 | 98 | A single-header version of [**`boost::hana`**](https://github.com/boostorg/hana) can be created using `unosolo` as follows: 99 | 100 | ```bash 101 | git clone https://github.com/boostorg/hana 102 | unosolo -p"./hana/include" -t"./hana/include/boost/hana.hpp" > hana_sh.hpp 103 | ``` 104 | 105 | I haven't tested it very thorougly, but I compiled [the example on `hana`'s README](https://github.com/boostorg/hana#overview) without any hiccups. 106 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Vittorio Romeo 2 | // MIT License | https://opensource.org/licenses/MIT 3 | // http://vittorioromeo.info | vittorio.romeo@outlook.com 4 | 5 | #[macro_use] 6 | extern crate structopt_derive; 7 | 8 | #[macro_use] 9 | extern crate lazy_static; 10 | 11 | extern crate regex; 12 | extern crate structopt; 13 | extern crate walkdir; 14 | 15 | use walkdir::WalkDir; 16 | use structopt::StructOpt; 17 | use std::io::BufReader; 18 | use std::io::BufRead; 19 | use std::fs::File; 20 | use std::path::Path; 21 | use std::path::PathBuf; 22 | use std::collections::HashMap; 23 | use std::collections::HashSet; 24 | use regex::Regex; 25 | 26 | #[derive(StructOpt, Debug)] 27 | #[structopt(name = "unosolo", 28 | about = "transforms a C++ header-only library in a self-contained single header.")] 29 | struct Opt { 30 | #[structopt(short = "p", 31 | long = "paths", 32 | help = "Include paths", 33 | default_value = ".")] 34 | paths: Vec, 35 | 36 | #[structopt(short = "v", 37 | long = "verbose", 38 | help = "Enable verbose mode")] 39 | verbose: bool, 40 | 41 | #[structopt(short = "t", 42 | long = "topinclude", 43 | help = "Top-level include file path (entrypoint)")] 44 | top_include: String, 45 | } 46 | 47 | /// Prints to `stderr` only if verbose mode is enabled. 48 | macro_rules! verbose_eprintln { 49 | ($opt:expr, $($tts:tt)*) => { 50 | if $opt.verbose { 51 | eprintln!($($tts)*); 52 | } 53 | } 54 | } 55 | 56 | /// Pretty-prints `x` to `stderr` only if verbose mode is enabled. 57 | macro_rules! verbose_eprettyprint { 58 | ($opt:expr, $x:ident) => { 59 | verbose_eprintln!($opt, "{}: {:#?}", stringify!($x), $x); 60 | } 61 | } 62 | 63 | /// Attempts to unwrap `x`, otherwise panics with formatted string. 64 | macro_rules! expect_fmt { 65 | ($x:expr, $($tts:tt)*) => { 66 | $x.unwrap_or_else(|_| panic!($($tts)*)) 67 | } 68 | } 69 | 70 | /// Generates a function called `fn_name` that takes a `s: &str` and 71 | /// returns `true` if `s` matches `regex_string`. 72 | macro_rules! regex_matcher { 73 | ($fn_name:ident, $regex_string:expr) => { 74 | fn $fn_name(s: &str) -> bool { 75 | lazy_static! { 76 | static ref RE: Regex = 77 | Regex::new($regex_string).unwrap(); 78 | } 79 | 80 | RE.is_match(s) 81 | } 82 | } 83 | } 84 | 85 | // Type aliases for the path graph. 86 | type PathSet = HashSet; 87 | 88 | /// Returns `true` if `x` is a path to an header currently supported by `unosolo`. 89 | fn is_header(x: &walkdir::DirEntry) -> bool { 90 | x.file_name() 91 | .to_str() 92 | .map_or(false, |s| { 93 | s.ends_with(".h") || s.ends_with(".hpp") || s.ends_with(".inl") || s.ends_with(".cpp") 94 | }) 95 | } 96 | 97 | /// Returns `true` if `s` is a line contaning only an inline C++ comment. 98 | regex_matcher!(is_comment, r#"^[[:blank:]]*//.*"#); 99 | 100 | /// Returns `true` if `s` is a line containing only `#pragma once`. 101 | regex_matcher!(is_pragma_once, r#"[[:blank:]]*#pragma once.*"#); 102 | 103 | /// Returns `s` without the first and last character. 104 | fn unquote(s: &str) -> &str { 105 | &s[1..s.len() - 1] 106 | } 107 | 108 | /// Returns `true` if `s` is a line containing only an `#include` directive. 109 | fn is_include_directive(s: &str) -> bool { 110 | s.find("#include") 111 | .map_or(false, |y| s[0..y].chars().all(|c| c.is_whitespace())) 112 | } 113 | 114 | fn unwrap_canonicalize(x: &str) -> std::path::PathBuf { 115 | expect_fmt!(std::path::Path::new(&x).canonicalize(), 116 | "Path {:#?} does not exist", 117 | x) 118 | } 119 | 120 | /// Builds the dependency graph and include directives set by reading the file at `entry_path`. 121 | fn fill_dependencies(include_directive_lines: &mut HashMap, 122 | absolute_includes: &HashMap, 123 | entry_path: &Path, 124 | prefix: &str, 125 | parent: &str) { 126 | let f = expect_fmt!(File::open(entry_path), "Could not open '{:#?}'", entry_path); 127 | let f = BufReader::new(f); 128 | 129 | for line in f.lines() 130 | .filter_map(|e| e.ok()) 131 | .filter(|x| is_include_directive(x)) { 132 | // Cut off `#include`. 133 | let filename = &line[9..]; 134 | 135 | enum IncludeType { 136 | Relative, 137 | Absolute, 138 | }; 139 | 140 | let include_type = match filename 141 | .chars() 142 | .nth(0) 143 | .expect("Invalid include directive found") { 144 | '"' => IncludeType::Relative, 145 | '<' => IncludeType::Absolute, 146 | _ => panic!("Invalid include directive found"), 147 | }; 148 | 149 | let unquoted = unquote(filename); 150 | match include_type { 151 | IncludeType::Relative => { 152 | Some(unwrap_canonicalize(&format!("{}/{}/{}", prefix, parent, unquoted))) 153 | } 154 | IncludeType::Absolute => { 155 | absolute_includes 156 | .get(unquoted) 157 | .map_or(None, |x| Some(x.to_path_buf())) 158 | } 159 | } 160 | .map(|cpath| { include_directive_lines.insert(line.clone(), cpath); }); 161 | } 162 | } 163 | 164 | /// Executes `f` for all header files in the user-specified search path. 165 | fn for_all_headers(opt: &Opt, mut f: F) 166 | where F: FnMut(&Path, &Path, &str, &str) -> () 167 | { 168 | for prefix in &opt.paths { 169 | let c_prefix = unwrap_canonicalize(prefix); 170 | let c_prefix_str = c_prefix.to_str().unwrap(); 171 | 172 | for entry in WalkDir::new(&prefix) 173 | .into_iter() 174 | .filter_map(|e| e.ok()) 175 | .filter(is_header) { 176 | let c_entry_path = entry.path().canonicalize().unwrap(); 177 | let at_library_root = c_entry_path.strip_prefix(&c_prefix).unwrap(); 178 | let parent = at_library_root 179 | .parent() 180 | .and_then(|x| x.to_str()) 181 | .unwrap(); 182 | 183 | f(&c_entry_path, at_library_root, c_prefix_str, parent) 184 | } 185 | } 186 | } 187 | 188 | /// Expands the file at `path` into `result`, recursively expanding non-visited 189 | /// header files. 190 | fn expand(opt: &Opt, 191 | result: &mut String, 192 | include_directive_lines: &HashMap, 193 | visited: &mut PathSet, 194 | path: &Path) { 195 | verbose_eprintln!(opt, "expanding {:#?}", path); 196 | 197 | let f = expect_fmt!(File::open(path), "File {:#?} doesn't exist", path); 198 | let f = BufReader::new(f); 199 | 200 | for line in f.lines() 201 | .filter_map(|e| e.ok()) 202 | .filter(|l| !is_comment(l) && !is_pragma_once(l)) { 203 | 204 | if let Some(cpath) = include_directive_lines.get(&line) { 205 | if !visited.contains(cpath) { 206 | visited.insert(cpath.clone()); 207 | expand(opt, result, include_directive_lines, visited, cpath); 208 | } 209 | } else { 210 | *result += &line; 211 | } 212 | 213 | *result += "\n"; 214 | } 215 | } 216 | 217 | /// Prints the final header file to `stdout`. 218 | fn produce_final_result(opt: &Opt, 219 | top_include_path: &Path, 220 | include_directive_lines: &HashMap) 221 | -> String { 222 | let mut result = String::new(); 223 | result.reserve(1024 * 24); // TODO: calculate from source files 224 | 225 | result += "// generated with `unosolo` 0.1.1\n"; 226 | result += "// https://github.com/SuperV1234/unosolo\n"; 227 | result += "#pragma once\n\n"; 228 | 229 | let mut visited = PathSet::new(); 230 | expand(opt, 231 | &mut result, 232 | include_directive_lines, 233 | &mut visited, 234 | top_include_path); 235 | 236 | result 237 | } 238 | 239 | fn resolve_absolute_includes(opt: &Opt) -> HashMap { 240 | let mut absolute_includes = HashMap::new(); 241 | for_all_headers(opt, |c_entry_path, at_library_root, _, _| { 242 | absolute_includes 243 | .entry(at_library_root.to_str().unwrap().to_string()) 244 | .or_insert_with(|| c_entry_path.to_path_buf()); 245 | }); 246 | 247 | absolute_includes 248 | } 249 | 250 | fn resolve_include_directive_lines(opt: &Opt, 251 | absolute_includes: &HashMap) 252 | -> HashMap { 253 | let mut include_directive_lines = HashMap::new(); 254 | for_all_headers(opt, |c_entry_path, _, prefix, parent| { 255 | fill_dependencies(&mut include_directive_lines, 256 | absolute_includes, 257 | c_entry_path, 258 | prefix, 259 | parent); 260 | }); 261 | 262 | include_directive_lines 263 | } 264 | 265 | fn produce_final_result_from_opt(opt: &Opt) -> String { 266 | 267 | // Resolve `absolute_includes` with "`<...>` -> absolute path" hash map. 268 | let absolute_includes = resolve_absolute_includes(opt); 269 | verbose_eprettyprint!(opt, absolute_includes); 270 | 271 | // Resolve `#include` directive lines with "line -> absolute path" hash map. 272 | let include_directive_lines = resolve_include_directive_lines(opt, &absolute_includes); 273 | verbose_eprettyprint!(opt, include_directive_lines); 274 | 275 | // Traverse graph starting from "top include path" and return "final 276 | // single header" string. 277 | produce_final_result(opt, 278 | &unwrap_canonicalize(&opt.top_include), 279 | &include_directive_lines) 280 | } 281 | 282 | fn main() { 283 | let opt = Opt::from_args(); 284 | verbose_eprintln!(opt, "Options: {:#?}", opt); 285 | 286 | // Produce final single header and print to `stdout`. 287 | println!("{}", produce_final_result_from_opt(&opt)); 288 | } 289 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #/bin/bash 2 | 3 | RED='\033[0;31m' 4 | NC='\033[0m' 5 | 6 | mkdir ./tempunosolotest 7 | cd ./tempunosolotest 8 | 9 | git clone https://github.com/SuperV1234/scelta.git && \ 10 | unosolo -p"./scelta/include" -t"./scelta/include/scelta.hpp" > scelta_sh.hpp && \ 11 | g++ -std=c++1z ./scelta_sh.hpp && \ 12 | clang++ -std=c++1z ./scelta_sh.hpp && \ 13 | echo -e "${RED}scelta OK!${NC}" 14 | 15 | git clone https://github.com/SuperV1234/vrm_pp.git && \ 16 | unosolo -p"./vrm_pp/include" -t"./vrm_pp/include/vrm/pp.hpp" > vrm_pp_sh.hpp && \ 17 | g++ -std=c++1z ./vrm_pp_sh.hpp && \ 18 | clang++ -std=c++1z ./vrm_pp_sh.hpp && \ 19 | echo -e "${RED}vrm_pp OK!${NC}" 20 | 21 | git clone https://github.com/SuperV1234/vrm_core.git && \ 22 | unosolo -p"./vrm_pp/include" "./vrm_core/include" -t"./vrm_core/include/vrm/core.hpp" > vrm_core_sh.hpp && \ 23 | g++ -std=c++1z ./vrm_core_sh.hpp && \ 24 | clang++ -std=c++1z ./vrm_core_sh.hpp && \ 25 | echo -e "${RED}vrm_core OK!${NC}" 26 | 27 | git clone https://github.com/boostorg/hana && \ 28 | unosolo -p"./hana/include" -t"./hana/include/boost/hana.hpp" > hana_sh.hpp && \ 29 | g++ -std=c++1z ./hana_sh.hpp && \ 30 | clang++ -std=c++1z ./hana_sh.hpp && \ 31 | echo -e "${RED}hana OK!${NC}" 32 | --------------------------------------------------------------------------------