|
| 1 | +//! Handles build script specific information |
| 2 | +
|
| 3 | +use std::{ |
| 4 | + ffi::OsStr, |
| 5 | + io::BufReader, |
| 6 | + path::{Path, PathBuf}, |
| 7 | + process::{Command, Stdio}, |
| 8 | +}; |
| 9 | + |
| 10 | +use anyhow::Result; |
| 11 | +use cargo_metadata::{BuildScript, Message, Package, PackageId}; |
| 12 | +use itertools::Itertools; |
| 13 | +use paths::AbsPathBuf; |
| 14 | +use rustc_hash::FxHashMap; |
| 15 | +use stdx::JodChild; |
| 16 | + |
| 17 | +use crate::{cfg_flag::CfgFlag, CargoConfig}; |
| 18 | + |
| 19 | +#[derive(Debug, Clone, Default)] |
| 20 | +pub(crate) struct BuildDataMap { |
| 21 | + data: FxHashMap<PackageId, BuildData>, |
| 22 | +} |
| 23 | +#[derive(Debug, Clone, Default, PartialEq, Eq)] |
| 24 | +pub struct BuildData { |
| 25 | + /// List of config flags defined by this package's build script |
| 26 | + pub cfgs: Vec<CfgFlag>, |
| 27 | + /// List of cargo-related environment variables with their value |
| 28 | + /// |
| 29 | + /// If the package has a build script which defines environment variables, |
| 30 | + /// they can also be found here. |
| 31 | + pub envs: Vec<(String, String)>, |
| 32 | + /// Directory where a build script might place its output |
| 33 | + pub out_dir: Option<AbsPathBuf>, |
| 34 | + /// Path to the proc-macro library file if this package exposes proc-macros |
| 35 | + pub proc_macro_dylib_path: Option<AbsPathBuf>, |
| 36 | +} |
| 37 | + |
| 38 | +impl BuildDataMap { |
| 39 | + pub(crate) fn new( |
| 40 | + cargo_toml: &Path, |
| 41 | + cargo_features: &CargoConfig, |
| 42 | + packages: &Vec<Package>, |
| 43 | + progress: &dyn Fn(String), |
| 44 | + ) -> Result<BuildDataMap> { |
| 45 | + let mut cmd = Command::new(toolchain::cargo()); |
| 46 | + cmd.args(&["check", "--workspace", "--message-format=json", "--manifest-path"]) |
| 47 | + .arg(cargo_toml); |
| 48 | + |
| 49 | + // --all-targets includes tests, benches and examples in addition to the |
| 50 | + // default lib and bins. This is an independent concept from the --targets |
| 51 | + // flag below. |
| 52 | + cmd.arg("--all-targets"); |
| 53 | + |
| 54 | + if let Some(target) = &cargo_features.target { |
| 55 | + cmd.args(&["--target", target]); |
| 56 | + } |
| 57 | + |
| 58 | + if cargo_features.all_features { |
| 59 | + cmd.arg("--all-features"); |
| 60 | + } else { |
| 61 | + if cargo_features.no_default_features { |
| 62 | + // FIXME: `NoDefaultFeatures` is mutual exclusive with `SomeFeatures` |
| 63 | + // https://github.com/oli-obk/cargo_metadata/issues/79 |
| 64 | + cmd.arg("--no-default-features"); |
| 65 | + } |
| 66 | + if !cargo_features.features.is_empty() { |
| 67 | + cmd.arg("--features"); |
| 68 | + cmd.arg(cargo_features.features.join(" ")); |
| 69 | + } |
| 70 | + } |
| 71 | + |
| 72 | + cmd.stdout(Stdio::piped()).stderr(Stdio::null()).stdin(Stdio::null()); |
| 73 | + |
| 74 | + let mut child = cmd.spawn().map(JodChild)?; |
| 75 | + let child_stdout = child.stdout.take().unwrap(); |
| 76 | + let stdout = BufReader::new(child_stdout); |
| 77 | + |
| 78 | + let mut res = BuildDataMap::default(); |
| 79 | + for message in cargo_metadata::Message::parse_stream(stdout) { |
| 80 | + if let Ok(message) = message { |
| 81 | + match message { |
| 82 | + Message::BuildScriptExecuted(BuildScript { |
| 83 | + package_id, |
| 84 | + out_dir, |
| 85 | + cfgs, |
| 86 | + env, |
| 87 | + .. |
| 88 | + }) => { |
| 89 | + let cfgs = { |
| 90 | + let mut acc = Vec::new(); |
| 91 | + for cfg in cfgs { |
| 92 | + match cfg.parse::<CfgFlag>() { |
| 93 | + Ok(it) => acc.push(it), |
| 94 | + Err(err) => { |
| 95 | + anyhow::bail!("invalid cfg from cargo-metadata: {}", err) |
| 96 | + } |
| 97 | + }; |
| 98 | + } |
| 99 | + acc |
| 100 | + }; |
| 101 | + let res = res.data.entry(package_id.clone()).or_default(); |
| 102 | + // cargo_metadata crate returns default (empty) path for |
| 103 | + // older cargos, which is not absolute, so work around that. |
| 104 | + if out_dir != PathBuf::default() { |
| 105 | + let out_dir = AbsPathBuf::assert(out_dir); |
| 106 | + res.out_dir = Some(out_dir); |
| 107 | + res.cfgs = cfgs; |
| 108 | + } |
| 109 | + |
| 110 | + res.envs = env; |
| 111 | + } |
| 112 | + Message::CompilerArtifact(message) => { |
| 113 | + progress(format!("metadata {}", message.target.name)); |
| 114 | + |
| 115 | + if message.target.kind.contains(&"proc-macro".to_string()) { |
| 116 | + let package_id = message.package_id; |
| 117 | + // Skip rmeta file |
| 118 | + if let Some(filename) = |
| 119 | + message.filenames.iter().find(|name| is_dylib(name)) |
| 120 | + { |
| 121 | + let filename = AbsPathBuf::assert(filename.clone()); |
| 122 | + let res = res.data.entry(package_id.clone()).or_default(); |
| 123 | + res.proc_macro_dylib_path = Some(filename); |
| 124 | + } |
| 125 | + } |
| 126 | + } |
| 127 | + Message::CompilerMessage(message) => { |
| 128 | + progress(message.target.name.clone()); |
| 129 | + } |
| 130 | + Message::Unknown => (), |
| 131 | + Message::BuildFinished(_) => {} |
| 132 | + Message::TextLine(_) => {} |
| 133 | + } |
| 134 | + } |
| 135 | + } |
| 136 | + res.inject_cargo_env(packages); |
| 137 | + Ok(res) |
| 138 | + } |
| 139 | + |
| 140 | + pub(crate) fn with_cargo_env(packages: &Vec<Package>) -> Self { |
| 141 | + let mut res = Self::default(); |
| 142 | + res.inject_cargo_env(packages); |
| 143 | + res |
| 144 | + } |
| 145 | + |
| 146 | + pub(crate) fn get(&self, id: &PackageId) -> Option<&BuildData> { |
| 147 | + self.data.get(id) |
| 148 | + } |
| 149 | + |
| 150 | + fn inject_cargo_env(&mut self, packages: &Vec<Package>) { |
| 151 | + for meta_pkg in packages { |
| 152 | + let resource = self.data.entry(meta_pkg.id.clone()).or_default(); |
| 153 | + inject_cargo_env(meta_pkg, &mut resource.envs); |
| 154 | + |
| 155 | + if let Some(out_dir) = &resource.out_dir { |
| 156 | + // NOTE: cargo and rustc seem to hide non-UTF-8 strings from env! and option_env!() |
| 157 | + if let Some(out_dir) = out_dir.to_str().map(|s| s.to_owned()) { |
| 158 | + resource.envs.push(("OUT_DIR".to_string(), out_dir)); |
| 159 | + } |
| 160 | + } |
| 161 | + } |
| 162 | + } |
| 163 | +} |
| 164 | + |
| 165 | +// FIXME: File a better way to know if it is a dylib |
| 166 | +fn is_dylib(path: &Path) -> bool { |
| 167 | + match path.extension().and_then(OsStr::to_str).map(|it| it.to_string().to_lowercase()) { |
| 168 | + None => false, |
| 169 | + Some(ext) => matches!(ext.as_str(), "dll" | "dylib" | "so"), |
| 170 | + } |
| 171 | +} |
| 172 | + |
| 173 | +/// Recreates the compile-time environment variables that Cargo sets. |
| 174 | +/// |
| 175 | +/// Should be synced with <https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-sets-for-crates> |
| 176 | +fn inject_cargo_env(package: &cargo_metadata::Package, env: &mut Vec<(String, String)>) { |
| 177 | + // FIXME: Missing variables: |
| 178 | + // CARGO, CARGO_PKG_HOMEPAGE, CARGO_CRATE_NAME, CARGO_BIN_NAME, CARGO_BIN_EXE_<name> |
| 179 | + |
| 180 | + let mut manifest_dir = package.manifest_path.clone(); |
| 181 | + manifest_dir.pop(); |
| 182 | + if let Some(cargo_manifest_dir) = manifest_dir.to_str() { |
| 183 | + env.push(("CARGO_MANIFEST_DIR".into(), cargo_manifest_dir.into())); |
| 184 | + } |
| 185 | + |
| 186 | + env.push(("CARGO_PKG_VERSION".into(), package.version.to_string())); |
| 187 | + env.push(("CARGO_PKG_VERSION_MAJOR".into(), package.version.major.to_string())); |
| 188 | + env.push(("CARGO_PKG_VERSION_MINOR".into(), package.version.minor.to_string())); |
| 189 | + env.push(("CARGO_PKG_VERSION_PATCH".into(), package.version.patch.to_string())); |
| 190 | + |
| 191 | + let pre = package.version.pre.iter().map(|id| id.to_string()).format("."); |
| 192 | + env.push(("CARGO_PKG_VERSION_PRE".into(), pre.to_string())); |
| 193 | + |
| 194 | + let authors = package.authors.join(";"); |
| 195 | + env.push(("CARGO_PKG_AUTHORS".into(), authors)); |
| 196 | + |
| 197 | + env.push(("CARGO_PKG_NAME".into(), package.name.clone())); |
| 198 | + env.push(("CARGO_PKG_DESCRIPTION".into(), package.description.clone().unwrap_or_default())); |
| 199 | + //env.push(("CARGO_PKG_HOMEPAGE".into(), package.homepage.clone().unwrap_or_default())); |
| 200 | + env.push(("CARGO_PKG_REPOSITORY".into(), package.repository.clone().unwrap_or_default())); |
| 201 | + env.push(("CARGO_PKG_LICENSE".into(), package.license.clone().unwrap_or_default())); |
| 202 | + |
| 203 | + let license_file = |
| 204 | + package.license_file.as_ref().map(|buf| buf.display().to_string()).unwrap_or_default(); |
| 205 | + env.push(("CARGO_PKG_LICENSE_FILE".into(), license_file)); |
| 206 | +} |
0 commit comments