pyoxidizerlib/
licensing.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! Licensing functionality.
6
7use {
8    crate::environment::{canonicalize_path, RustEnvironment},
9    anyhow::{anyhow, Context, Result},
10    cargo_toml::Manifest,
11    guppy::{
12        graph::{
13            cargo::{CargoOptions, CargoResolverVersion, CargoSet},
14            feature::{named_feature_filter, StandardFeatures},
15            DependencyDirection,
16        },
17        platform::{Platform, PlatformSpec, TargetFeatures, Triple},
18        MetadataCommand,
19    },
20    log::{info, warn},
21    python_packaging::licensing::{
22        ComponentFlavor, LicenseFlavor, LicensedComponent, LicensedComponents, SourceLocation,
23    },
24    std::{path::Path, sync::Arc},
25};
26
27/// Log a summary of licensing info.
28pub fn log_licensing_info(components: &LicensedComponents) {
29    for line in components.license_summary().lines() {
30        warn!("{}", line);
31    }
32    warn!("");
33
34    if let Some(report) = components.interesting_report() {
35        for line in report.lines() {
36            warn!("{}", line);
37        }
38        warn!("");
39    }
40
41    for line in components.spdx_license_breakdown().lines() {
42        info!("{}", line);
43    }
44    info!("");
45}
46
47/// Resolve licenses from a cargo manifest.
48pub fn licenses_from_cargo_manifest<'a>(
49    manifest_path: impl AsRef<Path>,
50    all_features: bool,
51    features: impl IntoIterator<Item = &'a str>,
52    target_triple: Option<impl Into<String>>,
53    rust_environment: &RustEnvironment,
54    include_main_package: bool,
55) -> Result<LicensedComponents> {
56    let manifest_path = canonicalize_path(manifest_path.as_ref())?;
57    let features = features.into_iter().collect::<Vec<&str>>();
58
59    let manifest_dir = manifest_path
60        .parent()
61        .ok_or_else(|| anyhow!("could not determine parent director of manifest"))?;
62
63    if all_features {
64        warn!(
65            "evaluating dependencies for {} using all features",
66            manifest_path.display()
67        );
68    } else {
69        warn!(
70            "evaluating dependencies for {} using features: {}",
71            manifest_path.display(),
72            features.join(", ")
73        );
74    }
75
76    let manifest = Manifest::from_path(&manifest_path)?;
77    let main_package = manifest
78        .package
79        .ok_or_else(|| anyhow!("could not find a package in Cargo manifest"))?
80        .name;
81
82    let mut command = MetadataCommand::new();
83
84    command.cargo_path(&rust_environment.cargo_exe);
85
86    command.current_dir(manifest_dir);
87
88    // We need to set RUSTC so things work with our managed Rust toolchain. But
89    // guppy doesn't have an API for that. So reinvent this wheel.
90    let mut command = command.cargo_command();
91
92    command.env("RUSTC", &rust_environment.rustc_exe);
93
94    let output = command.output().context("invoking cargo metadata")?;
95    if !output.status.success() {
96        return Err(anyhow!(
97            "error running cargo: {}",
98            String::from_utf8_lossy(&output.stderr)
99        ));
100    }
101
102    let stdout = String::from_utf8(output.stdout).context("converting output to UTF-8")?;
103
104    let json = stdout
105        .lines()
106        .find(|line| line.starts_with('{'))
107        .ok_or_else(|| anyhow!("could not find JSON output"))?;
108
109    let metadata = guppy::CargoMetadata::parse_json(json)?;
110
111    let package_graph = metadata.build_graph()?;
112
113    let main_package_id = package_graph
114        .packages()
115        .find(|p| p.name() == main_package)
116        .ok_or_else(|| anyhow!("could not find package {} in metadata", main_package))?
117        .id();
118
119    let workspace_package_set = package_graph.resolve_workspace();
120    let main_package_set = package_graph.query_forward([main_package_id])?.resolve();
121
122    // Simulate a cargo build from the current platform targeting a specified platform or the current.
123    let mut cargo_options = CargoOptions::new();
124    cargo_options.set_resolver(CargoResolverVersion::V2);
125    cargo_options.set_host_platform(PlatformSpec::Platform(Arc::new(Platform::current()?)));
126    cargo_options.set_target_platform(if let Some(triple) = target_triple {
127        PlatformSpec::Platform(Arc::new(Platform::from_triple(
128            Triple::new(triple.into())?,
129            TargetFeatures::Unknown,
130        )))
131    } else {
132        PlatformSpec::current()?
133    });
134
135    // Apply our desired features settings.
136    let initials = workspace_package_set.to_feature_set(named_feature_filter(
137        if all_features {
138            StandardFeatures::All
139        } else {
140            StandardFeatures::Default
141        },
142        features,
143    ));
144
145    // This is always empty because we don't use the functionality.
146    let features_only = package_graph
147        .resolve_none()
148        .to_feature_set(StandardFeatures::All);
149
150    let cargo_set = CargoSet::new(initials, features_only, &cargo_options)?;
151
152    // The meaningful packages for licensing are those that are built for the target
153    // unioned with proc macro crates for the host. It is important we capture the host
154    // proc macro crates because those can generate code that end up in the final binary.
155    let target_features = cargo_set.target_features();
156
157    let proc_macro_feature_set = package_graph
158        .resolve_ids(cargo_set.proc_macro_links().map(|link| link.to().id()))?
159        .to_feature_set(StandardFeatures::All);
160    let proc_macro_host_feature_set = cargo_set
161        .host_features()
162        .intersection(&proc_macro_feature_set);
163
164    let relevant_feature_set = target_features.union(&proc_macro_host_feature_set);
165
166    // Turn it into packages.
167    //
168    // Note: this has packages for the entire workspace. We still need to intersect
169    // with the packages set relevant to the main package!
170    let feature_list = relevant_feature_set.packages_with_features(DependencyDirection::Forward);
171
172    // Now turn the packages into licensing metadata.
173    let mut components = LicensedComponents::default();
174
175    for feature_list in feature_list {
176        let package = feature_list.package();
177
178        if !main_package_set.contains(package.id())? {
179            continue;
180        }
181
182        if package.id() == main_package_id && !include_main_package {
183            continue;
184        }
185
186        let flavor = ComponentFlavor::RustCrate(package.name().into());
187
188        let mut component = if let Some(expression) = package.license() {
189            // `/` is sometimes used as a delimiter for some reason.
190            let expression = expression.replace('/', " OR ");
191
192            LicensedComponent::new_spdx(flavor, &expression)?
193        } else {
194            LicensedComponent::new(flavor, LicenseFlavor::None)
195        };
196
197        for author in package.authors() {
198            component.add_author(author);
199        }
200        if let Some(value) = package.homepage() {
201            component.set_homepage(value);
202        }
203        if let Some(value) = package.repository() {
204            component.set_source_location(SourceLocation::Url(value.to_string()));
205        }
206
207        components.add_component(component);
208    }
209
210    Ok(components)
211}