cargo_add_dynamic/
lib.rs

1use anyhow::Result;
2use std::{
3    collections::VecDeque,
4    fs,
5    io::Write,
6    path::{Path, PathBuf},
7};
8
9#[derive(Debug, Clone)]
10pub 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<Vec<String>>,
19    pub path: Option<PathBuf>,
20    pub package: Option<String>,
21    pub rename: Option<String>,
22}
23
24impl 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::<Vec<_>>();
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)]
92pub 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
99impl 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<Path>,
108        target_package_name: Option<impl AsRef<str>>,
109    ) -> Result<Option<Self>> {
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::<toml_edit::Document>()?;
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<String> {
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<bool> {
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::<toml_edit::Document>()?;
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<Path>,
234    ) -> Result<Option<PathBuf>> {
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<str>) -> 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
283pub struct DylibWrapperPackage {
284    opts: Opts,
285}
286
287impl 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::<Vec<_>>())
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
381pub struct TargetPackage {
382    opts: Opts,
383}
384
385impl 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}