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 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 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 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 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 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 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}