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}