├── .gitignore ├── .dir-locals.el ├── scripts ├── install.sh ├── clean.sh └── test.sh ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE ├── README.md ├── src ├── main.rs └── lib.rs └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /examples/ 3 | -------------------------------------------------------------------------------- /.dir-locals.el: -------------------------------------------------------------------------------- 1 | ((nil . ((compile-command . "RUST_LOG=debug RUST_BACKTRACE=1 cargo run")))) -------------------------------------------------------------------------------- /scripts/install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | 5 | cargo install --path . 6 | -------------------------------------------------------------------------------- /scripts/clean.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | 5 | 6 | if [[ -n $(grep foo-dynamic Cargo.toml) ]]; then 7 | # poor mans cargo rm 8 | cat Cargo.toml | grep -v foo-dynamic > Cargo.toml.copy 9 | mv Cargo.toml{.copy,} 10 | fi 11 | 12 | if [[ -d foo-dynamic ]]; then 13 | rm -rf foo-dynamic 14 | fi 15 | -------------------------------------------------------------------------------- /scripts/test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | 5 | # A simple test that adds a dynamic dependency to the foo placeholder package 6 | # (https://crates.io/crates/foo) and removes it again 7 | 8 | ./scripts/clean.sh 9 | 10 | ./scripts/install.sh 11 | 12 | # RUST_BACKTRACE=1 RUST_LOG=trace cargo run -- foo 13 | cargo add-dynamic foo -v 14 | 15 | ./scripts/clean.sh 16 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | This package tries hard to adhere to [semver](https://semver.org/). 4 | 5 | ## [0.1.2] 6 | ### Changed 7 | - support for workspaces: 8 | 9 | When `cargo add-dynamic` is invoked, we will try to find a workspace toml file 10 | starting from cwd. We will check if the target package (the package the dylib 11 | should be added to) is a workspace member and if this is the case we will add 12 | the dylib package as a member as well. 13 | 14 | ## [0.1.1] 15 | ### Added 16 | - support for `--rename`, `--lib-dir`, `--verbose` flags 17 | 18 | ## [0.1.0] 19 | ### Added 20 | - `cargo add-dynamic` command 21 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cargo-add-dynamic" 3 | version = "0.1.2" 4 | categories = ["development-tools"] 5 | edition = "2021" 6 | exclude = [".dir-locals.el", "scripts/"] 7 | homepage = "https://github.com/rksm/cargo-add-dynamic" 8 | keywords = ["cargo"] 9 | license = "MIT" 10 | readme = "README.md" 11 | repository = "https://github.com/rksm/cargo-add-dynamic" 12 | description = " Cargo-add command to make dependencies into dylibs." 13 | 14 | [dependencies] 15 | anyhow = "1.0.64" 16 | clap = "3.2.20" 17 | toml_edit = "0.14.4" 18 | tracing = { version = "0.1.36", features = ["release_max_level_debug"] } 19 | tracing-subscriber = { version = "0.3.15", features = ["env-filter"] } 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2022 Robert Krahn 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cargo add-dynamic 2 | 3 | [![Crates.io](https://img.shields.io/crates/v/cargo-add-dynamic)](https://crates.io/crates/cargo-add-dynamic) 4 | 5 | This cargo command allows to wrap dependencies as dylibs. 6 | 7 | For why you might want this see [Speeding up incremental Rust compilation with dylibs](https://robert.kra.hn/posts/2022-09-09-speeding-up-incremental-rust-compilation-with-dylibs/). 8 | 9 | 10 | ## Installation 11 | 12 | ```shell 13 | cargo install cargo-add-dynamic 14 | ``` 15 | 16 | ## Example 17 | 18 | To add a new dependency as a dylib to the current project run for example 19 | 20 | ```shell 21 | cargo add-dynamic polars --features csv-file,lazy,list,describe,rows,fmt,strings,temporal 22 | ``` 23 | 24 | This will create a sub-package `polars-dynamic` with the following content. 25 | 26 | `polars-dynamic/Cargo.toml` 27 | 28 | ```toml 29 | [package] 30 | name = "polars-dynamic" 31 | version = "0.1.0" 32 | edition = "2021" 33 | 34 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 35 | 36 | [dependencies] 37 | polars = { version = "0.23.2", features = ["csv-file", "lazy", "list", "describe", "rows", "fmt", "strings", "temporal"] } 38 | 39 | [lib] 40 | crate-type = ["dylib"] 41 | ``` 42 | 43 | `polars-dynamic/src/lib.rs` 44 | 45 | ```rust 46 | pub use polars::*; 47 | ``` 48 | 49 | And it will add `polars = { version = "0.1.0", path = "polars-dynamic", package = "polars-dynamic" }` to the dependencies of the current package. 50 | 51 | 52 | ## Usage 53 | 54 | ``` 55 | add-dynamic 56 | Cargo command similar to `cargo add` that will add a dependency as a dynamic library (dylib) 57 | crate by creating a new sub-package whose only dependency is the specified and whose 58 | crate-type is ["dylib"]. 59 | 60 | USAGE: 61 | cargo-add-dynamic [OPTIONS] 62 | 63 | ARGS: 64 | 65 | 66 | OPTIONS: 67 | -F, --features ... Space or comma separated list of features to activate 68 | -h, --help Print help information 69 | --lib-dir Directory for the new sub-package. Defaults to -dynamic 70 | -n, --name Name of the dynamic library, defaults to -dynamic 71 | --no-default-features Disable the default features 72 | --offline Run without accessing the network 73 | --optional Mark the dependency as optional. The package name will be 74 | exposed as feature of your crate. 75 | -p, --package Package to modify 76 | --path Filesystem path to local crate to add 77 | --rename Rename the dependency 78 | Example uses: 79 | - Depending on multiple versions of a crate 80 | - Depend on crates with the same name from different registries 81 | -v, --verbose Additional (debug) logging. 82 | ``` 83 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use cargo_add_dynamic::{DylibWrapperPackage, Opts, TargetPackage, Workspace}; 3 | use clap::{App, Arg}; 4 | 5 | fn main() { 6 | let app = App::new("add-dynamic") 7 | .about("Cargo command similar to `cargo add` that will add a dependency as a dynamic library (dylib) crate by creating a new sub-package whose only dependency is the specified and whose crate-type is [\"dylib\"].") 8 | .arg(Arg::new("crate-name") 9 | .value_name("DEP") 10 | .required(true)) 11 | .arg( 12 | Arg::new("optional") 13 | .help("Mark the dependency as optional. The package name will be exposed as feature of your crate.") 14 | .long("optional"), 15 | ) 16 | .arg( 17 | Arg::new("verbose") 18 | .help("Additional (debug) logging.") 19 | .long("verbose") 20 | .short('v'), 21 | ) 22 | .arg(Arg::new("offline") 23 | .help("Run without accessing the network") 24 | .long("offline")) 25 | .arg( 26 | Arg::new("no-default-features") 27 | .help("Disable the default features") 28 | .long("no-default-features"), 29 | ) 30 | .arg( 31 | Arg::new("features") 32 | .help("Space or comma separated list of features to activate") 33 | .long("features") 34 | .short('F') 35 | .multiple_values(true) 36 | .takes_value(true) 37 | .value_name("FEATURES") 38 | .required(false), 39 | ) 40 | .arg( 41 | Arg::new("path") 42 | .help("Filesystem path to local crate to add") 43 | .long("path") 44 | .value_name("PATH") 45 | .required(false), 46 | ) 47 | .arg( 48 | Arg::new("rename") 49 | .help("Rename the dependency\nExample uses:\n- Depending on multiple versions of a crate\n- Depend on crates with the same name from different registries") 50 | .long("rename") 51 | .value_name("NAME") 52 | .required(false), 53 | ) 54 | .arg( 55 | Arg::new("name") 56 | .help("Name of the dynamic library, defaults to -dynamic") 57 | .long("name") 58 | .short('n') 59 | .value_name("NAME") 60 | .required(false), 61 | ) 62 | .arg( 63 | Arg::new("lib-dir") 64 | .help("Directory for the new sub-package. Defaults to -dynamic") 65 | .long("lib-dir") 66 | .value_name("DIR") 67 | .required(false), 68 | ) 69 | .arg( 70 | Arg::new("package") 71 | .help("Package to modify") 72 | .long("package") 73 | .short('p') 74 | .value_name("SPEC") 75 | .required(false), 76 | ); 77 | 78 | let opts = Opts::from_args(app); 79 | 80 | let level = if opts.verbose { "debug" } else { "info" }; 81 | 82 | tracing_subscriber::fmt() 83 | .with_env_filter( 84 | tracing_subscriber::EnvFilter::try_from_default_env() 85 | .unwrap_or_else(|_| tracing_subscriber::EnvFilter::builder().parse_lossy(level)), 86 | ) 87 | .init(); 88 | 89 | run(opts).unwrap(); 90 | } 91 | 92 | fn run(opts: Opts) -> Result<()> { 93 | // optional: update workspace so that it contains the new dylib as a member 94 | let cwd = std::env::current_dir()?; 95 | if let Some(mut workspace) = Workspace::find_starting_in_dir(&cwd, opts.package.as_ref())? { 96 | let lib_dir = if let Some(path_to_cwd) = workspace.relative_path_to_workspace_from(cwd)? { 97 | path_to_cwd.join(&opts.lib_dir) 98 | } else { 99 | opts.lib_dir.clone() 100 | }; 101 | workspace.add_member(lib_dir.to_string_lossy())?; 102 | } 103 | 104 | // create the dylib package 105 | let dylib_package = DylibWrapperPackage::new(opts.clone()); 106 | dylib_package.cargo_new_lib()?; 107 | dylib_package.cargo_add_dependency_to_new_lib()?; 108 | dylib_package.modify_dynamic_lib()?; 109 | 110 | // add the dylib package to the target package 111 | let target_package = TargetPackage::new(opts); 112 | target_package.cargo_add_dynamic_library_to_target_package()?; 113 | 114 | Ok(()) 115 | } 116 | -------------------------------------------------------------------------------- /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 = "ansi_term" 7 | version = "0.12.1" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" 10 | dependencies = [ 11 | "winapi", 12 | ] 13 | 14 | [[package]] 15 | name = "anyhow" 16 | version = "1.0.64" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "b9a8f622bcf6ff3df478e9deba3e03e4e04b300f8e6a139e192c05fa3490afc7" 19 | 20 | [[package]] 21 | name = "atty" 22 | version = "0.2.14" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 25 | dependencies = [ 26 | "hermit-abi", 27 | "libc", 28 | "winapi", 29 | ] 30 | 31 | [[package]] 32 | name = "autocfg" 33 | version = "1.1.0" 34 | source = "registry+https://github.com/rust-lang/crates.io-index" 35 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 36 | 37 | [[package]] 38 | name = "bitflags" 39 | version = "1.3.2" 40 | source = "registry+https://github.com/rust-lang/crates.io-index" 41 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 42 | 43 | [[package]] 44 | name = "bytes" 45 | version = "1.2.1" 46 | source = "registry+https://github.com/rust-lang/crates.io-index" 47 | checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db" 48 | 49 | [[package]] 50 | name = "cargo-add-dynamic" 51 | version = "0.1.2" 52 | dependencies = [ 53 | "anyhow", 54 | "clap", 55 | "toml_edit", 56 | "tracing", 57 | "tracing-subscriber", 58 | ] 59 | 60 | [[package]] 61 | name = "cfg-if" 62 | version = "1.0.0" 63 | source = "registry+https://github.com/rust-lang/crates.io-index" 64 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 65 | 66 | [[package]] 67 | name = "clap" 68 | version = "3.2.20" 69 | source = "registry+https://github.com/rust-lang/crates.io-index" 70 | checksum = "23b71c3ce99b7611011217b366d923f1d0a7e07a92bb2dbf1e84508c673ca3bd" 71 | dependencies = [ 72 | "atty", 73 | "bitflags", 74 | "clap_lex", 75 | "indexmap", 76 | "strsim", 77 | "termcolor", 78 | "textwrap", 79 | ] 80 | 81 | [[package]] 82 | name = "clap_lex" 83 | version = "0.2.4" 84 | source = "registry+https://github.com/rust-lang/crates.io-index" 85 | checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" 86 | dependencies = [ 87 | "os_str_bytes", 88 | ] 89 | 90 | [[package]] 91 | name = "combine" 92 | version = "4.6.6" 93 | source = "registry+https://github.com/rust-lang/crates.io-index" 94 | checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" 95 | dependencies = [ 96 | "bytes", 97 | "memchr", 98 | ] 99 | 100 | [[package]] 101 | name = "either" 102 | version = "1.8.0" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" 105 | 106 | [[package]] 107 | name = "hashbrown" 108 | version = "0.12.3" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" 111 | 112 | [[package]] 113 | name = "hermit-abi" 114 | version = "0.1.19" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 117 | dependencies = [ 118 | "libc", 119 | ] 120 | 121 | [[package]] 122 | name = "indexmap" 123 | version = "1.9.1" 124 | source = "registry+https://github.com/rust-lang/crates.io-index" 125 | checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" 126 | dependencies = [ 127 | "autocfg", 128 | "hashbrown", 129 | ] 130 | 131 | [[package]] 132 | name = "itertools" 133 | version = "0.10.5" 134 | source = "registry+https://github.com/rust-lang/crates.io-index" 135 | checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" 136 | dependencies = [ 137 | "either", 138 | ] 139 | 140 | [[package]] 141 | name = "lazy_static" 142 | version = "1.4.0" 143 | source = "registry+https://github.com/rust-lang/crates.io-index" 144 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 145 | 146 | [[package]] 147 | name = "libc" 148 | version = "0.2.132" 149 | source = "registry+https://github.com/rust-lang/crates.io-index" 150 | checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5" 151 | 152 | [[package]] 153 | name = "log" 154 | version = "0.4.17" 155 | source = "registry+https://github.com/rust-lang/crates.io-index" 156 | checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" 157 | dependencies = [ 158 | "cfg-if", 159 | ] 160 | 161 | [[package]] 162 | name = "matchers" 163 | version = "0.1.0" 164 | source = "registry+https://github.com/rust-lang/crates.io-index" 165 | checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" 166 | dependencies = [ 167 | "regex-automata", 168 | ] 169 | 170 | [[package]] 171 | name = "memchr" 172 | version = "2.5.0" 173 | source = "registry+https://github.com/rust-lang/crates.io-index" 174 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" 175 | 176 | [[package]] 177 | name = "once_cell" 178 | version = "1.14.0" 179 | source = "registry+https://github.com/rust-lang/crates.io-index" 180 | checksum = "2f7254b99e31cad77da24b08ebf628882739a608578bb1bcdfc1f9c21260d7c0" 181 | 182 | [[package]] 183 | name = "os_str_bytes" 184 | version = "6.3.0" 185 | source = "registry+https://github.com/rust-lang/crates.io-index" 186 | checksum = "9ff7415e9ae3fff1225851df9e0d9e4e5479f947619774677a63572e55e80eff" 187 | 188 | [[package]] 189 | name = "pin-project-lite" 190 | version = "0.2.9" 191 | source = "registry+https://github.com/rust-lang/crates.io-index" 192 | checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" 193 | 194 | [[package]] 195 | name = "proc-macro2" 196 | version = "1.0.43" 197 | source = "registry+https://github.com/rust-lang/crates.io-index" 198 | checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab" 199 | dependencies = [ 200 | "unicode-ident", 201 | ] 202 | 203 | [[package]] 204 | name = "quote" 205 | version = "1.0.21" 206 | source = "registry+https://github.com/rust-lang/crates.io-index" 207 | checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" 208 | dependencies = [ 209 | "proc-macro2", 210 | ] 211 | 212 | [[package]] 213 | name = "regex" 214 | version = "1.6.0" 215 | source = "registry+https://github.com/rust-lang/crates.io-index" 216 | checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" 217 | dependencies = [ 218 | "regex-syntax", 219 | ] 220 | 221 | [[package]] 222 | name = "regex-automata" 223 | version = "0.1.10" 224 | source = "registry+https://github.com/rust-lang/crates.io-index" 225 | checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" 226 | dependencies = [ 227 | "regex-syntax", 228 | ] 229 | 230 | [[package]] 231 | name = "regex-syntax" 232 | version = "0.6.27" 233 | source = "registry+https://github.com/rust-lang/crates.io-index" 234 | checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" 235 | 236 | [[package]] 237 | name = "sharded-slab" 238 | version = "0.1.4" 239 | source = "registry+https://github.com/rust-lang/crates.io-index" 240 | checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" 241 | dependencies = [ 242 | "lazy_static", 243 | ] 244 | 245 | [[package]] 246 | name = "smallvec" 247 | version = "1.9.0" 248 | source = "registry+https://github.com/rust-lang/crates.io-index" 249 | checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" 250 | 251 | [[package]] 252 | name = "strsim" 253 | version = "0.10.0" 254 | source = "registry+https://github.com/rust-lang/crates.io-index" 255 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 256 | 257 | [[package]] 258 | name = "syn" 259 | version = "1.0.99" 260 | source = "registry+https://github.com/rust-lang/crates.io-index" 261 | checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13" 262 | dependencies = [ 263 | "proc-macro2", 264 | "quote", 265 | "unicode-ident", 266 | ] 267 | 268 | [[package]] 269 | name = "termcolor" 270 | version = "1.1.3" 271 | source = "registry+https://github.com/rust-lang/crates.io-index" 272 | checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" 273 | dependencies = [ 274 | "winapi-util", 275 | ] 276 | 277 | [[package]] 278 | name = "textwrap" 279 | version = "0.15.0" 280 | source = "registry+https://github.com/rust-lang/crates.io-index" 281 | checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" 282 | 283 | [[package]] 284 | name = "thread_local" 285 | version = "1.1.4" 286 | source = "registry+https://github.com/rust-lang/crates.io-index" 287 | checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" 288 | dependencies = [ 289 | "once_cell", 290 | ] 291 | 292 | [[package]] 293 | name = "toml_edit" 294 | version = "0.14.4" 295 | source = "registry+https://github.com/rust-lang/crates.io-index" 296 | checksum = "5376256e44f2443f8896ac012507c19a012df0fe8758b55246ae51a2279db51f" 297 | dependencies = [ 298 | "combine", 299 | "indexmap", 300 | "itertools", 301 | ] 302 | 303 | [[package]] 304 | name = "tracing" 305 | version = "0.1.36" 306 | source = "registry+https://github.com/rust-lang/crates.io-index" 307 | checksum = "2fce9567bd60a67d08a16488756721ba392f24f29006402881e43b19aac64307" 308 | dependencies = [ 309 | "cfg-if", 310 | "pin-project-lite", 311 | "tracing-attributes", 312 | "tracing-core", 313 | ] 314 | 315 | [[package]] 316 | name = "tracing-attributes" 317 | version = "0.1.22" 318 | source = "registry+https://github.com/rust-lang/crates.io-index" 319 | checksum = "11c75893af559bc8e10716548bdef5cb2b983f8e637db9d0e15126b61b484ee2" 320 | dependencies = [ 321 | "proc-macro2", 322 | "quote", 323 | "syn", 324 | ] 325 | 326 | [[package]] 327 | name = "tracing-core" 328 | version = "0.1.29" 329 | source = "registry+https://github.com/rust-lang/crates.io-index" 330 | checksum = "5aeea4303076558a00714b823f9ad67d58a3bbda1df83d8827d21193156e22f7" 331 | dependencies = [ 332 | "once_cell", 333 | "valuable", 334 | ] 335 | 336 | [[package]] 337 | name = "tracing-log" 338 | version = "0.1.3" 339 | source = "registry+https://github.com/rust-lang/crates.io-index" 340 | checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" 341 | dependencies = [ 342 | "lazy_static", 343 | "log", 344 | "tracing-core", 345 | ] 346 | 347 | [[package]] 348 | name = "tracing-subscriber" 349 | version = "0.3.15" 350 | source = "registry+https://github.com/rust-lang/crates.io-index" 351 | checksum = "60db860322da191b40952ad9affe65ea23e7dd6a5c442c2c42865810c6ab8e6b" 352 | dependencies = [ 353 | "ansi_term", 354 | "matchers", 355 | "once_cell", 356 | "regex", 357 | "sharded-slab", 358 | "smallvec", 359 | "thread_local", 360 | "tracing", 361 | "tracing-core", 362 | "tracing-log", 363 | ] 364 | 365 | [[package]] 366 | name = "unicode-ident" 367 | version = "1.0.3" 368 | source = "registry+https://github.com/rust-lang/crates.io-index" 369 | checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf" 370 | 371 | [[package]] 372 | name = "valuable" 373 | version = "0.1.0" 374 | source = "registry+https://github.com/rust-lang/crates.io-index" 375 | checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" 376 | 377 | [[package]] 378 | name = "winapi" 379 | version = "0.3.9" 380 | source = "registry+https://github.com/rust-lang/crates.io-index" 381 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 382 | dependencies = [ 383 | "winapi-i686-pc-windows-gnu", 384 | "winapi-x86_64-pc-windows-gnu", 385 | ] 386 | 387 | [[package]] 388 | name = "winapi-i686-pc-windows-gnu" 389 | version = "0.4.0" 390 | source = "registry+https://github.com/rust-lang/crates.io-index" 391 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 392 | 393 | [[package]] 394 | name = "winapi-util" 395 | version = "0.1.5" 396 | source = "registry+https://github.com/rust-lang/crates.io-index" 397 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 398 | dependencies = [ 399 | "winapi", 400 | ] 401 | 402 | [[package]] 403 | name = "winapi-x86_64-pc-windows-gnu" 404 | version = "0.4.0" 405 | source = "registry+https://github.com/rust-lang/crates.io-index" 406 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 407 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use std::{ 3 | collections::VecDeque, 4 | fs, 5 | io::Write, 6 | path::{Path, PathBuf}, 7 | }; 8 | 9 | #[derive(Debug, Clone)] 10 | pub struct Opts { 11 | pub verbose: bool, 12 | pub crate_name: String, 13 | pub name: String, 14 | pub lib_dir: PathBuf, 15 | pub optional: bool, 16 | pub offline: bool, 17 | pub no_default_features: bool, 18 | pub features: Option>, 19 | pub path: Option, 20 | pub package: Option, 21 | pub rename: Option, 22 | } 23 | 24 | impl Opts { 25 | pub fn from_args(app: clap::App) -> Self { 26 | // for cargo invocation... how to do this correctly? 27 | let mut args = std::env::args().collect::>(); 28 | if args[1] == "add-dynamic" { 29 | args.remove(1); 30 | } 31 | 32 | let args = app.get_matches_from(args); 33 | 34 | let crate_name = args.value_of("crate-name").expect("crate_name").to_string(); 35 | 36 | let name = args 37 | .value_of("name") 38 | .map(|n| n.to_string()) 39 | .unwrap_or_else(|| format!("{crate_name}-dynamic")); 40 | 41 | let path = args.value_of("path").map(|p| { 42 | let p = PathBuf::from(p); 43 | if p.is_relative() { 44 | std::env::current_dir().expect("cwd").join(p) 45 | } else { 46 | p 47 | } 48 | }); 49 | 50 | let package = args.value_of("package").map(|p| p.to_string()); 51 | 52 | let rename = args.value_of("rename").map(|n| n.to_string()); 53 | 54 | let lib_dir = args 55 | .value_of("lib-dir") 56 | .map(PathBuf::from) 57 | .unwrap_or_else(|| PathBuf::from(&name)); 58 | 59 | let verbose = args.is_present("verbose"); 60 | 61 | let optional = args.is_present("optional"); 62 | 63 | let offline = args.is_present("offline"); 64 | 65 | let no_default_features = args.is_present("no-default-features"); 66 | 67 | let features = args 68 | .values_of("features") 69 | .map(|features| features.map(|ea| ea.to_string()).collect()); 70 | 71 | Self { 72 | verbose, 73 | crate_name, 74 | name, 75 | lib_dir, 76 | path, 77 | optional, 78 | offline, 79 | no_default_features, 80 | package, 81 | features, 82 | rename, 83 | } 84 | } 85 | 86 | fn lib_dir_str(&self) -> &str { 87 | self.lib_dir.to_str().unwrap() 88 | } 89 | } 90 | 91 | #[derive(Debug)] 92 | pub struct Workspace { 93 | target_package_name: String, 94 | target_is_root_package: bool, 95 | cargo_toml_file: PathBuf, 96 | cargo_toml_doc: toml_edit::Document, 97 | } 98 | 99 | impl Workspace { 100 | /// Walks upwards starting from `dir`, trying to find 101 | /// - a target package (that the dylib should be added to) 102 | /// - a cargo workspace Cargo.toml file 103 | /// 104 | /// If no Cargo.toml workspace file is found will return `None`. If one is 105 | /// found but no target package, will return an error. 106 | pub fn find_starting_in_dir( 107 | dir: impl AsRef, 108 | target_package_name: Option>, 109 | ) -> Result> { 110 | tracing::debug!("trying to find workspace"); 111 | 112 | let mut target_package_name = target_package_name.map(|n| n.as_ref().to_string()); 113 | let mut workspace_toml = None; 114 | let mut target_is_root_package = false; 115 | let mut dir = dir.as_ref(); 116 | let mut relative_path_to_current_dir = Vec::new(); 117 | 118 | loop { 119 | let cargo_toml = dir.join("Cargo.toml"); 120 | 121 | if cargo_toml.exists() { 122 | let cargo_content = fs::read_to_string(&cargo_toml)?; 123 | let doc = cargo_content.parse::()?; 124 | let is_workspace = doc 125 | .get("workspace") 126 | .map(|w| w.is_table_like()) 127 | .unwrap_or(false); 128 | 129 | if is_workspace {} 130 | 131 | if target_package_name.is_none() { 132 | if let Some(name) = doc 133 | .get("package") 134 | .and_then(|p| p.get("name")) 135 | .and_then(|n| n.as_str()) 136 | { 137 | tracing::debug!("found target package: {name}"); 138 | target_package_name = Some(name.to_string()); 139 | if is_workspace { 140 | target_is_root_package = true; 141 | } 142 | } 143 | } 144 | 145 | if is_workspace { 146 | tracing::debug!("found workspace toml at {cargo_toml:?}"); 147 | workspace_toml = Some((cargo_toml, doc)); 148 | break; 149 | } 150 | } 151 | 152 | if let Some(parent_dir) = dir.parent() { 153 | if let Some(dir_name) = dir.file_name() { 154 | relative_path_to_current_dir.push(dir_name); 155 | } 156 | dir = parent_dir; 157 | } else { 158 | break; 159 | } 160 | } 161 | 162 | match (workspace_toml, target_package_name) { 163 | (None, _) => Ok(None), 164 | (Some(_), None) => Err(anyhow::anyhow!( 165 | "Found workspace but no target package. Please specify a package with --package." 166 | )), 167 | (Some((cargo_toml_file, cargo_toml_doc)), Some(package)) => { 168 | let workspace = Self { 169 | target_package_name: package, 170 | target_is_root_package, 171 | cargo_toml_doc, 172 | cargo_toml_file, 173 | }; 174 | Ok(if workspace.target_package_is_in_workspace()? { 175 | tracing::debug!( 176 | "target package {} is in workspace {:?}. Is it a root package? {}", 177 | workspace.target_package_name, 178 | workspace.cargo_toml_file, 179 | workspace.target_is_root_package 180 | ); 181 | Some(workspace) 182 | } else { 183 | None 184 | }) 185 | } 186 | } 187 | } 188 | 189 | fn members(&self) -> Vec { 190 | if let Some(members) = self 191 | .cargo_toml_doc 192 | .get("workspace") 193 | .and_then(|el| el.get("members")) 194 | .and_then(|el| el.as_array()) 195 | { 196 | members 197 | .into_iter() 198 | .filter_map(|ea| ea.as_str().map(|s| s.to_string())) 199 | .collect() 200 | } else { 201 | Vec::new() 202 | } 203 | } 204 | 205 | fn target_package_is_in_workspace(&self) -> Result { 206 | if self.target_is_root_package { 207 | return Ok(true); 208 | } 209 | 210 | for member in self.members() { 211 | let base_dir = self.cargo_toml_file.parent().unwrap(); 212 | let member_toml = base_dir.join(&member).join("Cargo.toml"); 213 | if member_toml.exists() { 214 | let toml = fs::read_to_string(member_toml)?; 215 | let doc = toml.parse::()?; 216 | let member_is_target = doc 217 | .get("package") 218 | .and_then(|p| p.get("name")) 219 | .and_then(|n| n.as_str()) 220 | .map(|name| name == self.target_package_name) 221 | .unwrap_or(false); 222 | if member_is_target { 223 | return Ok(true); 224 | } 225 | } 226 | } 227 | 228 | Ok(false) 229 | } 230 | 231 | pub fn relative_path_to_workspace_from( 232 | &self, 233 | dir: impl AsRef, 234 | ) -> Result> { 235 | let dir = dir.as_ref(); 236 | let mut dir = if !dir.is_absolute() { 237 | dir.canonicalize()? 238 | } else { 239 | dir.to_path_buf() 240 | }; 241 | let workspace_dir = self.cargo_toml_file.parent().unwrap(); 242 | let mut relative = VecDeque::new(); 243 | 244 | loop { 245 | if workspace_dir == dir { 246 | break; 247 | } 248 | if let Some(parent) = dir.parent() { 249 | relative.push_front(dir.file_name().map(PathBuf::from).unwrap()); 250 | dir = parent.to_path_buf(); 251 | } else { 252 | return Ok(None); 253 | } 254 | } 255 | 256 | if relative.is_empty() { 257 | return Ok(None); 258 | } 259 | 260 | Ok(Some( 261 | relative 262 | .into_iter() 263 | .fold(PathBuf::from(""), |path, ea| path.join(ea)), 264 | )) 265 | } 266 | 267 | pub fn add_member(&mut self, member: impl AsRef) -> Result<()> { 268 | if let Some(members) = self 269 | .cargo_toml_doc 270 | .get_mut("workspace") 271 | .and_then(|el| el.get_mut("members")) 272 | .and_then(|el| el.as_array_mut()) 273 | { 274 | members.push(member.as_ref()); 275 | } 276 | 277 | fs::write(&self.cargo_toml_file, self.cargo_toml_doc.to_string())?; 278 | 279 | Ok(()) 280 | } 281 | } 282 | 283 | pub struct DylibWrapperPackage { 284 | opts: Opts, 285 | } 286 | 287 | impl DylibWrapperPackage { 288 | pub fn new(opts: Opts) -> Self { 289 | Self { opts } 290 | } 291 | 292 | /// Create a new package for wrapping the libray as a dylib 293 | pub fn cargo_new_lib(&self) -> Result<()> { 294 | let opts = &self.opts; 295 | 296 | let args = vec!["new", "--lib", "--name", &opts.name, opts.lib_dir_str()]; 297 | 298 | tracing::debug!("running cargo {}", args.join(" ")); 299 | 300 | let result = std::process::Command::new("cargo") 301 | .args(args) 302 | .spawn() 303 | .and_then(|mut proc| proc.wait())?; 304 | 305 | if !result.success() { 306 | let code = result.code().unwrap_or(2); 307 | std::process::exit(code); 308 | } 309 | 310 | Ok(()) 311 | } 312 | 313 | /// Add the dependency-to-be-wrapped to the package created by 314 | /// [`cargo_new_lib`]. 315 | pub fn cargo_add_dependency_to_new_lib(&self) -> Result<()> { 316 | let opts = &self.opts; 317 | 318 | let mut args = vec!["add", &opts.crate_name]; 319 | 320 | if opts.offline { 321 | args.push("--offline"); 322 | } 323 | 324 | if let Some(features) = self 325 | .opts 326 | .features 327 | .as_ref() 328 | .map(|features| features.iter().map(|ea| ea.as_str()).collect::>()) 329 | { 330 | args.push("--features"); 331 | args.extend(features); 332 | } 333 | 334 | if opts.no_default_features { 335 | args.push("--no-default-features"); 336 | } 337 | 338 | if let Some(path) = &opts.path { 339 | args.push("--path"); 340 | args.push(path.to_str().expect("path")); 341 | } 342 | 343 | tracing::debug!("running cargo {}", args.join(" ")); 344 | 345 | let result = std::process::Command::new("cargo") 346 | .args(args) 347 | .current_dir(opts.lib_dir_str()) 348 | .spawn() 349 | .and_then(|mut proc| proc.wait())?; 350 | 351 | if !result.success() { 352 | let code = result.code().unwrap_or(2); 353 | std::process::exit(code); 354 | } 355 | Ok(()) 356 | } 357 | 358 | /// Modify the source of the package created by [`cargo_new_lib`] to re-export 359 | /// the original package and make it `crate-type = ["dylib"]`. 360 | pub fn modify_dynamic_lib(&self) -> Result<()> { 361 | let opts = &self.opts; 362 | 363 | let cargo_toml = opts.lib_dir.join("Cargo.toml"); 364 | tracing::debug!("Updating {cargo_toml:?}"); 365 | let mut cargo_toml = fs::OpenOptions::new().append(true).open(cargo_toml)?; 366 | writeln!(cargo_toml, "\n[lib]\ncrate-type = [\"dylib\"]")?; 367 | 368 | let lib_rs = opts.lib_dir.join("src/lib.rs"); 369 | tracing::debug!("Updating {lib_rs:?}"); 370 | let mut lib_rs = fs::OpenOptions::new() 371 | .truncate(true) 372 | .write(true) 373 | .open(lib_rs)?; 374 | let crate_name = opts.crate_name.replace('-', "_"); 375 | writeln!(lib_rs, "pub use {crate_name}::*;")?; 376 | 377 | Ok(()) 378 | } 379 | } 380 | 381 | pub struct TargetPackage { 382 | opts: Opts, 383 | } 384 | 385 | impl TargetPackage { 386 | pub fn new(opts: Opts) -> Self { 387 | Self { opts } 388 | } 389 | 390 | /// Modify the target package that should make use of the original dependency as 391 | /// dylib. 392 | pub fn cargo_add_dynamic_library_to_target_package(&self) -> Result<()> { 393 | let opts = &self.opts; 394 | 395 | let name = opts.rename.as_ref().unwrap_or(&opts.crate_name); 396 | 397 | let mut args = vec![ 398 | "add", 399 | &opts.name, 400 | "--rename", 401 | name, 402 | "--path", 403 | opts.lib_dir_str(), 404 | ]; 405 | 406 | if opts.offline { 407 | args.push("--offline"); 408 | } 409 | 410 | if opts.optional { 411 | args.push("--optional"); 412 | } 413 | 414 | if let Some(package) = &opts.package { 415 | args.push("--package"); 416 | args.push(package); 417 | } 418 | 419 | tracing::debug!("running cargo {}", args.join(" ")); 420 | 421 | let result = std::process::Command::new("cargo") 422 | .args(args) 423 | .spawn() 424 | .and_then(|mut proc| proc.wait())?; 425 | 426 | if !result.success() { 427 | let code = result.code().unwrap_or(2); 428 | std::process::exit(code); 429 | } 430 | Ok(()) 431 | } 432 | } 433 | --------------------------------------------------------------------------------