├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── rust-toolchain.toml ├── src ├── cli.rs ├── filter.rs ├── logging.rs ├── main.rs ├── process.rs └── resolve.rs └── tests └── integration ├── filtering.rs ├── inlining.rs ├── main.rs ├── misc.rs ├── resolving.rs └── util.rs /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: push 2 | name: ci 3 | 4 | jobs: 5 | fmt: 6 | name: Source formatting check 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout 10 | uses: actions/checkout@v2 11 | 12 | - name: Install rust 13 | uses: actions-rs/toolchain@v1 14 | with: 15 | profile: minimal 16 | toolchain: stable 17 | override: true 18 | components: rustfmt 19 | 20 | - name: Check formatting 21 | uses: actions-rs/cargo@v1 22 | with: 23 | command: fmt 24 | args: -- --check 25 | 26 | check: 27 | name: Compilation check 28 | runs-on: ubuntu-latest 29 | strategy: 30 | matrix: 31 | rust: 32 | - stable 33 | - beta 34 | - nightly 35 | - 1.56.0 36 | steps: 37 | - name: Checkout 38 | uses: actions/checkout@v2 39 | 40 | - name: Install rust 41 | uses: actions-rs/toolchain@v1 42 | with: 43 | profile: minimal 44 | toolchain: ${{ matrix.rust }} 45 | override: true 46 | 47 | - name: Check compilation 48 | uses: actions-rs/cargo@v1 49 | with: 50 | command: check 51 | 52 | clippy: 53 | name: Lint check 54 | runs-on: ubuntu-latest 55 | steps: 56 | - name: Checkout 57 | uses: actions/checkout@v2 58 | 59 | - name: Install rust 60 | uses: actions-rs/toolchain@v1 61 | with: 62 | profile: minimal 63 | toolchain: stable 64 | override: true 65 | components: clippy 66 | 67 | - name: Check lints 68 | uses: actions-rs/cargo@v1 69 | with: 70 | command: clippy 71 | args: -- -D warnings 72 | 73 | test: 74 | name: Tests 75 | runs-on: ubuntu-latest 76 | steps: 77 | - name: Checkout 78 | uses: actions/checkout@v2 79 | 80 | - name: Install rust 81 | uses: actions-rs/toolchain@v1 82 | with: 83 | profile: minimal 84 | toolchain: stable 85 | override: true 86 | 87 | - name: Run tests 88 | uses: actions-rs/cargo@v1 89 | with: 90 | command: test 91 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [1.0.1] - 2022-06-07 4 | 5 | Updated dependencies. 6 | 7 | ## [1.0.0] - 2021-11-17 8 | 9 | Initial version. 10 | 11 | [1.0.0]: https://github.com/Felerius/cpp-amalgamate/releases/tag/1.0.0 12 | [1.0.1]: https://github.com/Felerius/cpp-amalgamate/releases/tag/1.0.1 13 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "0.7.18" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "anyhow" 16 | version = "1.0.57" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "08f9b8508dccb7687a1d6c4ce66b2b0ecef467c94667de27d8d7fe1f8d2a9cdc" 19 | 20 | [[package]] 21 | name = "assert_cmd" 22 | version = "2.0.4" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "93ae1ddd39efd67689deb1979d80bad3bf7f2b09c6e6117c8d1f2443b5e2f83e" 25 | dependencies = [ 26 | "bstr", 27 | "doc-comment", 28 | "predicates", 29 | "predicates-core", 30 | "predicates-tree", 31 | "wait-timeout", 32 | ] 33 | 34 | [[package]] 35 | name = "assert_fs" 36 | version = "1.0.7" 37 | source = "registry+https://github.com/rust-lang/crates.io-index" 38 | checksum = "cf09bb72e00da477c2596865e8873227e2196d263cca35414048875dbbeea1be" 39 | dependencies = [ 40 | "doc-comment", 41 | "globwalk", 42 | "predicates", 43 | "predicates-core", 44 | "predicates-tree", 45 | "tempfile", 46 | ] 47 | 48 | [[package]] 49 | name = "atty" 50 | version = "0.2.14" 51 | source = "registry+https://github.com/rust-lang/crates.io-index" 52 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 53 | dependencies = [ 54 | "hermit-abi", 55 | "libc", 56 | "winapi", 57 | ] 58 | 59 | [[package]] 60 | name = "autocfg" 61 | version = "1.1.0" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 64 | 65 | [[package]] 66 | name = "bitflags" 67 | version = "1.3.2" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 70 | 71 | [[package]] 72 | name = "bstr" 73 | version = "0.2.17" 74 | source = "registry+https://github.com/rust-lang/crates.io-index" 75 | checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" 76 | dependencies = [ 77 | "lazy_static", 78 | "memchr", 79 | "regex-automata", 80 | ] 81 | 82 | [[package]] 83 | name = "cfg-if" 84 | version = "1.0.0" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 87 | 88 | [[package]] 89 | name = "clap" 90 | version = "3.1.18" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "d2dbdf4bdacb33466e854ce889eee8dfd5729abf7ccd7664d0a2d60cd384440b" 93 | dependencies = [ 94 | "atty", 95 | "bitflags", 96 | "clap_derive", 97 | "clap_lex", 98 | "indexmap", 99 | "lazy_static", 100 | "strsim", 101 | "termcolor", 102 | "terminal_size", 103 | "textwrap", 104 | ] 105 | 106 | [[package]] 107 | name = "clap_derive" 108 | version = "3.1.18" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "25320346e922cffe59c0bbc5410c8d8784509efb321488971081313cb1e1a33c" 111 | dependencies = [ 112 | "heck", 113 | "proc-macro-error", 114 | "proc-macro2", 115 | "quote", 116 | "syn", 117 | ] 118 | 119 | [[package]] 120 | name = "clap_lex" 121 | version = "0.2.0" 122 | source = "registry+https://github.com/rust-lang/crates.io-index" 123 | checksum = "a37c35f1112dad5e6e0b1adaff798507497a18fceeb30cceb3bae7d1427b9213" 124 | dependencies = [ 125 | "os_str_bytes", 126 | ] 127 | 128 | [[package]] 129 | name = "cpp-amalgamate" 130 | version = "1.0.1" 131 | dependencies = [ 132 | "anyhow", 133 | "assert_cmd", 134 | "assert_fs", 135 | "clap", 136 | "env_logger", 137 | "globset", 138 | "indoc", 139 | "itertools", 140 | "log", 141 | "once_cell", 142 | "predicates", 143 | "regex", 144 | ] 145 | 146 | [[package]] 147 | name = "crossbeam-utils" 148 | version = "0.8.8" 149 | source = "registry+https://github.com/rust-lang/crates.io-index" 150 | checksum = "0bf124c720b7686e3c2663cf54062ab0f68a88af2fb6a030e87e30bf721fcb38" 151 | dependencies = [ 152 | "cfg-if", 153 | "lazy_static", 154 | ] 155 | 156 | [[package]] 157 | name = "difflib" 158 | version = "0.4.0" 159 | source = "registry+https://github.com/rust-lang/crates.io-index" 160 | checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" 161 | 162 | [[package]] 163 | name = "doc-comment" 164 | version = "0.3.3" 165 | source = "registry+https://github.com/rust-lang/crates.io-index" 166 | checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" 167 | 168 | [[package]] 169 | name = "either" 170 | version = "1.6.1" 171 | source = "registry+https://github.com/rust-lang/crates.io-index" 172 | checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" 173 | 174 | [[package]] 175 | name = "env_logger" 176 | version = "0.9.0" 177 | source = "registry+https://github.com/rust-lang/crates.io-index" 178 | checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3" 179 | dependencies = [ 180 | "atty", 181 | "humantime", 182 | "log", 183 | "regex", 184 | "termcolor", 185 | ] 186 | 187 | [[package]] 188 | name = "fastrand" 189 | version = "1.7.0" 190 | source = "registry+https://github.com/rust-lang/crates.io-index" 191 | checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" 192 | dependencies = [ 193 | "instant", 194 | ] 195 | 196 | [[package]] 197 | name = "float-cmp" 198 | version = "0.9.0" 199 | source = "registry+https://github.com/rust-lang/crates.io-index" 200 | checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" 201 | dependencies = [ 202 | "num-traits", 203 | ] 204 | 205 | [[package]] 206 | name = "fnv" 207 | version = "1.0.7" 208 | source = "registry+https://github.com/rust-lang/crates.io-index" 209 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 210 | 211 | [[package]] 212 | name = "globset" 213 | version = "0.4.8" 214 | source = "registry+https://github.com/rust-lang/crates.io-index" 215 | checksum = "10463d9ff00a2a068db14231982f5132edebad0d7660cd956a1c30292dbcbfbd" 216 | dependencies = [ 217 | "aho-corasick", 218 | "bstr", 219 | "fnv", 220 | "log", 221 | "regex", 222 | ] 223 | 224 | [[package]] 225 | name = "globwalk" 226 | version = "0.8.1" 227 | source = "registry+https://github.com/rust-lang/crates.io-index" 228 | checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc" 229 | dependencies = [ 230 | "bitflags", 231 | "ignore", 232 | "walkdir", 233 | ] 234 | 235 | [[package]] 236 | name = "hashbrown" 237 | version = "0.11.2" 238 | source = "registry+https://github.com/rust-lang/crates.io-index" 239 | checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" 240 | 241 | [[package]] 242 | name = "heck" 243 | version = "0.4.0" 244 | source = "registry+https://github.com/rust-lang/crates.io-index" 245 | checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" 246 | 247 | [[package]] 248 | name = "hermit-abi" 249 | version = "0.1.19" 250 | source = "registry+https://github.com/rust-lang/crates.io-index" 251 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 252 | dependencies = [ 253 | "libc", 254 | ] 255 | 256 | [[package]] 257 | name = "humantime" 258 | version = "2.1.0" 259 | source = "registry+https://github.com/rust-lang/crates.io-index" 260 | checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" 261 | 262 | [[package]] 263 | name = "ignore" 264 | version = "0.4.18" 265 | source = "registry+https://github.com/rust-lang/crates.io-index" 266 | checksum = "713f1b139373f96a2e0ce3ac931cd01ee973c3c5dd7c40c0c2efe96ad2b6751d" 267 | dependencies = [ 268 | "crossbeam-utils", 269 | "globset", 270 | "lazy_static", 271 | "log", 272 | "memchr", 273 | "regex", 274 | "same-file", 275 | "thread_local", 276 | "walkdir", 277 | "winapi-util", 278 | ] 279 | 280 | [[package]] 281 | name = "indexmap" 282 | version = "1.8.2" 283 | source = "registry+https://github.com/rust-lang/crates.io-index" 284 | checksum = "e6012d540c5baa3589337a98ce73408de9b5a25ec9fc2c6fd6be8f0d39e0ca5a" 285 | dependencies = [ 286 | "autocfg", 287 | "hashbrown", 288 | ] 289 | 290 | [[package]] 291 | name = "indoc" 292 | version = "1.0.6" 293 | source = "registry+https://github.com/rust-lang/crates.io-index" 294 | checksum = "05a0bd019339e5d968b37855180087b7b9d512c5046fbd244cf8c95687927d6e" 295 | 296 | [[package]] 297 | name = "instant" 298 | version = "0.1.12" 299 | source = "registry+https://github.com/rust-lang/crates.io-index" 300 | checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" 301 | dependencies = [ 302 | "cfg-if", 303 | ] 304 | 305 | [[package]] 306 | name = "itertools" 307 | version = "0.10.3" 308 | source = "registry+https://github.com/rust-lang/crates.io-index" 309 | checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3" 310 | dependencies = [ 311 | "either", 312 | ] 313 | 314 | [[package]] 315 | name = "lazy_static" 316 | version = "1.4.0" 317 | source = "registry+https://github.com/rust-lang/crates.io-index" 318 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 319 | 320 | [[package]] 321 | name = "libc" 322 | version = "0.2.126" 323 | source = "registry+https://github.com/rust-lang/crates.io-index" 324 | checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" 325 | 326 | [[package]] 327 | name = "log" 328 | version = "0.4.17" 329 | source = "registry+https://github.com/rust-lang/crates.io-index" 330 | checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" 331 | dependencies = [ 332 | "cfg-if", 333 | ] 334 | 335 | [[package]] 336 | name = "memchr" 337 | version = "2.5.0" 338 | source = "registry+https://github.com/rust-lang/crates.io-index" 339 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" 340 | 341 | [[package]] 342 | name = "normalize-line-endings" 343 | version = "0.3.0" 344 | source = "registry+https://github.com/rust-lang/crates.io-index" 345 | checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" 346 | 347 | [[package]] 348 | name = "num-traits" 349 | version = "0.2.15" 350 | source = "registry+https://github.com/rust-lang/crates.io-index" 351 | checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" 352 | dependencies = [ 353 | "autocfg", 354 | ] 355 | 356 | [[package]] 357 | name = "once_cell" 358 | version = "1.12.0" 359 | source = "registry+https://github.com/rust-lang/crates.io-index" 360 | checksum = "7709cef83f0c1f58f666e746a08b21e0085f7440fa6a29cc194d68aac97a4225" 361 | 362 | [[package]] 363 | name = "os_str_bytes" 364 | version = "6.1.0" 365 | source = "registry+https://github.com/rust-lang/crates.io-index" 366 | checksum = "21326818e99cfe6ce1e524c2a805c189a99b5ae555a35d19f9a284b427d86afa" 367 | 368 | [[package]] 369 | name = "predicates" 370 | version = "2.1.1" 371 | source = "registry+https://github.com/rust-lang/crates.io-index" 372 | checksum = "a5aab5be6e4732b473071984b3164dbbfb7a3674d30ea5ff44410b6bcd960c3c" 373 | dependencies = [ 374 | "difflib", 375 | "float-cmp", 376 | "itertools", 377 | "normalize-line-endings", 378 | "predicates-core", 379 | "regex", 380 | ] 381 | 382 | [[package]] 383 | name = "predicates-core" 384 | version = "1.0.3" 385 | source = "registry+https://github.com/rust-lang/crates.io-index" 386 | checksum = "da1c2388b1513e1b605fcec39a95e0a9e8ef088f71443ef37099fa9ae6673fcb" 387 | 388 | [[package]] 389 | name = "predicates-tree" 390 | version = "1.0.5" 391 | source = "registry+https://github.com/rust-lang/crates.io-index" 392 | checksum = "4d86de6de25020a36c6d3643a86d9a6a9f552107c0559c60ea03551b5e16c032" 393 | dependencies = [ 394 | "predicates-core", 395 | "termtree", 396 | ] 397 | 398 | [[package]] 399 | name = "proc-macro-error" 400 | version = "1.0.4" 401 | source = "registry+https://github.com/rust-lang/crates.io-index" 402 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 403 | dependencies = [ 404 | "proc-macro-error-attr", 405 | "proc-macro2", 406 | "quote", 407 | "syn", 408 | "version_check", 409 | ] 410 | 411 | [[package]] 412 | name = "proc-macro-error-attr" 413 | version = "1.0.4" 414 | source = "registry+https://github.com/rust-lang/crates.io-index" 415 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 416 | dependencies = [ 417 | "proc-macro2", 418 | "quote", 419 | "version_check", 420 | ] 421 | 422 | [[package]] 423 | name = "proc-macro2" 424 | version = "1.0.39" 425 | source = "registry+https://github.com/rust-lang/crates.io-index" 426 | checksum = "c54b25569025b7fc9651de43004ae593a75ad88543b17178aa5e1b9c4f15f56f" 427 | dependencies = [ 428 | "unicode-ident", 429 | ] 430 | 431 | [[package]] 432 | name = "quote" 433 | version = "1.0.18" 434 | source = "registry+https://github.com/rust-lang/crates.io-index" 435 | checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" 436 | dependencies = [ 437 | "proc-macro2", 438 | ] 439 | 440 | [[package]] 441 | name = "redox_syscall" 442 | version = "0.2.13" 443 | source = "registry+https://github.com/rust-lang/crates.io-index" 444 | checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42" 445 | dependencies = [ 446 | "bitflags", 447 | ] 448 | 449 | [[package]] 450 | name = "regex" 451 | version = "1.5.6" 452 | source = "registry+https://github.com/rust-lang/crates.io-index" 453 | checksum = "d83f127d94bdbcda4c8cc2e50f6f84f4b611f69c902699ca385a39c3a75f9ff1" 454 | dependencies = [ 455 | "aho-corasick", 456 | "memchr", 457 | "regex-syntax", 458 | ] 459 | 460 | [[package]] 461 | name = "regex-automata" 462 | version = "0.1.10" 463 | source = "registry+https://github.com/rust-lang/crates.io-index" 464 | checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" 465 | 466 | [[package]] 467 | name = "regex-syntax" 468 | version = "0.6.26" 469 | source = "registry+https://github.com/rust-lang/crates.io-index" 470 | checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64" 471 | 472 | [[package]] 473 | name = "remove_dir_all" 474 | version = "0.5.3" 475 | source = "registry+https://github.com/rust-lang/crates.io-index" 476 | checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" 477 | dependencies = [ 478 | "winapi", 479 | ] 480 | 481 | [[package]] 482 | name = "same-file" 483 | version = "1.0.6" 484 | source = "registry+https://github.com/rust-lang/crates.io-index" 485 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 486 | dependencies = [ 487 | "winapi-util", 488 | ] 489 | 490 | [[package]] 491 | name = "strsim" 492 | version = "0.10.0" 493 | source = "registry+https://github.com/rust-lang/crates.io-index" 494 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 495 | 496 | [[package]] 497 | name = "syn" 498 | version = "1.0.96" 499 | source = "registry+https://github.com/rust-lang/crates.io-index" 500 | checksum = "0748dd251e24453cb8717f0354206b91557e4ec8703673a4b30208f2abaf1ebf" 501 | dependencies = [ 502 | "proc-macro2", 503 | "quote", 504 | "unicode-ident", 505 | ] 506 | 507 | [[package]] 508 | name = "tempfile" 509 | version = "3.3.0" 510 | source = "registry+https://github.com/rust-lang/crates.io-index" 511 | checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" 512 | dependencies = [ 513 | "cfg-if", 514 | "fastrand", 515 | "libc", 516 | "redox_syscall", 517 | "remove_dir_all", 518 | "winapi", 519 | ] 520 | 521 | [[package]] 522 | name = "termcolor" 523 | version = "1.1.3" 524 | source = "registry+https://github.com/rust-lang/crates.io-index" 525 | checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" 526 | dependencies = [ 527 | "winapi-util", 528 | ] 529 | 530 | [[package]] 531 | name = "terminal_size" 532 | version = "0.1.17" 533 | source = "registry+https://github.com/rust-lang/crates.io-index" 534 | checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df" 535 | dependencies = [ 536 | "libc", 537 | "winapi", 538 | ] 539 | 540 | [[package]] 541 | name = "termtree" 542 | version = "0.2.4" 543 | source = "registry+https://github.com/rust-lang/crates.io-index" 544 | checksum = "507e9898683b6c43a9aa55b64259b721b52ba226e0f3779137e50ad114a4c90b" 545 | 546 | [[package]] 547 | name = "textwrap" 548 | version = "0.15.0" 549 | source = "registry+https://github.com/rust-lang/crates.io-index" 550 | checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" 551 | dependencies = [ 552 | "terminal_size", 553 | ] 554 | 555 | [[package]] 556 | name = "thread_local" 557 | version = "1.1.4" 558 | source = "registry+https://github.com/rust-lang/crates.io-index" 559 | checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" 560 | dependencies = [ 561 | "once_cell", 562 | ] 563 | 564 | [[package]] 565 | name = "unicode-ident" 566 | version = "1.0.0" 567 | source = "registry+https://github.com/rust-lang/crates.io-index" 568 | checksum = "d22af068fba1eb5edcb4aea19d382b2a3deb4c8f9d475c589b6ada9e0fd493ee" 569 | 570 | [[package]] 571 | name = "version_check" 572 | version = "0.9.4" 573 | source = "registry+https://github.com/rust-lang/crates.io-index" 574 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 575 | 576 | [[package]] 577 | name = "wait-timeout" 578 | version = "0.2.0" 579 | source = "registry+https://github.com/rust-lang/crates.io-index" 580 | checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" 581 | dependencies = [ 582 | "libc", 583 | ] 584 | 585 | [[package]] 586 | name = "walkdir" 587 | version = "2.3.2" 588 | source = "registry+https://github.com/rust-lang/crates.io-index" 589 | checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" 590 | dependencies = [ 591 | "same-file", 592 | "winapi", 593 | "winapi-util", 594 | ] 595 | 596 | [[package]] 597 | name = "winapi" 598 | version = "0.3.9" 599 | source = "registry+https://github.com/rust-lang/crates.io-index" 600 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 601 | dependencies = [ 602 | "winapi-i686-pc-windows-gnu", 603 | "winapi-x86_64-pc-windows-gnu", 604 | ] 605 | 606 | [[package]] 607 | name = "winapi-i686-pc-windows-gnu" 608 | version = "0.4.0" 609 | source = "registry+https://github.com/rust-lang/crates.io-index" 610 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 611 | 612 | [[package]] 613 | name = "winapi-util" 614 | version = "0.1.5" 615 | source = "registry+https://github.com/rust-lang/crates.io-index" 616 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 617 | dependencies = [ 618 | "winapi", 619 | ] 620 | 621 | [[package]] 622 | name = "winapi-x86_64-pc-windows-gnu" 623 | version = "0.4.0" 624 | source = "registry+https://github.com/rust-lang/crates.io-index" 625 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 626 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cpp-amalgamate" 3 | version = "1.0.1" 4 | authors = ["David Stangl "] 5 | description = """ 6 | cpp-amalgamate recursively combines C++ source files and the headers they include into a single 7 | output file. 8 | """ 9 | repository = "https://github.com/Felerius/cpp-amalgamate" 10 | homepage = "https://github.com/Felerius/cpp-amalgamate" 11 | documentation = "https://github.com/Felerius/cpp-amalgamate" 12 | keywords = ["cpp", "amalgamation", "competitive", "programming"] 13 | categories = ["command-line-utilities", "development-tools"] 14 | license = "MIT" 15 | edition = "2021" 16 | rust-version = "1.56" 17 | resolver = "2" 18 | 19 | [dependencies] 20 | anyhow = "1.0.57" 21 | clap = { version = "3.1.18", features = ["derive", "wrap_help"] } 22 | env_logger = "0.9.0" 23 | globset = "0.4.8" 24 | itertools = "0.10.3" 25 | log = { version = "0.4.17", features = ["std"] } 26 | regex = "1.5.6" 27 | 28 | [dev-dependencies] 29 | assert_cmd = "2.0.4" 30 | assert_fs = "1.0.7" 31 | indoc = "1.0.6" 32 | once_cell = "1.12.0" 33 | predicates = "2.1.1" 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 David Stangl 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 |
2 | 3 | # cpp-amalgamate 4 | 5 | [![Build status](https://github.com/Felerius/cpp-inline-includes/actions/workflows/ci.yml/badge.svg)](https://github.com/Felerius/cpp-inline-includes/actions) 6 | [![License](https://img.shields.io/crates/l/cpp-amalgamate)](https://github.com/Felerius/cpp-amalgamate/blob/main/LICENSE) 7 | [![Crates.io](https://img.shields.io/crates/v/cpp-amalgamate)](https://crates.io/crates/cpp-amalgamate) 8 | 9 |
10 | 11 | cpp-amalgamate recursively combines C++ source files and the headers they include into a single 12 | output file. It tracks which headers have been included and skips any further references to them. 13 | Which includes are inlined and which are left as is can be precisely controlled. 14 | 15 | It originated as an automated way to inline pre-written snippets when submitting to competitive 16 | programming sites such as [Codeforces](https://codeforces.com/) or [AtCoder](https://atcoder.jp). 17 | Since then, it has been generalized and might be useful in other contexts as well. 18 | 19 | ## Features & limitations 20 | 21 | When provided with one or more source files and accompanying search directories for includes, 22 | cpp-amalgamate will concatenate the source files and recursively inline all include statements it 23 | can resolve using the given search directories (or from the current directory, for 24 | `#include "..."`). While, by default, all resolvable includes are inlined, this can be controlled 25 | using the `--filter*` family of options. It can also insert corresponding `#line num "file"` 26 | directives which allows compilers or debuggers to resolve lines in the combined file back to their 27 | origin. 28 | 29 | However, cpp-amalgamate does not interpret preprocessor instructions beyond `#include`. This notably 30 | means that it cannot understand traditional header guards using `#if` instructions. Instead, 31 | cpp-amalgamate assumes that every header should be included at most once, as if it was guarded by a 32 | header guard or `#pragma once`. It does detect `#pragma once` instructions and removes them, as 33 | these cause warnings or errors when compiling the combined file with some compilers. 34 | 35 | This simplified behavior also might cause problems if `#include` statements themselves are inside 36 | `#if` blocks. If the same header is referenced inside two separate `#if` blocks, it will only be 37 | expanded in the former while the latter `#include` will be removed. 38 | 39 | ## Usage 40 | 41 | The basic invocation for cpp-amalgamate is 42 | 43 | ```shell 44 | cpp-amalgamate [options] source-files... 45 | ``` 46 | 47 | To specify search directories, use `-d`/`--dir`. You can also use `--dir-quote` or `--dir-system` 48 | for search directories that should only be used for quote (i.e., `#include "..."`) or system 49 | includes (i.e., `#include <...>`). Note that cpp-amalgamate does not use any search directories by 50 | default! 51 | 52 | ### Filtering 53 | 54 | Using `-f`/`--filter`, you can specify globs for includes that should not be inlined. As with search 55 | directories, `--filter-quote` and `--filter-system` are versions only applicable to one type of 56 | include. Globs can be inverted with a leading `!`, causing matching headers to be inlined even if a 57 | previous glob excluded them. Globs are evaluated in order, with the last matching glob determining 58 | whether a header is included or not. By default (i.e., if no glob matches), all headers are inlined. 59 | 60 | Note that these globs are applied to the absolute path of the header with all symbolic links 61 | resolved. This means that often a `**` fragment will be necessary, which matches any number of path 62 | entries. That is, 63 | 64 | * `**` matches any file, 65 | * `**/*.hpp` all files with the extension `.hpp`, 66 | * and `/usr/local/include/**` all files in `/usr/local/include`. 67 | 68 | For the full details on the supported syntax, check the 69 | [globset documentation](https://docs.rs/globset/0.4.8/globset/#syntax). 70 | 71 | ### Miscellaneous 72 | 73 | Other flags supported by cpp-amalgamate are: 74 | 75 | * `-o`/`--output`: Write the combined source file to a file rather than the standard output. 76 | * `--line-directives`: Add `#line num "file"` directives to the output, allowing compilers and 77 | debuggers to resolve lines to their original files. 78 | * `-v`/`--verbose` and `-q`/`--quiet`: Increase or decrease the level of log messages shown. By 79 | default, only warnings and errors are shown. 80 | * `--unresolvable-include`: Specifies what is done when an include cannot be resolved. Possible 81 | values are `error`, `warn`, and `ignore`, with the latter being the default. This can be useful 82 | to assert that all includes end up inlined Also available as `--unresolvable-quote-include` and 83 | `--unresolvable-system-include`. 84 | * `--cyclic-include`: Specifies how a cyclic include is handled. Supports the same values as 85 | `--unresolvable-include` except with `error` as the default. 86 | 87 | ## Installation 88 | 89 | Each [GitHub release](https://github.com/Felerius/cpp-amalgamate/releases) contains precompiled 90 | binaries for most common operating systems/architectures. Alternatively, cpp-amalgamate can be 91 | installed using cargo which is bundled with Rust: 92 | 93 | ```shell 94 | cargo install cpp-amalgamate 95 | ``` 96 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.56.0" 3 | components = ["rustfmt", "clippy"] 4 | -------------------------------------------------------------------------------- /src/cli.rs: -------------------------------------------------------------------------------- 1 | //! Definition and parsing of cli arguments 2 | use std::path::{Path, PathBuf}; 3 | 4 | use clap::{ArgMatches, FromArgMatches, IntoApp, Parser}; 5 | use itertools::Itertools; 6 | use log::LevelFilter; 7 | 8 | use crate::{filter::InvertibleGlob, logging::ErrorHandling}; 9 | 10 | const ABOUT: &str = "cpp-amalgamate recursively combines C++ source files and the headers they 11 | include into a single output file. It tracks which headers have been included and skips any further 12 | references to them. Which includes are inlined and which are left as is can be precisely 13 | controlled."; 14 | const HELP_TEMPLATE: &str = "\ 15 | {before-help}{bin} {version}\n\ 16 | {author-with-newline}\ 17 | {about-section}\n\ 18 | {usage-heading}\n {usage}\n\ 19 | \n\ 20 | {all-args}{after-help}\ 21 | "; 22 | 23 | #[derive(Debug, Parser)] 24 | #[clap( 25 | author, 26 | version, 27 | about=ABOUT, 28 | help_template=HELP_TEMPLATE, 29 | hide_possible_values=true, 30 | // To make this work, we cannot use default_value for arguments. 31 | arg_required_else_help=true, 32 | )] 33 | pub struct Opts { 34 | /// ArgMatches used to create this instance 35 | #[clap(skip)] 36 | matches: ArgMatches, 37 | 38 | /// Source files to process 39 | #[clap(required = true, parse(from_os_str))] 40 | pub files: Vec, 41 | 42 | /// Redirect output to a file 43 | #[clap(short, long, parse(from_os_str), value_name = "file")] 44 | pub output: Option, 45 | 46 | /// Add a search directory for both system and quote includes 47 | #[clap( 48 | short, 49 | long, 50 | value_name = "dir", 51 | multiple_occurrences = true, 52 | number_of_values = 1 53 | )] 54 | dir: Vec, 55 | 56 | /// Add a search directory for quote includes 57 | #[clap( 58 | long, 59 | parse(from_os_str), 60 | value_name = "dir", 61 | multiple_occurrences = true, 62 | number_of_values = 1 63 | )] 64 | dir_quote: Vec, 65 | 66 | /// Add a search directory for system includes 67 | #[clap( 68 | long, 69 | parse(from_os_str), 70 | value_name = "dir", 71 | multiple_occurrences = true, 72 | number_of_values = 1 73 | )] 74 | dir_system: Vec, 75 | 76 | /// Filter which includes are inlined. 77 | /// 78 | /// By default, cpp-amalgamate inlines every header it can resolve using the given search 79 | /// directories. With this option, headers can be excluded from being inlined. By prefixing the 80 | /// glob with '!', previously excluded files can be selectively added again. The globs given to 81 | /// this and --filter-quote/--filter-system are evaluated in order, with the latest matching 82 | /// glob taking precedence. 83 | /// 84 | /// Globs are matched on the full path of the included file, with all symlinks resolved. A '**' 85 | /// can be used to match any number of directories, for example '**/a.hpp', 86 | /// '/usr/local/include/**', or even '/usr/**/*.hpp'. 87 | #[clap( 88 | short, 89 | long, 90 | value_name = "glob", 91 | multiple_occurrences = true, 92 | number_of_values = 1 93 | )] 94 | filter: Vec, 95 | 96 | /// Filter which quote includes are inlined. 97 | /// 98 | /// This option works just like --filter, except it only applies to quote includes. 99 | #[clap( 100 | long, 101 | value_name = "glob", 102 | multiple_occurrences = true, 103 | number_of_values = 1 104 | )] 105 | filter_quote: Vec, 106 | 107 | /// Filter which system includes are inlined. 108 | /// 109 | /// This option works just like --filter, except it only applies to system includes. 110 | #[clap( 111 | long, 112 | value_name = "glob", 113 | multiple_occurrences = true, 114 | number_of_values = 1 115 | )] 116 | filter_system: Vec, 117 | 118 | /// How to handle an unresolvable include. 119 | /// 120 | /// By default, cpp-amalgamate ignores includes which cannot be resolved to allow specifying 121 | /// only the necessary search directories. This flag can be used to assert that all includes are 122 | /// being inlined. 123 | /// 124 | /// The possible values for this flag are error (aborts processing), warn (continues 125 | /// processing), and ignore (the default). 126 | #[clap( 127 | long, 128 | value_name = "handling", 129 | possible_values = &ErrorHandling::NAMES, 130 | conflicts_with_all = &["unresolvable-quote-include", "unresolvable-system-include"] 131 | )] 132 | unresolvable_include: Option, 133 | 134 | /// How to handle an unresolvable quote include. 135 | /// 136 | /// Works like --unresolvable-include, except only for quote includes. 137 | #[clap( 138 | long, 139 | value_name = "handling", 140 | possible_values = &ErrorHandling::NAMES, 141 | )] 142 | unresolvable_quote_include: Option, 143 | 144 | /// How to handle an unresolvable system include. 145 | /// 146 | /// Works like --unresolvable-include, except only for system includes. 147 | #[clap( 148 | long, 149 | value_name = "handling", 150 | possible_values = &ErrorHandling::NAMES, 151 | )] 152 | unresolvable_system_include: Option, 153 | 154 | /// How to handle a cyclic include. 155 | /// 156 | /// Uses the same values as --unresolvable-include (error, warn, ignore), except that it 157 | /// defaults to error. 158 | #[clap( 159 | long, 160 | value_name = "handling", 161 | possible_values = &ErrorHandling::NAMES, 162 | )] 163 | cyclic_include: Option, 164 | 165 | /// Increase the verbosity of the output (can be passed multiple times). 166 | /// 167 | /// By default, only warnings and errors are reported. Passing '-v' includes info, '-vv' debug, 168 | /// and '-vvv` trace log messages. 169 | #[clap(short, long, parse(from_occurrences))] 170 | verbose: i8, 171 | 172 | /// Report only errors (-q) or nothing (-qq) 173 | #[clap(short, long, parse(from_occurrences), conflicts_with = "verbose")] 174 | quiet: i8, 175 | 176 | /// Add #line directives. 177 | /// 178 | /// These allow compilers and debuggers to resolve lines in the amalgamated file to their 179 | /// original files. 180 | #[clap(long)] 181 | pub line_directives: bool, 182 | } 183 | 184 | fn with_indices<'a, T>( 185 | matches: &'a ArgMatches, 186 | name: &str, 187 | values: &'a [T], 188 | ) -> impl Iterator + 'a { 189 | matches.indices_of(name).into_iter().flatten().zip(values) 190 | } 191 | 192 | impl Opts { 193 | pub fn parse() -> Self { 194 | let cmd = Self::command(); 195 | let matches = cmd.get_matches(); 196 | let mut opts = Self::from_arg_matches(&matches) 197 | .expect("from_arg_matches should never return None when derived?!"); 198 | opts.matches = matches; 199 | opts 200 | } 201 | 202 | fn merge_by_cli_order<'a, T>( 203 | &'a self, 204 | list1: &'a [T], 205 | name1: &str, 206 | list2: &'a [T], 207 | name2: &str, 208 | ) -> impl Iterator + 'a { 209 | with_indices(&self.matches, name1, list1) 210 | .merge_by(with_indices(&self.matches, name2, list2), |x, y| x.0 < y.0) 211 | .map(|(_, val)| val) 212 | } 213 | 214 | /// Returns a list of all quote search dirs in the order given on the cli. 215 | /// 216 | /// This is a merged list of the shared search dirs and the quote only search dirs. 217 | pub fn quote_search_dirs(&self) -> impl Iterator { 218 | self.merge_by_cli_order(&self.dir, "dir", &self.dir_quote, "dir-quote") 219 | .map(PathBuf::as_path) 220 | } 221 | 222 | /// Returns a list of all system search dirs in the order given on the cli. 223 | /// 224 | /// This is a merged list of the shared search dirs and the system only search dirs. 225 | pub fn system_search_dirs(&self) -> impl Iterator { 226 | self.merge_by_cli_order(&self.dir, "dir", &self.dir_system, "dir-system") 227 | .map(PathBuf::as_path) 228 | } 229 | 230 | /// Returns a list of all filter globs for quote includes in the order given on the cli. 231 | /// 232 | /// This is a merged list of the --filter and --filter-quote options. 233 | pub fn quote_filter_globs(&self) -> impl Iterator { 234 | self.merge_by_cli_order(&self.filter, "filter", &self.filter_quote, "filter-quote") 235 | } 236 | 237 | /// Returns a list of all filter globs for system includes in the order given on the cli. 238 | /// 239 | /// This is a merged list of the --filter and --filter-system options. 240 | pub fn system_filter_globs(&self) -> impl Iterator { 241 | self.merge_by_cli_order(&self.filter, "filter", &self.filter_system, "filter-system") 242 | } 243 | 244 | pub fn unresolvable_quote_include_handling(&self) -> ErrorHandling { 245 | self.unresolvable_include 246 | .or(self.unresolvable_quote_include) 247 | .unwrap_or(ErrorHandling::Ignore) 248 | } 249 | 250 | pub fn unresolvable_system_include_handling(&self) -> ErrorHandling { 251 | self.unresolvable_include 252 | .or(self.unresolvable_system_include) 253 | .unwrap_or(ErrorHandling::Ignore) 254 | } 255 | 256 | pub fn cyclic_include_handling(&self) -> ErrorHandling { 257 | self.cyclic_include.unwrap_or(ErrorHandling::Error) 258 | } 259 | 260 | pub fn log_level(&self) -> LevelFilter { 261 | match self.verbose - self.quiet { 262 | i8::MIN..=-2 => LevelFilter::Off, 263 | -1 => LevelFilter::Error, 264 | 0 => LevelFilter::Warn, 265 | 1 => LevelFilter::Info, 266 | 2 => LevelFilter::Debug, 267 | 3..=i8::MAX => LevelFilter::Trace, 268 | } 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /src/filter.rs: -------------------------------------------------------------------------------- 1 | //! Filtering of which includes to inline 2 | use std::{path::Path, str::FromStr}; 3 | 4 | use anyhow::{Error, Result}; 5 | use globset::{Candidate, Glob, GlobSet, GlobSetBuilder}; 6 | use log::{debug, log_enabled, Level}; 7 | 8 | use crate::logging::debug_file_name; 9 | 10 | #[derive(Debug, Clone)] 11 | pub struct InvertibleGlob { 12 | glob: Glob, 13 | inverted: bool, 14 | } 15 | 16 | impl FromStr for InvertibleGlob { 17 | type Err = Error; 18 | 19 | fn from_str(s: &str) -> Result { 20 | let (inverted, remainder) = s.strip_prefix('!').map_or((false, s), |tail| (true, tail)); 21 | Ok(Self { 22 | glob: Glob::new(remainder)?, 23 | inverted, 24 | }) 25 | } 26 | } 27 | 28 | #[derive(Debug)] 29 | struct GlobInfo { 30 | str: String, 31 | inverted: bool, 32 | } 33 | 34 | #[derive(Debug)] 35 | pub struct InliningFilter { 36 | quote_set: GlobSet, 37 | quote_infos: Vec, 38 | system_set: GlobSet, 39 | system_infos: Vec, 40 | indices: Vec, 41 | } 42 | 43 | fn build_set_and_infos( 44 | type_name: &str, 45 | globs: impl IntoIterator, 46 | ) -> Result<(GlobSet, Vec)> { 47 | let mut set_builder = GlobSetBuilder::new(); 48 | let infos: Vec<_> = globs 49 | .into_iter() 50 | .map(|invertible_glob| { 51 | let str = invertible_glob.glob.glob().to_owned(); 52 | set_builder.add(invertible_glob.glob); 53 | GlobInfo { 54 | str, 55 | inverted: invertible_glob.inverted, 56 | } 57 | }) 58 | .collect(); 59 | 60 | if log_enabled!(Level::Debug) { 61 | let glob_strs: Vec<_> = infos.iter().map(|info| info.str.clone()).collect(); 62 | debug!("{} ignore globs: {:?}", type_name, glob_strs); 63 | } 64 | 65 | Ok((set_builder.build()?, infos)) 66 | } 67 | 68 | fn check_should_inline( 69 | path: &Path, 70 | set: &GlobSet, 71 | infos: &[GlobInfo], 72 | indices: &mut Vec, 73 | ) -> bool { 74 | let candidate = Candidate::new(path); 75 | let log_name = debug_file_name(path); 76 | set.matches_candidate_into(&candidate, indices); 77 | if let Some(&idx) = indices.last() { 78 | let glob_str = &infos[idx].str; 79 | if infos[idx].inverted { 80 | debug!("Inlining {:?} (cause: '{}')", log_name, glob_str); 81 | true 82 | } else { 83 | debug!("Not inlining {:?} (cause: '{}')", log_name, glob_str); 84 | false 85 | } 86 | } else { 87 | debug!("Inlining {:?} by default", log_name); 88 | true 89 | } 90 | } 91 | 92 | impl InliningFilter { 93 | pub fn new( 94 | quote_globs: impl IntoIterator, 95 | system_globs: impl IntoIterator, 96 | ) -> Result { 97 | let (quote_set, quote_infos) = build_set_and_infos("Quote", quote_globs)?; 98 | let (system_set, system_infos) = build_set_and_infos("System", system_globs)?; 99 | Ok(Self { 100 | quote_set, 101 | quote_infos, 102 | system_set, 103 | system_infos, 104 | indices: Vec::new(), 105 | }) 106 | } 107 | 108 | /// Check whether a path should be included. 109 | pub fn should_inline(&mut self, path: &Path, is_system: bool) -> bool { 110 | let (set, infos) = if is_system { 111 | (&self.system_set, &self.system_infos) 112 | } else { 113 | (&self.quote_set, &self.quote_infos) 114 | }; 115 | check_should_inline(path, set, infos, &mut self.indices) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/logging.rs: -------------------------------------------------------------------------------- 1 | /// Simple stderr logger, with level filter and color controllable by cli arguments. 2 | use std::{ffi::OsStr, path::Path, str::FromStr}; 3 | 4 | use anyhow::{Error, Result}; 5 | 6 | pub fn debug_file_name(path: &Path) -> &OsStr { 7 | let default = OsStr::new(""); 8 | path.file_name().unwrap_or(default) 9 | } 10 | 11 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 12 | pub enum ErrorHandling { 13 | Error, 14 | Warn, 15 | Ignore, 16 | } 17 | 18 | impl ErrorHandling { 19 | pub const NAMES: [&'static str; 3] = ["error", "warn", "ignore"]; 20 | } 21 | 22 | impl FromStr for ErrorHandling { 23 | type Err = Error; 24 | 25 | fn from_str(s: &str) -> Result { 26 | Ok(match s { 27 | "error" => Self::Error, 28 | "warn" => Self::Warn, 29 | "ignore" => Self::Ignore, 30 | _ => anyhow::bail!("Invalid error level: \"{}\"", s), 31 | }) 32 | } 33 | } 34 | 35 | #[macro_export] 36 | macro_rules! error_handling_handle { 37 | ($handling:expr, $fmt:expr, $($arg:tt)*) => {{ 38 | match $handling { 39 | $crate::logging::ErrorHandling::Error => Err(::anyhow::format_err!($fmt, $($arg)*)), 40 | $crate::logging::ErrorHandling::Warn => { 41 | ::log::warn!($fmt, $($arg)*); 42 | Ok(()) 43 | } 44 | $crate::logging::ErrorHandling::Ignore => { 45 | ::log::debug!("Ignoring: {}", ::std::format_args!($fmt, $($arg)*)); 46 | Ok(()) 47 | } 48 | } 49 | }} 50 | } 51 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![warn( 2 | // Lint groups 3 | future_incompatible, 4 | nonstandard_style, 5 | rust_2018_compatibility, 6 | rust_2018_idioms, 7 | rust_2021_compatibility, 8 | // Allow by default 9 | elided_lifetimes_in_paths, 10 | missing_debug_implementations, 11 | trivial_casts, 12 | trivial_numeric_casts, 13 | unused_extern_crates, 14 | unused_import_braces, 15 | unused_qualifications, 16 | // Clippy 17 | clippy::all, 18 | clippy::pedantic, 19 | clippy::cargo, 20 | clippy::clone_on_ref_ptr, 21 | clippy::decimal_literal_representation, 22 | clippy::filetype_is_file, 23 | clippy::float_cmp_const, 24 | clippy::get_unwrap, 25 | clippy::if_then_some_else_none, 26 | clippy::rc_mutex, 27 | clippy::rest_pat_in_fully_bound_structs, 28 | clippy::shadow_unrelated, 29 | clippy::todo, 30 | clippy::unimplemented, 31 | clippy::unwrap_used, 32 | clippy::verbose_file_reads, 33 | )] 34 | #![allow(clippy::module_name_repetitions, clippy::non_ascii_literal)] 35 | 36 | mod cli; 37 | mod filter; 38 | mod logging; 39 | mod process; 40 | mod resolve; 41 | 42 | use std::{ 43 | env, 44 | fs::File, 45 | io::{self, BufWriter, Write}, 46 | path::PathBuf, 47 | }; 48 | 49 | use anyhow::{Context, Result}; 50 | use log::{error, info}; 51 | 52 | use crate::{ 53 | cli::Opts, 54 | filter::InliningFilter, 55 | logging::ErrorHandling, 56 | process::{ErrorHandlingOpts, Processor}, 57 | resolve::IncludeResolver, 58 | }; 59 | 60 | fn run_with_writer(opts: &Opts, writer: impl Write) -> Result<()> { 61 | let resolver = IncludeResolver::new( 62 | opts.quote_search_dirs().map(PathBuf::from).collect(), 63 | opts.system_search_dirs().map(PathBuf::from).collect(), 64 | )?; 65 | let filter = InliningFilter::new( 66 | opts.quote_filter_globs().cloned(), 67 | opts.system_filter_globs().cloned(), 68 | )?; 69 | let error_handling_opts = ErrorHandlingOpts { 70 | cyclic_include: opts.cyclic_include_handling(), 71 | unresolvable_quote_include: opts.unresolvable_quote_include_handling(), 72 | unresolvable_system_include: opts.unresolvable_system_include_handling(), 73 | }; 74 | let mut processor = Processor::new( 75 | writer, 76 | resolver, 77 | opts.line_directives, 78 | filter, 79 | error_handling_opts, 80 | ); 81 | opts.files 82 | .iter() 83 | .try_for_each(|source_file| processor.process(source_file)) 84 | } 85 | 86 | fn try_main() -> Result<()> { 87 | let opts = Opts::parse(); 88 | 89 | let mut builder = env_logger::builder(); 90 | if env::var_os("RUST_LOG_VERBOSE").is_some() { 91 | builder.format_timestamp_millis(); 92 | } else { 93 | builder 94 | .format_level(true) 95 | .format_module_path(false) 96 | .format_target(false) 97 | .format_timestamp(None); 98 | } 99 | builder.filter_level(opts.log_level()).init(); 100 | 101 | if let Some(out_file) = &opts.output { 102 | info!("Writing to {:?}", out_file); 103 | let writer = BufWriter::new(File::create(out_file).context("Failed to open output file")?); 104 | run_with_writer(&opts, writer) 105 | } else { 106 | info!("Writing to terminal"); 107 | let stdout = io::stdout(); 108 | run_with_writer(&opts, stdout.lock()) 109 | } 110 | } 111 | 112 | fn main() { 113 | if let Err(error) = try_main() { 114 | error!("{:#}", error); 115 | std::process::exit(1); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/process.rs: -------------------------------------------------------------------------------- 1 | /// Main recursive processing of source files/includes. 2 | use std::{ 3 | collections::{hash_map::Entry, HashMap}, 4 | error, 5 | fmt::{self, Debug, Display, Formatter}, 6 | fs::File, 7 | io::{BufRead, BufReader, Write}, 8 | path::{Path, PathBuf}, 9 | }; 10 | 11 | use anyhow::{Context, Result}; 12 | use log::{debug, info, trace}; 13 | use regex::{CaptureLocations, Regex}; 14 | 15 | use crate::{ 16 | error_handling_handle, filter::InliningFilter, logging::debug_file_name, 17 | resolve::IncludeResolver, ErrorHandling, 18 | }; 19 | 20 | fn static_regex(re: &'static str) -> Regex { 21 | Regex::new(re).expect("invalid hardcoded regex") 22 | } 23 | 24 | const EMPTY_STACK_IDX: usize = usize::MAX; 25 | 26 | #[derive(Debug)] 27 | pub struct CyclicIncludeError { 28 | pub cycle: Vec, 29 | } 30 | 31 | impl Display for CyclicIncludeError { 32 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 33 | writeln!(f, "Cyclic include detected:")?; 34 | for file in &self.cycle { 35 | writeln!(f, "\t{}", file.display())?; 36 | } 37 | Ok(()) 38 | } 39 | } 40 | 41 | impl error::Error for CyclicIncludeError {} 42 | 43 | #[derive(Debug)] 44 | struct FileState { 45 | canonical_path: PathBuf, 46 | included_by: usize, 47 | line_num: usize, 48 | in_stack: bool, 49 | } 50 | 51 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 52 | struct LineRef { 53 | file_idx: usize, 54 | num: usize, 55 | } 56 | 57 | #[derive(Debug)] 58 | pub struct ErrorHandlingOpts { 59 | pub cyclic_include: ErrorHandling, 60 | pub unresolvable_quote_include: ErrorHandling, 61 | pub unresolvable_system_include: ErrorHandling, 62 | } 63 | 64 | #[derive(Debug)] 65 | struct Regexes { 66 | include: Regex, 67 | include_locs: CaptureLocations, 68 | pragma_once: Regex, 69 | } 70 | 71 | impl Regexes { 72 | fn new() -> Self { 73 | let include = static_regex(r#"^\s*#\s*include\s*(["<][^>"]+[">])\s*$"#); 74 | let include_locs = include.capture_locations(); 75 | Self { 76 | include, 77 | include_locs, 78 | pragma_once: static_regex(r"^\s*#\s*pragma\s+once\s*$"), 79 | } 80 | } 81 | } 82 | 83 | #[derive(Debug, PartialEq, Eq)] 84 | enum IncludeHandling { 85 | Inline, 86 | Remove, 87 | Leave, 88 | } 89 | 90 | #[derive(Debug)] 91 | pub struct Processor { 92 | writer: W, 93 | resolver: IncludeResolver, 94 | inlining_filter: InliningFilter, 95 | files: Vec, 96 | known_files: HashMap, 97 | tail_idx: usize, 98 | expected_line: Option, 99 | error_handling_opts: ErrorHandlingOpts, 100 | regexes: Regexes, 101 | } 102 | 103 | impl Processor { 104 | pub fn new( 105 | writer: W, 106 | resolver: IncludeResolver, 107 | line_directives: bool, 108 | inlining_filter: InliningFilter, 109 | error_handling_opts: ErrorHandlingOpts, 110 | ) -> Self { 111 | let expected_line = line_directives.then(|| LineRef { 112 | file_idx: EMPTY_STACK_IDX, 113 | num: 0, 114 | }); 115 | Self { 116 | writer, 117 | resolver, 118 | inlining_filter, 119 | files: Vec::new(), 120 | known_files: HashMap::new(), 121 | tail_idx: EMPTY_STACK_IDX, 122 | expected_line, 123 | error_handling_opts, 124 | regexes: Regexes::new(), 125 | } 126 | } 127 | 128 | fn push_to_stack(&mut self, canonical_path: PathBuf) -> Result { 129 | match self.known_files.entry(canonical_path) { 130 | Entry::Vacant(entry) => { 131 | let idx = self.files.len(); 132 | self.files.push(FileState { 133 | canonical_path: entry.key().clone(), 134 | included_by: self.tail_idx, 135 | line_num: 0, 136 | in_stack: true, 137 | }); 138 | info!("Processing {:?}", debug_file_name(entry.key())); 139 | entry.insert(idx); 140 | self.tail_idx = idx; 141 | Ok(IncludeHandling::Inline) 142 | } 143 | Entry::Occupied(entry) => { 144 | let idx = *entry.get(); 145 | if self.files[idx].in_stack { 146 | assert_ne!( 147 | self.tail_idx, EMPTY_STACK_IDX, 148 | "cannot get include cycles with only one file on the stack" 149 | ); 150 | 151 | let mut cycle = vec![self.files[self.tail_idx].canonical_path.clone()]; 152 | let mut cycle_tail_idx = self.tail_idx; 153 | while cycle_tail_idx != idx { 154 | cycle_tail_idx = self.files[cycle_tail_idx].included_by; 155 | cycle.push(self.files[cycle_tail_idx].canonical_path.clone()); 156 | } 157 | 158 | error_handling_handle!( 159 | self.error_handling_opts.cyclic_include, 160 | "{}", 161 | CyclicIncludeError { cycle } 162 | )?; 163 | Ok(IncludeHandling::Leave) 164 | } else { 165 | debug!( 166 | "Skipping {:?}, already included", 167 | debug_file_name(entry.key()) 168 | ); 169 | Ok(IncludeHandling::Remove) 170 | } 171 | } 172 | } 173 | } 174 | 175 | fn output_copied_line(&mut self, line: &str) -> Result<()> { 176 | if let Some(expected_line) = &mut self.expected_line { 177 | let cur_file = &self.files[self.tail_idx]; 178 | let cur_line = LineRef { 179 | file_idx: self.tail_idx, 180 | num: cur_file.line_num, 181 | }; 182 | if cur_line != *expected_line { 183 | writeln!( 184 | self.writer, 185 | "#line {} \"{}\"", 186 | cur_line.num, 187 | cur_file.canonical_path.display() 188 | )?; 189 | *expected_line = cur_line; 190 | } 191 | expected_line.num += 1; 192 | } 193 | 194 | write!(self.writer, "{}", line)?; 195 | Ok(()) 196 | } 197 | 198 | /// Returns `true` if the include statement should be kept, `false` if it shouldn't. 199 | fn process_include(&mut self, include_ref: &str, current_dir: &Path) -> Result { 200 | assert!( 201 | include_ref.len() >= 3, 202 | "error in hardcoded include regex: include ref too short" 203 | ); 204 | 205 | let maybe_resolved_path = if include_ref.starts_with('"') && include_ref.ends_with('"') { 206 | self.resolver 207 | .resolve_quote(&include_ref[1..(include_ref.len() - 1)], current_dir)? 208 | } else if include_ref.starts_with('<') && include_ref.ends_with('>') { 209 | self.resolver 210 | .resolve_system(&include_ref[1..(include_ref.len() - 1)])? 211 | } else { 212 | debug!("Found weird include-like statement: {}", include_ref); 213 | return Ok(true); 214 | }; 215 | let is_system = include_ref.starts_with('<'); 216 | 217 | if let Some(resolved_path) = maybe_resolved_path { 218 | if self 219 | .inlining_filter 220 | .should_inline(&resolved_path, is_system) 221 | { 222 | return Ok(match self.push_to_stack(resolved_path)? { 223 | IncludeHandling::Inline => { 224 | self.process_recursively()?; 225 | false 226 | } 227 | IncludeHandling::Remove => false, 228 | IncludeHandling::Leave => true, 229 | }); 230 | } 231 | } else { 232 | let handling = if is_system { 233 | self.error_handling_opts.unresolvable_system_include 234 | } else { 235 | self.error_handling_opts.unresolvable_quote_include 236 | }; 237 | error_handling_handle!(handling, "Could not resolve {}", include_ref)?; 238 | } 239 | 240 | Ok(true) 241 | } 242 | 243 | /// Returns `true` when a line was processed, `false` if at eof. 244 | fn process_line( 245 | &mut self, 246 | mut reader: impl BufRead, 247 | line: &mut String, 248 | current_dir: &Path, 249 | ) -> Result { 250 | line.clear(); 251 | let bytes_read = reader.read_line(line).with_context(|| { 252 | format!( 253 | "Failed to read from \"{}\"", 254 | self.files[self.tail_idx].canonical_path.display() 255 | ) 256 | })?; 257 | 258 | if bytes_read == 0 { 259 | return Ok(false); 260 | } 261 | 262 | self.files[self.tail_idx].line_num += 1; 263 | if self.regexes.pragma_once.is_match(line) { 264 | trace!("Skipping pragma once"); 265 | return Ok(true); 266 | } 267 | 268 | let maybe_match = self 269 | .regexes 270 | .include 271 | .captures_read(&mut self.regexes.include_locs, line); 272 | if maybe_match.is_some() { 273 | let (ref_start, ref_end) = self 274 | .regexes 275 | .include_locs 276 | .get(1) 277 | .expect("invalid hardcoded regex: missing capture group"); 278 | if !self.process_include(&line[ref_start..ref_end], current_dir)? { 279 | return Ok(true); 280 | } 281 | } 282 | 283 | self.output_copied_line(line) 284 | .context("Failed writing to output")?; 285 | Ok(true) 286 | } 287 | 288 | fn process_recursively(&mut self) -> Result<()> { 289 | let path = &self.files[self.tail_idx].canonical_path; 290 | let current_dir = path 291 | .parent() 292 | .context("Processed file has no parent directory")? 293 | .to_path_buf(); 294 | 295 | let mut reader = File::open(path) 296 | .with_context(|| format!("Failed to open file \"{}\"", path.display())) 297 | .map(BufReader::new)?; 298 | let mut line = String::new(); 299 | 300 | while self.process_line(&mut reader, &mut line, ¤t_dir)? {} 301 | 302 | self.files[self.tail_idx].in_stack = false; 303 | self.tail_idx = self.files[self.tail_idx].included_by; 304 | 305 | Ok(()) 306 | } 307 | 308 | pub fn process(&mut self, source_file: &Path) -> Result<()> { 309 | info!("Processing source file {:?}", debug_file_name(source_file)); 310 | let canonical_path = source_file.canonicalize().with_context(|| { 311 | format!( 312 | "Failed to canonicalize source file path \"{}\"", 313 | source_file.display() 314 | ) 315 | })?; 316 | 317 | assert_eq!(self.tail_idx, EMPTY_STACK_IDX); 318 | if self.push_to_stack(canonical_path)? == IncludeHandling::Inline { 319 | self.process_recursively()?; 320 | } 321 | assert_eq!(self.tail_idx, EMPTY_STACK_IDX); 322 | 323 | Ok(()) 324 | } 325 | } 326 | -------------------------------------------------------------------------------- /src/resolve.rs: -------------------------------------------------------------------------------- 1 | //! Resolves paths in include statements to the included files. 2 | use std::{ 3 | fmt::{self, Display, Formatter}, 4 | path::{Path, PathBuf}, 5 | }; 6 | 7 | use anyhow::{Context, Result}; 8 | use log::{debug, trace}; 9 | 10 | #[derive(Debug)] 11 | struct IncludePrinter<'a>(&'a str, bool); 12 | 13 | impl Display for IncludePrinter<'_> { 14 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 15 | if self.1 { 16 | write!(f, "\"{}\"", self.0) 17 | } else { 18 | write!(f, "<{}>", self.0) 19 | } 20 | } 21 | } 22 | 23 | #[derive(Debug)] 24 | pub struct IncludeResolver { 25 | quote_search_paths: Vec, 26 | system_search_paths: Vec, 27 | } 28 | 29 | fn resolve( 30 | path: &str, 31 | search_path: &[PathBuf], 32 | current_dir: Option<&Path>, 33 | ) -> Result> { 34 | let printer = IncludePrinter(path, current_dir.is_some()); 35 | let current_dir_canonicalized = current_dir 36 | .map(Path::canonicalize) 37 | .transpose() 38 | .context("failed to canonicalize current directory")?; 39 | 40 | let maybe_resolved = current_dir_canonicalized 41 | .as_deref() 42 | .into_iter() 43 | .chain(search_path.iter().map(PathBuf::as_path)) 44 | .find_map(|include_dir| { 45 | let potential_path = include_dir.join(path); 46 | trace!("Trying to resolve {} to {:?}", printer, potential_path); 47 | 48 | (potential_path.exists() && !potential_path.is_dir()).then(|| { 49 | potential_path.canonicalize().with_context(|| { 50 | format!( 51 | "Failed to canonicalize path to include: \"{}\"", 52 | potential_path.display() 53 | ) 54 | }) 55 | }) 56 | }) 57 | .transpose()?; 58 | 59 | let (left, right) = current_dir.map_or(('<', '>'), |_| ('"', '"')); 60 | if let Some(resolved) = &maybe_resolved { 61 | debug!("Resolved {}{}{} to {:?}", left, path, right, resolved); 62 | } else { 63 | debug!("Failed to resolve {}{}{}", left, path, right); 64 | } 65 | 66 | Ok(maybe_resolved) 67 | } 68 | 69 | impl IncludeResolver { 70 | pub fn new( 71 | mut quote_search_dirs: Vec, 72 | mut system_search_dirs: Vec, 73 | ) -> Result { 74 | for path_vec in [&mut quote_search_dirs, &mut system_search_dirs] { 75 | for path in path_vec { 76 | *path = path.canonicalize().with_context(|| { 77 | format!("Failed to canonicalize search path: \"{}\"", path.display()) 78 | })?; 79 | } 80 | } 81 | 82 | debug!("Quote search dirs: {:#?}", quote_search_dirs); 83 | debug!("System search dirs: {:#?}", system_search_dirs); 84 | Ok(Self { 85 | quote_search_paths: quote_search_dirs, 86 | system_search_paths: system_search_dirs, 87 | }) 88 | } 89 | 90 | /// Tries to find the file referenced in a quote include statement. 91 | /// 92 | /// If found, returns the canonicalized path to the file. 93 | pub fn resolve_quote(&self, path: &str, current_dir: &Path) -> Result> { 94 | resolve(path, &self.quote_search_paths, Some(current_dir)) 95 | } 96 | 97 | /// Tries to find the file referenced in a system include statement. 98 | /// 99 | /// If found, returns the canonicalized path to the file. 100 | pub fn resolve_system(&self, path: &str) -> Result> { 101 | resolve(path, &self.system_search_paths, None) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /tests/integration/filtering.rs: -------------------------------------------------------------------------------- 1 | use crate::util; 2 | 3 | use anyhow::Result; 4 | use indoc::indoc; 5 | 6 | #[test] 7 | fn blacklist_filters() -> Result<()> { 8 | util::builder() 9 | .source_file(indoc! {" 10 | #include 11 | #include 12 | "})? 13 | .search_dir("-d", [("a.hpp", "// a.hpp\n"), ("b.hpp", "// b.hpp\n")])? 14 | .command() 15 | .args(["--filter", "**/b.hpp"]) 16 | .assert() 17 | .success() 18 | .stdout(indoc! {" 19 | // a.hpp 20 | #include 21 | "}); 22 | Ok(()) 23 | } 24 | 25 | #[test] 26 | fn whilelist_filters() -> Result<()> { 27 | util::builder() 28 | .source_file(indoc! {" 29 | #include 30 | #include 31 | "})? 32 | .search_dir("-d", [("a.hpp", "// a.hpp\n"), ("b.hpp", "// b.hpp\n")])? 33 | .command() 34 | .args(["-f", "**", "-f", "!**/b.hpp"]) 35 | .assert() 36 | .success() 37 | .stdout(indoc! {" 38 | #include 39 | // b.hpp 40 | "}); 41 | Ok(()) 42 | } 43 | 44 | #[test] 45 | fn quote_and_system_only_filters() -> Result<()> { 46 | util::builder() 47 | .source_file(indoc! {r#" 48 | #include 49 | #include 50 | #include "a.hpp" 51 | #include "b.hpp" 52 | "#})? 53 | .search_dir("-d", [("a.hpp", "// a.hpp\n"), ("b.hpp", "// b.hpp\n")])? 54 | .command() 55 | .args([ 56 | "-f", 57 | "**", 58 | "--filter-quote", 59 | "!**/b.hpp", 60 | "--filter-system", 61 | "!**/a.hpp", 62 | ]) 63 | .assert() 64 | .success() 65 | .stdout(indoc! {r#" 66 | // a.hpp 67 | #include 68 | #include "a.hpp" 69 | // b.hpp 70 | "#}); 71 | Ok(()) 72 | } 73 | 74 | #[test] 75 | fn filter_precedence() -> Result<()> { 76 | util::builder() 77 | .source_file("#include ")? 78 | .search_dir("-d", [("a/b/c.hpp", "arst")])? 79 | .command() 80 | .args(["-f", "**/a/**", "-f", "!**/a/b/**", "-f", "**/a/b/c.hpp"]) 81 | .assert() 82 | .success() 83 | .stdout("#include "); 84 | Ok(()) 85 | } 86 | -------------------------------------------------------------------------------- /tests/integration/inlining.rs: -------------------------------------------------------------------------------- 1 | use crate::util; 2 | 3 | use std::path::PathBuf; 4 | 5 | use anyhow::Result; 6 | use assert_fs::prelude::*; 7 | use indoc::{formatdoc, indoc}; 8 | use predicates::prelude::*; 9 | 10 | #[test] 11 | fn cyclic_includes() -> Result<()> { 12 | for handling in [None, Some("error"), Some("warn"), Some("ignore")] { 13 | let builder = util::builder() 14 | .source_file("#include ")? 15 | .search_dir( 16 | "-d", 17 | [("a.hpp", "#include "), ("b.hpp", "#include ")], 18 | )?; 19 | let mut command = builder.command(); 20 | if let Some(handling) = handling { 21 | command.args(["--cyclic-include", handling]); 22 | } 23 | 24 | let mut assert = command.assert(); 25 | let handling = handling.unwrap_or("error"); 26 | if handling == "error" { 27 | assert = assert.failure(); 28 | } else { 29 | assert = assert.success().stdout("#include "); 30 | } 31 | if handling != "ignore" { 32 | assert.stderr(predicate::str::is_empty().not()); 33 | } 34 | } 35 | 36 | Ok(()) 37 | } 38 | 39 | #[test] 40 | fn cyclic_include_back_to_source_file() -> Result<()> { 41 | let mut a_path = PathBuf::new(); 42 | util::builder() 43 | .search_dir_setup("-d", |dir| { 44 | dir.child("a.hpp").write_str("#include ")?; 45 | dir.child("b.hpp").write_str("#include ")?; 46 | a_path = dir.child("a.hpp").to_path_buf(); 47 | Ok(()) 48 | })? 49 | .command() 50 | .arg(a_path) 51 | .assert() 52 | .failure(); 53 | Ok(()) 54 | } 55 | 56 | #[test] 57 | fn already_included_source_file() -> Result<()> { 58 | let mut include_path = PathBuf::new(); 59 | util::builder() 60 | .source_file("#include ")? 61 | .search_dir_setup("-d", |dir| { 62 | dir.child("a.hpp").write_str("arst")?; 63 | include_path = dir.child("a.hpp").to_path_buf(); 64 | Ok(()) 65 | })? 66 | .command() 67 | .arg(include_path) 68 | .assert() 69 | .success() 70 | .stdout("arst"); 71 | Ok(()) 72 | } 73 | 74 | #[test] 75 | fn include_headers_at_most_once() -> Result<()> { 76 | util::builder() 77 | .source_file(indoc! {" 78 | #include 79 | #include 80 | "})? 81 | .search_dir("-d", [("a.hpp", "#include "), ("b.hpp", "arst\n")])? 82 | .command() 83 | .assert() 84 | .success() 85 | .stdout("arst\n"); 86 | Ok(()) 87 | } 88 | 89 | #[test] 90 | fn file_identity_considers_symlinks() -> Result<()> { 91 | util::builder() 92 | .source_file(indoc! {" 93 | #include 94 | #include 95 | "})? 96 | .search_dir_setup("-d", |dir| { 97 | dir.child("a.hpp").write_str("arst\n")?; 98 | dir.child("b.hpp").symlink_to_file(dir.child("a.hpp"))?; 99 | Ok(()) 100 | })? 101 | .command() 102 | .assert() 103 | .success() 104 | .stdout("arst\n"); 105 | Ok(()) 106 | } 107 | 108 | #[test] 109 | fn weird_include_statements() -> Result<()> { 110 | util::builder() 111 | .source_file("# \t include \t \t ")? 112 | .search_dir("-d", [("a.hpp", "arst")])? 113 | .command() 114 | .assert() 115 | .success() 116 | .stdout("arst"); 117 | Ok(()) 118 | } 119 | 120 | #[test] 121 | fn line_directives() -> Result<()> { 122 | let builder = util::builder() 123 | .source_file(indoc! {" 124 | arst 125 | #include 126 | 127 | arst 128 | #include 129 | arst 130 | "})? 131 | .search_dir("-d", [("a.hpp", "#include \n"), ("b.hpp", "qwfp\n")])?; 132 | 133 | let src_file = builder.source_files[0].to_path_buf().canonicalize()?; 134 | let dir = &builder.search_dirs[0].1; 135 | let b_hpp = dir.child("b.hpp").to_path_buf().canonicalize()?; 136 | 137 | builder 138 | .command() 139 | .arg("--line-directives") 140 | .assert() 141 | .success() 142 | .stdout(formatdoc! {r#" 143 | #line 1 "{src_file}" 144 | arst 145 | #line 1 "{b_hpp}" 146 | qwfp 147 | #line 3 "{src_file}" 148 | 149 | arst 150 | #line 6 "{src_file}" 151 | arst 152 | "#, 153 | src_file=src_file.display(), 154 | b_hpp=b_hpp.display() 155 | }); 156 | Ok(()) 157 | } 158 | 159 | #[test] 160 | fn pragma_once_removal() -> Result<()> { 161 | util::builder() 162 | .source_file(indoc! {" 163 | #include 164 | #include 165 | "})? 166 | .search_dir( 167 | "-d", 168 | [ 169 | ("a.hpp", "#pragma once\n"), 170 | ("b.hpp", "# \tpragma\t once \t\n"), 171 | ], 172 | )? 173 | .command() 174 | .assert() 175 | .success() 176 | .stdout(""); 177 | Ok(()) 178 | } 179 | -------------------------------------------------------------------------------- /tests/integration/main.rs: -------------------------------------------------------------------------------- 1 | // Testing utilities 2 | mod util; 3 | 4 | // Integration tests 5 | mod filtering; 6 | mod inlining; 7 | mod misc; 8 | mod resolving; 9 | -------------------------------------------------------------------------------- /tests/integration/misc.rs: -------------------------------------------------------------------------------- 1 | use crate::util; 2 | 3 | use anyhow::Result; 4 | use assert_fs::{prelude::*, NamedTempFile}; 5 | 6 | #[test] 7 | fn invoking_help() { 8 | // Running with -h 9 | let short_help_output = util::command() 10 | .arg("-h") 11 | .assert() 12 | .success() 13 | .get_output() 14 | .stdout 15 | .clone(); 16 | 17 | // Running without arguments 18 | util::command().assert().failure().stderr(short_help_output); 19 | 20 | // Running with --help 21 | util::command().arg("--help").assert().success(); 22 | } 23 | 24 | #[test] 25 | fn missing_source_files() { 26 | util::command().arg("-d").arg("/").assert().failure(); 27 | } 28 | 29 | #[test] 30 | fn redirecting_output() -> Result<()> { 31 | let out_file = NamedTempFile::new("out.cpp")?; 32 | util::builder() 33 | .source_file("arst")? 34 | .command() 35 | .arg("-o") 36 | .arg(out_file.path()) 37 | .assert() 38 | .success() 39 | .stdout(""); 40 | out_file.assert("arst"); 41 | Ok(()) 42 | } 43 | 44 | #[test] 45 | fn multiple_source_files() -> Result<()> { 46 | util::builder() 47 | .source_file("a")? 48 | .source_file("b")? 49 | .source_file("c")? 50 | .command() 51 | .assert() 52 | .success() 53 | .stdout("abc"); 54 | Ok(()) 55 | } 56 | -------------------------------------------------------------------------------- /tests/integration/resolving.rs: -------------------------------------------------------------------------------- 1 | use crate::util; 2 | 3 | use anyhow::Result; 4 | use assert_fs::prelude::*; 5 | use indoc::indoc; 6 | use predicates::prelude::*; 7 | 8 | #[test] 9 | fn basic_file_resolving() -> Result<()> { 10 | util::builder() 11 | .source_file(indoc! {r#" 12 | #include "a.hpp" 13 | // hello? 14 | #include 15 | "#})? 16 | .search_dir("-d", [("a.hpp", "// a.hpp\n")])? 17 | .search_dir("--dir", [("b/c.hpp", "// b/c.hpp\n")])? 18 | .command() 19 | .assert() 20 | .success() 21 | .stdout(indoc! {" 22 | // a.hpp 23 | // hello? 24 | // b/c.hpp 25 | "}); 26 | 27 | Ok(()) 28 | } 29 | 30 | #[test] 31 | fn quote_and_system_only_search_dirs() -> Result<()> { 32 | util::builder() 33 | .source_file(indoc! {r#" 34 | #include "a.hpp" 35 | #include "b.hpp" 36 | #include "c.hpp" 37 | #include 38 | #include 39 | #include 40 | "#})? 41 | .search_dir("--dir-quote", [("a.hpp", "// a.hpp quote\n")])? 42 | .search_dir("--dir-system", [("b.hpp", "// b.hpp system\n")])? 43 | .search_dir( 44 | "-d", 45 | [ 46 | ("a.hpp", "// a.hpp shared\n"), 47 | ("b.hpp", "// b.hpp shared\n"), 48 | ("c.hpp", "// c.hpp shared\n"), 49 | ], 50 | )? 51 | .command() 52 | .assert() 53 | .stdout(indoc! {" 54 | // a.hpp quote 55 | // b.hpp shared 56 | // c.hpp shared 57 | // a.hpp shared 58 | // b.hpp system 59 | "}); 60 | Ok(()) 61 | } 62 | 63 | #[test] 64 | fn precedence_of_search_dirs() -> Result<()> { 65 | util::builder() 66 | .source_file("#include ")? 67 | .search_dir("-d", [("a.hpp", "// 1")])? 68 | .search_dir("-d", [("a.hpp", "// 2")])? 69 | .command() 70 | .assert() 71 | .success() 72 | .stdout("// 1"); 73 | 74 | Ok(()) 75 | } 76 | 77 | #[test] 78 | fn resolving_to_file_symlinks() -> Result<()> { 79 | util::builder() 80 | .source_file("#include ")? 81 | .search_dir_setup("-d", |dir| { 82 | let a_path = dir.child("a.hpp"); 83 | a_path.write_str("// a.hpp")?; 84 | dir.child("b.hpp").symlink_to_file(a_path)?; 85 | Ok(()) 86 | })? 87 | .command() 88 | .assert() 89 | .success() 90 | .stdout("// a.hpp"); 91 | 92 | Ok(()) 93 | } 94 | 95 | #[test] 96 | fn directories_are_not_valid_resolves() -> Result<()> { 97 | util::builder() 98 | .source_file("#include ")? 99 | .search_dir_setup("-d", |dir| { 100 | dir.child("a").create_dir_all()?; 101 | Ok(()) 102 | })? 103 | .command() 104 | .assert() 105 | .success() 106 | .stdout("#include "); 107 | Ok(()) 108 | } 109 | 110 | #[test] 111 | fn unresolvable_include_error_options() -> Result<()> { 112 | let handling_options = ["error", "warn", "ignore"]; 113 | for handling in handling_options { 114 | for (left, right) in [('<', '>'), ('"', '"')] { 115 | let input = format!("#include {}a{}", left, right); 116 | let mut assert = util::builder() 117 | .source_file(&input)? 118 | .command() 119 | .args(["--unresolvable-include", handling]) 120 | .assert(); 121 | 122 | if handling == "error" { 123 | assert = assert.failure(); 124 | } else { 125 | assert = assert.success().stdout(input); 126 | } 127 | if handling != "ignore" { 128 | assert.stderr(predicate::str::is_empty().not()); 129 | } 130 | } 131 | } 132 | 133 | for quote_handling in handling_options { 134 | for system_handling in handling_options { 135 | for (left, right, relevant_handling) in 136 | [('<', '>', system_handling), ('"', '"', quote_handling)] 137 | { 138 | let input = format!("#include {}a{}", left, right); 139 | let mut assert = util::builder() 140 | .source_file(&input)? 141 | .command() 142 | .args([ 143 | "--unresolvable-quote-include", 144 | quote_handling, 145 | "--unresolvable-system-include", 146 | system_handling, 147 | ]) 148 | .assert(); 149 | 150 | if relevant_handling == "error" { 151 | assert = assert.failure(); 152 | } else { 153 | assert = assert.success().stdout(input); 154 | } 155 | if relevant_handling != "ignore" { 156 | assert.stderr(predicate::str::is_empty().not()); 157 | } 158 | } 159 | } 160 | } 161 | Ok(()) 162 | } 163 | -------------------------------------------------------------------------------- /tests/integration/util.rs: -------------------------------------------------------------------------------- 1 | // Usages by other integration tests don't seem to be picked up consistently? 2 | #![allow(dead_code)] 3 | use std::path::PathBuf; 4 | 5 | use anyhow::Result; 6 | use assert_cmd::Command; 7 | use assert_fs::{prelude::*, NamedTempFile, TempDir}; 8 | use once_cell::sync::Lazy; 9 | 10 | static BINARY: Lazy = 11 | Lazy::new(|| assert_cmd::cargo::cargo_bin(assert_cmd::crate_name!())); 12 | 13 | pub fn command() -> Command { 14 | Command::new(&*BINARY) 15 | } 16 | 17 | pub fn builder() -> TestSetupBuilder { 18 | TestSetupBuilder { 19 | search_dirs: Vec::new(), 20 | source_files: Vec::new(), 21 | } 22 | } 23 | 24 | pub struct TestSetupBuilder { 25 | pub search_dirs: Vec<(&'static str, TempDir)>, 26 | pub source_files: Vec, 27 | } 28 | 29 | impl TestSetupBuilder { 30 | pub fn source_file(mut self, content: &str) -> Result { 31 | let file = NamedTempFile::new("src.cpp")?; 32 | file.write_str(content)?; 33 | self.source_files.push(file); 34 | Ok(self) 35 | } 36 | 37 | pub fn search_dir_setup( 38 | mut self, 39 | option: &'static str, 40 | setup: impl FnOnce(&mut TempDir) -> Result<()>, 41 | ) -> Result { 42 | let mut temp_dir = TempDir::new()?; 43 | setup(&mut temp_dir)?; 44 | self.search_dirs.push((option, temp_dir)); 45 | Ok(self) 46 | } 47 | 48 | pub fn search_dir<'a>( 49 | self, 50 | option: &'static str, 51 | files: impl IntoIterator, 52 | ) -> Result { 53 | self.search_dir_setup(option, |dir| { 54 | for (path, content) in files { 55 | dir.child(path).write_str(content)?; 56 | } 57 | Ok(()) 58 | }) 59 | } 60 | 61 | pub fn command(&self) -> Command { 62 | let mut cmd = command(); 63 | cmd.args(self.source_files.iter().map(NamedTempFile::path)); 64 | for (option, dir) in &self.search_dirs { 65 | cmd.arg(option).arg(dir.path()); 66 | } 67 | cmd 68 | } 69 | } 70 | --------------------------------------------------------------------------------