cli_xtask/config/dist_package.rs
1#[cfg(any(
2 feature = "subcommand-dist-build-license",
3 feature = "subcommand-dist-build-doc"
4))]
5use cargo_metadata::camino::Utf8PathBuf;
6use cargo_metadata::{camino::Utf8Path, Package};
7
8use super::{DistTargetConfig, DistTargetConfigBuilder};
9use crate::{workspace::PackageExt, Result};
10
11/// Configures and constructs [`DistPackageConfig`].
12///
13/// This struct is build from [`DistConfigBuilder`](super::DistConfigBuilder).
14///
15/// # Examples
16///
17/// Creates [`DistConfigBuilder`](super::DistConfigBuilder) and
18/// `DistPackageConfigBuilder` from the workspace root package:
19///
20/// ```rust
21/// # fn main() -> cli_xtask::Result<()> {
22/// use cli_xtask::{config::DistConfigBuilder, workspace};
23///
24/// let workspace = workspace::current();
25///
26/// let (dist_config, pkg_config) = DistConfigBuilder::from_root_package(workspace)?;
27/// # Ok(())
28/// # }
29/// ```
30///
31/// Creates [`DistConfigBuilder`](super::DistConfigBuilder) and
32/// `DistPackageConfigBuilder` from the name of package:
33///
34/// ```rust
35/// # fn main() -> cli_xtask::Result<()> {
36/// use cli_xtask::{config::DistConfigBuilder, workspace};
37///
38/// let workspace = workspace::current();
39/// let package = workspace.workspace_packages()[0];
40///
41/// let (dist_config, pkg_config) = DistConfigBuilder::from_package_name(workspace, &package.name)?;
42/// # Ok(())
43/// # }
44/// ```
45///
46/// Creates `DistPackageConfigBuilder` from the name of package and
47/// [`DistConfigBuilder`](super::DistConfigBuilder):
48///
49/// ```rust
50/// # fn main() -> cli_xtask::Result<()> {
51/// use cli_xtask::{config::DistConfigBuilder, workspace};
52///
53/// let workspace = workspace::current();
54/// let package = workspace.workspace_packages()[0];
55///
56/// let dist_config = DistConfigBuilder::new("app-dist", workspace);
57/// let pkg_config = dist_config.package_by_name(&package.name)?.build()?;
58/// # Ok(())
59/// # }
60/// ```
61#[derive(Debug)]
62pub struct DistPackageConfigBuilder<'a> {
63 name: String,
64 metadata: &'a Package,
65 targets: Option<Vec<DistTargetConfig<'a>>>,
66 #[cfg(feature = "subcommand-dist-build-bin")]
67 cargo_build_options: Vec<String>,
68 #[cfg(feature = "subcommand-dist-build-license")]
69 license_files: Option<Vec<Utf8PathBuf>>,
70 #[cfg(feature = "subcommand-dist-build-doc")]
71 documents: Option<Vec<Utf8PathBuf>>,
72}
73
74impl<'a> DistPackageConfigBuilder<'a> {
75 pub(crate) fn new(package: &'a Package) -> Self {
76 Self {
77 name: package.name.clone(),
78 metadata: package,
79 targets: None,
80 #[cfg(feature = "subcommand-dist-build-bin")]
81 cargo_build_options: vec![],
82 #[cfg(feature = "subcommand-dist-build-license")]
83 license_files: None,
84 #[cfg(feature = "subcommand-dist-build-doc")]
85 documents: None,
86 }
87 }
88
89 /// Creates new `DistTargetConfigBuilder`s from all binary targets of the
90 /// package.
91 ///
92 /// # Errors
93 ///
94 /// Returns an error if the [`DistTargetConfig`] cannot be built.
95 ///
96 /// # Examples
97 ///
98 /// ```rust
99 /// # fn main() -> cli_xtask::Result<()> {
100 /// use cli_xtask::{
101 /// config::{DistConfigBuilder, DistTargetConfigBuilder},
102 /// workspace, Result,
103 /// };
104 ///
105 /// let workspace = workspace::current();
106 ///
107 /// let (dist_config, pkg_config) = DistConfigBuilder::from_package_name(workspace, "xtask")?;
108 /// let target_builders = pkg_config.all_binaries();
109 /// let targets = target_builders
110 /// .into_iter()
111 /// .map(DistTargetConfigBuilder::build)
112 /// .collect::<Result<Vec<_>>>()?;
113 /// let pkg_config = pkg_config.targets(targets).build()?;
114 /// let dist_config = dist_config.package(pkg_config).build()?;
115 ///
116 /// let target = &dist_config.packages()[0].targets()[0];
117 /// assert_eq!(target.name(), "xtask");
118 /// # Ok(())
119 /// # }
120 /// ```
121 pub fn all_binaries(&self) -> Vec<DistTargetConfigBuilder<'a>> {
122 collect_targets(self.metadata, "bin")
123 }
124
125 /// Creates new `DistTargetConfigBuilder`s from given kind of targets in the
126 /// package.
127 ///
128 /// # Errors
129 ///
130 /// Returns an error if the [`DistTargetConfig`] cannot be built.
131 ///
132 /// # Examples
133 ///
134 /// ```rust
135 /// # fn main() -> cli_xtask::Result<()> {
136 /// use cli_xtask::{
137 /// config::{DistConfigBuilder, DistTargetConfigBuilder},
138 /// workspace, Result,
139 /// };
140 ///
141 /// let workspace = workspace::current();
142 ///
143 /// let (dist_config, pkg_config) = DistConfigBuilder::from_package_name(workspace, "cli-xtask")?;
144 /// let target_builders = pkg_config.all_targets("bin");
145 /// let targets = target_builders
146 /// .into_iter()
147 /// .map(DistTargetConfigBuilder::build)
148 /// .collect::<Result<Vec<_>>>()?;
149 /// let pkg_config = pkg_config.targets(targets).build()?;
150 /// let dist_config = dist_config.package(pkg_config).build()?;
151 ///
152 /// let target = &dist_config.packages()[0].targets()[0];
153 /// assert_eq!(target.name(), "cli-xtask");
154 /// # Ok(())
155 /// # }
156 /// ```
157 pub fn all_targets(&self, kind: &str) -> Vec<DistTargetConfigBuilder<'a>> {
158 collect_targets(self.metadata, kind)
159 }
160
161 /// Create a new `DistTargetConfigBuilder` from the name of the binary
162 /// target.
163 ///
164 /// # Errors
165 ///
166 /// Returns an error if the binary target with the given name is not found.
167 ///
168 /// # Examples
169 ///
170 /// ```rust
171 /// # fn main() -> cli_xtask::Result<()> {
172 /// use cli_xtask::{
173 /// clap::CommandFactory,
174 /// config::{DistConfigBuilder, DistTargetConfigBuilder},
175 /// workspace, Result,
176 /// };
177 ///
178 /// let workspace = workspace::current();
179 ///
180 /// let (dist_config, pkg_config) = DistConfigBuilder::from_package_name(workspace, "xtask")?;
181 /// let target_builder = pkg_config.binary_by_name("xtask")?;
182 /// let target = target_builder.build()?;
183 /// let pkg_config = pkg_config.target(target).build()?;
184 /// let dist_config = dist_config.package(pkg_config).build()?;
185 ///
186 /// let target = &dist_config.packages()[0].targets()[0];
187 /// assert_eq!(target.name(), "xtask");
188 /// # Ok(())
189 /// # }
190 /// ```
191 pub fn binary_by_name(&self, name: &str) -> Result<DistTargetConfigBuilder<'a>> {
192 DistTargetConfigBuilder::target_by_name(self.metadata, name, "bin")
193 }
194
195 /// Create a new `DistTargetConfigBuilder` from the name and kind of the
196 /// target.
197 ///
198 /// # Errors
199 ///
200 /// Returns an error if the target with the given name and kind is not
201 /// found.
202 ///
203 /// # Examples
204 ///
205 /// ```rust
206 /// # fn main() -> cli_xtask::Result<()> {
207 /// use cli_xtask::{
208 /// clap::CommandFactory,
209 /// config::{DistConfigBuilder, DistTargetConfigBuilder},
210 /// workspace, Result,
211 /// };
212 ///
213 /// let workspace = workspace::current();
214 ///
215 /// let (dist_config, pkg_config) = DistConfigBuilder::from_package_name(workspace, "cli-xtask")?;
216 /// let target_builder = pkg_config.target_by_name("cli-xtask", "bin")?;
217 /// let target = target_builder.build()?;
218 /// let pkg_config = pkg_config.target(target).build()?;
219 /// let dist_config = dist_config.package(pkg_config).build()?;
220 ///
221 /// let target = &dist_config.packages()[0].targets()[0];
222 /// assert_eq!(target.name(), "cli-xtask");
223 /// # Ok(())
224 /// # }
225 /// ```
226 pub fn target_by_name(&self, name: &str, kind: &str) -> Result<DistTargetConfigBuilder<'a>> {
227 DistTargetConfigBuilder::target_by_name(self.metadata, name, kind)
228 }
229
230 /// Add a target of the package to the list of targets to be distributed.
231 ///
232 /// # Examples
233 ///
234 /// ```rust
235 /// # fn main() -> cli_xtask::Result<()> {
236 /// use cli_xtask::{
237 /// clap::CommandFactory,
238 /// config::{DistConfigBuilder, DistTargetConfigBuilder},
239 /// workspace, Result,
240 /// };
241 ///
242 /// let workspace = workspace::current();
243 ///
244 /// let (dist_config, pkg_config) = DistConfigBuilder::from_package_name(workspace, "cli-xtask")?;
245 /// let target_builder = pkg_config.target_by_name("cli-xtask", "bin")?;
246 /// let target = target_builder.build()?;
247 /// let pkg_config = pkg_config.target(target).build()?;
248 /// let dist_config = dist_config.package(pkg_config).build()?;
249 ///
250 /// let target = &dist_config.packages()[0].targets()[0];
251 /// assert_eq!(target.name(), "cli-xtask");
252 /// # Ok(())
253 /// # }
254 /// ```
255 pub fn target(mut self, target: DistTargetConfig<'a>) -> Self {
256 self.targets.get_or_insert(vec![]).push(target);
257 self
258 }
259
260 /// Add a targets of the package to the list of targets to be distributed.
261 ///
262 /// # Examples
263 ///
264 /// ```rust
265 /// # fn main() -> cli_xtask::Result<()> {
266 /// use cli_xtask::{
267 /// config::{DistConfigBuilder, DistTargetConfigBuilder},
268 /// workspace, Result,
269 /// };
270 ///
271 /// let workspace = workspace::current();
272 ///
273 /// let (dist_config, pkg_config) = DistConfigBuilder::from_package_name(workspace, "cli-xtask")?;
274 /// let target_builders = pkg_config.all_targets("bin");
275 /// let targets = target_builders
276 /// .into_iter()
277 /// .map(DistTargetConfigBuilder::build)
278 /// .collect::<Result<Vec<_>>>()?;
279 /// let pkg_config = pkg_config.targets(targets).build()?;
280 /// let dist_config = dist_config.package(pkg_config).build()?;
281 ///
282 /// let target = &dist_config.packages()[0].targets()[0];
283 /// assert_eq!(target.name(), "cli-xtask");
284 /// # Ok(())
285 /// # }
286 /// ```
287 pub fn targets(mut self, targets: impl IntoIterator<Item = DistTargetConfig<'a>>) -> Self {
288 self.targets.get_or_insert(vec![]).extend(targets);
289 self
290 }
291
292 /// Adds cargo build options to be used when building the package.
293 ///
294 /// # Examples
295 ///
296 /// ```rust
297 /// # fn main() -> cli_xtask::Result<()> {
298 /// use cli_xtask::{config::DistConfigBuilder, workspace};
299 ///
300 /// let workspace = workspace::current();
301 ///
302 /// let (dist_config, pkg_config) = DistConfigBuilder::from_root_package(workspace)?;
303 /// let pkg_config = pkg_config.cargo_build_options(["--features", "feature-a"]);
304 /// # Ok(())
305 /// # }
306 /// ```
307 #[cfg(feature = "subcommand-dist-build-bin")]
308 #[cfg_attr(docsrs, doc(cfg(feature = "subcommand-dist-build-bin")))]
309 pub fn cargo_build_options(
310 mut self,
311 options: impl IntoIterator<Item = impl Into<String>>,
312 ) -> Self {
313 self.cargo_build_options
314 .extend(options.into_iter().map(Into::into));
315 self
316 }
317
318 /// Adds a package license files to the list of files to be distributed.
319 ///
320 /// If the given path is a relative path, it is resolved against the package
321 /// root direcotry.
322 ///
323 /// # Examples
324 ///
325 /// ```rust
326 /// # fn main() -> cli_xtask::Result<()> {
327 /// use cli_xtask::{config::DistConfigBuilder, workspace};
328 ///
329 /// let workspace = workspace::current();
330 ///
331 /// let (dist_config, pkg_config) = DistConfigBuilder::from_root_package(workspace)?;
332 /// let pkg_config = pkg_config.license_files(
333 /// ["LICENSE-MIT", "LICENSE-APACHE"]
334 /// .into_iter()
335 /// .map(Into::into),
336 /// );
337 /// # Ok(())
338 /// # }
339 /// ```
340 #[cfg(feature = "subcommand-dist-build-license")]
341 #[cfg_attr(docsrs, doc(cfg(feature = "subcommand-dist-build-license")))]
342 pub fn license_files(mut self, files: impl IntoIterator<Item = Utf8PathBuf>) -> Self {
343 let package_root = self.metadata.root_directory();
344 let files = files.into_iter().map(|file| {
345 if file.is_relative() {
346 package_root.join(file)
347 } else {
348 file
349 }
350 });
351 match &mut self.license_files {
352 Some(fs) => fs.extend(files),
353 lf @ None => *lf = Some(files.collect()),
354 }
355 self
356 }
357
358 /// Adds a package documentation files to the list of files to be
359 /// distributed.
360 ///
361 /// If the given path is a relative path, it is resolved against the package
362 /// root direcotry.
363 ///
364 /// # Examples
365 ///
366 /// ```rust
367 /// # fn main() -> cli_xtask::Result<()> {
368 /// use cli_xtask::{config::DistConfigBuilder, workspace};
369 ///
370 /// let workspace = workspace::current();
371 ///
372 /// let (dist_config, pkg_config) = DistConfigBuilder::from_root_package(workspace)?;
373 /// let pkg_config = pkg_config.documents(["CHANGELOG.md"].into_iter().map(Into::into));
374 /// # Ok(())
375 /// # }
376 /// ```
377 #[cfg(feature = "subcommand-dist-build-doc")]
378 #[cfg_attr(docsrs, doc(cfg(feature = "subcommand-dist-build-doc")))]
379 pub fn documents(mut self, files: impl IntoIterator<Item = Utf8PathBuf>) -> Self {
380 let package_root = self.metadata.root_directory();
381 let files = files.into_iter().map(|file| {
382 if file.is_relative() {
383 package_root.join(file)
384 } else {
385 file
386 }
387 });
388 match &mut self.documents {
389 Some(ds) => ds.extend(files),
390 ds @ None => *ds = Some(files.collect()),
391 }
392 self
393 }
394
395 /// Builds a [`DistPackageConfig`] from the current configuration.
396 ///
397 /// # Errors
398 ///
399 /// Returns an error if the [`DistPackageConfig`] cannot be built.
400 pub fn build(self) -> Result<DistPackageConfig<'a>> {
401 let targets = match self.targets {
402 Some(targets) => targets,
403 None => collect_targets(self.metadata, "bin")
404 .into_iter()
405 .map(DistTargetConfigBuilder::build)
406 .collect::<Result<Vec<_>>>()?,
407 };
408 Ok(DistPackageConfig {
409 name: self.name,
410 metadata: self.metadata,
411 targets,
412 #[cfg(feature = "subcommand-dist-build-bin")]
413 cargo_build_options: self.cargo_build_options,
414 #[cfg(feature = "subcommand-dist-build-license")]
415 license_files: collect_license_files(self.metadata, self.license_files)?,
416 #[cfg(feature = "subcommand-dist-build-doc")]
417 documents: self.documents.unwrap_or_default(),
418 })
419 }
420}
421
422/// Configuration for the distribution of the package.
423#[derive(Debug)]
424pub struct DistPackageConfig<'a> {
425 name: String,
426 metadata: &'a Package,
427 targets: Vec<DistTargetConfig<'a>>,
428 #[cfg(feature = "subcommand-dist-build-bin")]
429 cargo_build_options: Vec<String>,
430 #[cfg(feature = "subcommand-dist-build-license")]
431 license_files: Vec<Utf8PathBuf>,
432 #[cfg(feature = "subcommand-dist-build-doc")]
433 documents: Vec<Utf8PathBuf>,
434}
435
436impl<'a> DistPackageConfig<'a> {
437 /// Returns the name of the package.
438 pub fn name(&self) -> &str {
439 &self.name
440 }
441
442 /// Returns the metadata of the package.
443 pub fn metadata(&self) -> &'a Package {
444 self.metadata
445 }
446
447 /// Returns the list of targets to be distributed.
448 ///
449 /// Targets can be added by associated functions of
450 /// [`DistPackageConfigBuilder`].
451 ///
452 /// If no targets are added, the list of targets is constructed from all
453 /// binaries target of the package.
454 pub fn targets(&self) -> &[DistTargetConfig] {
455 &self.targets
456 }
457
458 /// Returns the path to the package's root directory.
459 pub fn root_directory(&self) -> &Utf8Path {
460 self.metadata.root_directory()
461 }
462
463 /// Returns the list of cargo build options to be used when building the
464 #[cfg(feature = "subcommand-dist-build-bin")]
465 #[cfg_attr(docsrs, doc(cfg(feature = "subcommand-dist-build-bin")))]
466 pub fn cargo_build_options(&self) -> &[String] {
467 &self.cargo_build_options
468 }
469
470 /// Returns the list of license files to be distributed.
471 ///
472 /// License files can be added by
473 /// [`DistPackageConfigBuilder::license_files`] function.
474 ///
475 /// If no license files are added, the list of license files is constructed
476 /// from the `license-file` field of the `[package]` section of the
477 /// manifest.
478 ///
479 /// If no license files are added and the `license-file` field is not
480 /// present, the file matches the pattern `/^LICENSE(?:-|_|\.|$)/i` in the
481 /// root directory of the package.
482 #[cfg(feature = "subcommand-dist-build-license")]
483 #[cfg_attr(docsrs, doc(cfg(feature = "subcommand-dist-build-license")))]
484 pub fn license_files(&self) -> &[Utf8PathBuf] {
485 &self.license_files
486 }
487
488 /// Returns the list of documentation files to be distributed.
489 ///
490 /// Documentation files can be added by
491 /// [`DistPackageConfigBuilder::documents`] function.
492 /// If no documentation files are added, this function returns empty list.
493 #[cfg(feature = "subcommand-dist-build-doc")]
494 #[cfg_attr(docsrs, doc(cfg(feature = "subcommand-dist-build-doc")))]
495 pub fn documents(&self) -> &[Utf8PathBuf] {
496 &self.documents
497 }
498}
499
500fn collect_targets<'a>(package: &'a Package, kind: &str) -> Vec<DistTargetConfigBuilder<'a>> {
501 package
502 .targets
503 .iter()
504 .filter(|target| target.kind.iter().any(|x| x == &kind.into()))
505 .map(DistTargetConfigBuilder::from_metadata)
506 .collect()
507}
508
509#[cfg(feature = "subcommand-dist-build-license")]
510fn collect_license_files(
511 package: &Package,
512 files: Option<Vec<Utf8PathBuf>>,
513) -> Result<Vec<Utf8PathBuf>> {
514 use once_cell::sync::Lazy;
515 use regex::{Regex, RegexBuilder};
516 let src_dir = package.root_directory();
517
518 if let Some(files) = files {
519 return Ok(files);
520 }
521
522 if let Some(license_file) = &package.license_file {
523 return Ok(vec![src_dir.join(license_file)]);
524 }
525
526 let mut files = vec![];
527 for src_entry in src_dir.read_dir_utf8()? {
528 let src_entry = src_entry?;
529 if !src_entry.file_type()?.is_file() {
530 continue;
531 }
532
533 let src_file = src_entry.path();
534 static RE: Lazy<Regex> = Lazy::new(|| {
535 RegexBuilder::new(r"^LICENSE(?:-|_|\.|$)")
536 .case_insensitive(true)
537 .build()
538 .unwrap()
539 });
540
541 let src_name = match src_file.file_name() {
542 Some(name) => name,
543 None => continue,
544 };
545 if !RE.is_match(src_name) {
546 continue;
547 }
548 files.push(src_file.to_owned());
549 }
550
551 Ok(files)
552}