pyoxidizerlib/
licensing.rs1use {
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
27pub 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
47pub 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 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 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 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 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 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 let feature_list = relevant_feature_set.packages_with_features(DependencyDirection::Forward);
171
172 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 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}