artifact_dependency/
lib.rs

1// Copyright (C) 2023 Intel Corporation, Rowan Hart
2// SPDX-License-Identifier: Apache-2.0
3
4//! Feature-light crate to build and use dependencies whose results are Artifacts:
5//! - Static Libraries
6//! - C Dynamic Libraries
7//! - Binaries
8
9#![deny(clippy::unwrap_used)]
10#![forbid(unsafe_code)]
11
12use anyhow::{anyhow, bail, ensure, Result};
13use cargo_metadata::{camino::Utf8PathBuf, MetadataCommand, Package};
14use serde::{Deserialize, Serialize};
15use std::{
16    env::var,
17    hash::{Hash, Hasher},
18    path::PathBuf,
19    process::{Command, Stdio},
20    str::FromStr,
21};
22use typed_builder::TypedBuilder;
23
24#[derive(Clone, Debug, Copy)]
25/// Crate type to include as the built [`Artifact`]
26pub enum CrateType {
27    Executable,
28    CDynamicLibrary,
29    Dylib,
30    StaticLibrary,
31    RustLibrary,
32    ProcMacro,
33    // NOTE: Doesn't include raw-dylib, which allows DLL linking without import libraries:
34    // https://rust-lang.github.io/rfcs/2627-raw-dylib-kind.html
35}
36
37#[derive(Clone, Debug)]
38/// Profile to build. [`ArtifactDependency`] defaults to building the current profile in use,
39/// but a different profile can be selected.
40pub enum Profile {
41    Release,
42    Dev,
43    Other(String),
44}
45
46impl FromStr for Profile {
47    type Err = anyhow::Error;
48
49    fn from_str(s: &str) -> Result<Self> {
50        match s {
51            "release" => Ok(Self::Release),
52            "dev" => Ok(Self::Dev),
53            "debug" => Ok(Self::Dev),
54            _ => Ok(Self::Other(s.to_string())),
55        }
56    }
57}
58
59#[derive(TypedBuilder, Clone, Debug)]
60/// Builder to find and optionally build an artifact dependency from a particular workspace
61pub struct ArtifactDependency {
62    #[builder(setter(into, strip_option), default)]
63    /// Workspace root to search for an artifact dependency in. Defaults to the current workspace
64    /// if one is not provided.
65    pub workspace_root: Option<PathBuf>,
66    /// Crate name to search for an artifact dependency for. Defaults to `CARGO_PKG_NAME` if it
67    /// is set, otherwise the root package name in the workspace. If neither are set, an error
68    /// will be returned.
69    #[builder(setter(into, strip_option), default)]
70    pub crate_name: Option<String>,
71    /// Type of artifact to build.
72    pub artifact_type: CrateType,
73    #[builder(setter(into))]
74    /// Profile, defaults to the current profile
75    pub profile: Profile,
76    /// Build the artifact if it is missing
77    pub build_missing: bool,
78    #[builder(default = true)]
79    /// (Re-)build the artifact even if it is not missing. This is the default because otherwise
80    /// it's very common to have a "what is going on why aren't my print statements showing up"
81    /// moment
82    pub build_always: bool,
83    #[builder(setter(into), default)]
84    pub features: Vec<String>,
85    #[builder(setter(into, strip_option), default)]
86    pub target_name: Option<String>,
87    #[builder(setter(into), default)]
88    pub capture_output: bool,
89    #[builder(setter(into, strip_option), default)]
90    pub env: Option<Vec<(String, String)>>,
91}
92
93// NOTE: Artifact naming is not very easy to discern, we have to dig hard into rustc.
94// Windows dll import lib: https://github.com/rust-lang/rust/blob/b2b34bd83192c3d16c88655158f7d8d612513e88/compiler/rustc_codegen_llvm/src/back/archive.rs#L129
95// Others by crate type: https://github.com/rust-lang/rust/blob/b2b34bd83192c3d16c88655158f7d8d612513e88/compiler/rustc_session/src/output.rs#L141
96// The default settings: https://github.com/rust-lang/rust/blob/db9d1b20bba1968c1ec1fc49616d4742c1725b4b/compiler/rustc_target/src/spec/mod.rs#L1422-L1529
97//
98// | Platform Spec   | DLL Prefix | DLL Suffix | EXE Suffix | Staticlib Prefix | Staticlib Suffix |
99// | Default         | lib (d)    | .so (d)    |            | lib (d)          | .a (d)           |
100// | MSVC            |            | .dll       | .exe       |                  | .lib             |
101// | Windows GNU     |            | .dll       | .exe       | lib (d)          | .a (d)           |
102// | WASM            | lib (d)    | .wasm      | .wasm      | lib (d)          | .a (d)           |
103// | AIX             | lib (d)    | .a         |            | lib (d)          | .a (d)           |
104// | Apple           | lib (d)    | .dylib     |            | lib (d)          | .a (d,framework?)|
105// | NVPTX           |            | .ptx       | .ptx       | lib (d)          | .a (d)           |
106// | Windows GNULLVM |            | .dll       | .exe       | lib (d)          | .a (d)           |
107
108#[cfg(target_family = "unix")]
109/// The dll prefix, dll suffix, staticlib prefix, staticlib suffix, and exe suffix for the current target
110pub const ARTIFACT_NAMEPARTS: (&str, &str, &str, &str, &str) = ("lib", ".so", "lib", ".a", "");
111#[cfg(target_family = "darwin")]
112/// The dll prefix, dll suffix, staticlib prefix, staticlib suffix, and exe suffix for the current target
113pub const ARTIFACT_NAMEPARTS: (&str, &str, &str, &str, &str) = ("lib", ".dylib", "lib", ".a", "");
114#[cfg(all(target_os = "windows", target_env = "msvc"))]
115/// The dll prefix, dll suffix, staticlib prefix, staticlib suffix, and exe suffix for the current target
116pub const ARTIFACT_NAMEPARTS: (&str, &str, &str, &str, &str) = ("", ".dll", "", ".lib", ".exe");
117#[cfg(all(target_os = "windows", any(target_env = "gnu", target_env = "gnullvm")))]
118/// The dll prefix, dll suffix, staticlib prefix, staticlib suffix, and exe suffix for the current target
119pub const ARTIFACT_NAMEPARTS: (&str, &str, &str, &str, &str) = ("", ".dll", "lib", ".a", ".exe");
120
121#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
122/// A built artifact
123pub struct Artifact {
124    /// The path to the artifact output, as specified by the `artifact_type` field if the
125    /// dependency has multiple outputs.
126    pub path: PathBuf,
127    /// Package metadata for the artifact
128    pub package: Package,
129}
130
131impl Hash for Artifact {
132    fn hash<H>(&self, state: &mut H)
133    where
134        H: Hasher,
135    {
136        self.path.hash(state);
137        self.package.name.hash(state);
138        self.package.version.hash(state);
139        self.package.authors.hash(state);
140        self.package.id.hash(state);
141        self.package.description.hash(state);
142        self.package.license.hash(state);
143        self.package.license_file.hash(state);
144        self.package.targets.hash(state);
145        self.package.manifest_path.hash(state);
146        self.package.categories.hash(state);
147        self.package.keywords.hash(state);
148        self.package.readme.hash(state);
149        self.package.repository.hash(state);
150        self.package.homepage.hash(state);
151        self.package.documentation.hash(state);
152        self.package.edition.hash(state);
153        self.package.links.hash(state);
154        self.package.publish.hash(state);
155        self.package.default_run.hash(state);
156        self.package.rust_version.hash(state);
157    }
158}
159
160impl Artifact {
161    /// Instantiate a new artifact at a path with a given metadata object
162    fn new(path: PathBuf, package: Package) -> Self {
163        Self { path, package }
164    }
165}
166
167impl ArtifactDependency {
168    /// Build the dependency by invoking `cargo build`
169    pub fn build(&mut self) -> Result<Artifact> {
170        let workspace_root = if let Some(workspace_root) = self.workspace_root.clone() {
171            workspace_root
172        } else {
173            MetadataCommand::new()
174                .no_deps()
175                .exec()?
176                .workspace_root
177                .into()
178        };
179
180        let metadata = MetadataCommand::new()
181            .current_dir(&workspace_root)
182            .no_deps()
183            .manifest_path(workspace_root.join("Cargo.toml"))
184            .exec()?;
185
186        self.crate_name = if let Some(crate_name) = self.crate_name.as_ref() {
187            Some(crate_name.clone())
188        } else if let Ok(crate_name) = var("CARGO_PKG_NAME") {
189            Some(crate_name)
190        } else if let Some(root_package) = metadata.root_package() {
191            Some(root_package.name.clone())
192        } else {
193            bail!("No name provided and no root package in provided workspace at {}, could not determine crate name.", workspace_root.display());
194        };
195
196        let crate_name = self
197            .crate_name
198            .as_ref()
199            .cloned()
200            .ok_or_else(|| anyhow!("self.crate_name must have a value at this point"))?;
201
202        let package = metadata
203            .packages
204            .iter()
205            .find(|p| p.name == crate_name)
206            .ok_or_else(|| {
207                anyhow!(
208                    "No package matching name {} found in packages {:?} workspace at {}",
209                    crate_name,
210                    metadata
211                        .packages
212                        .iter()
213                        .map(|p| p.name.clone())
214                        .collect::<Vec<_>>(),
215                    workspace_root.display()
216                )
217            })?;
218
219        let package_name = package.name.clone();
220        let package_result_name = package_name.replace('-', "_");
221
222        let (dll_prefix, dll_suffix, staticlib_prefix, staticlib_suffix, exe_suffix) =
223            ARTIFACT_NAMEPARTS;
224
225        let profile = self.profile.clone();
226
227        let profile_target_path = metadata.target_directory.join(match &profile {
228            Profile::Release => "release".to_string(),
229            Profile::Dev => "debug".to_string(),
230            Profile::Other(o) => o.clone(),
231        });
232
233        let artifact_path = match self.artifact_type {
234            CrateType::Executable => {
235                profile_target_path.join(format!("{}{}", &package_result_name, exe_suffix))
236            }
237            CrateType::CDynamicLibrary => profile_target_path.join(format!(
238                "{}{}{}",
239                dll_prefix, &package_result_name, dll_suffix
240            )),
241            CrateType::StaticLibrary => profile_target_path.join(format!(
242                "{}{}{}",
243                staticlib_prefix, package_result_name, staticlib_suffix
244            )),
245            _ => bail!(
246                "Crate type {:?} is not supported as an artifact dependency source",
247                self.artifact_type
248            ),
249        };
250
251        let artifact_path = if (self.build_missing && !artifact_path.exists()) || self.build_always
252        {
253            let cargo = var("CARGO").unwrap_or("cargo".to_string());
254            let mut cargo_command = Command::new(cargo);
255            cargo_command
256                .arg("build")
257                .arg("--manifest-path")
258                .arg(workspace_root.join("Cargo.toml"))
259                .arg("--package")
260                .arg(&package_name);
261
262            // TODO: This will solve one build script trying to build the artifact at
263            // once, but doesn't resolve parallel scripts trying to both build it
264            // simultaneously, we need to actually detect the lock.
265            let build_target_dir = if let Some(target_name) = self.target_name.as_ref() {
266                metadata.target_directory.join(target_name)
267            } else {
268                metadata.target_directory
269            };
270
271            cargo_command.arg("--target-dir").arg(&build_target_dir);
272
273            match &profile {
274                Profile::Release => {
275                    cargo_command.arg("--release");
276                }
277                Profile::Other(o) => {
278                    cargo_command.args(vec!["--profile".to_string(), o.clone()]);
279                }
280                _ => {}
281            }
282
283            cargo_command.arg(format!("--features={}", self.features.join(",")));
284
285            if let Some(env) = self.env.as_ref() {
286                cargo_command.envs(env.iter().cloned());
287            }
288
289            if self.capture_output {
290                let output = cargo_command
291                    .stderr(Stdio::piped())
292                    .stdout(Stdio::piped())
293                    .output()?;
294
295                if !output.status.success() {
296                    bail!(
297                        "Failed to build artifact crate:\nstdout: {}\nstderr: {}",
298                        String::from_utf8_lossy(&output.stdout),
299                        String::from_utf8_lossy(&output.stderr)
300                    );
301                }
302            } else {
303                let status = cargo_command.status()?;
304
305                if !status.success() {
306                    bail!("Failed to build artifact crate");
307                }
308            }
309
310            let artifact_path: PathBuf = build_target_dir
311                .join({
312                    let components = artifact_path
313                        .components()
314                        .rev()
315                        .take(2)
316                        .map(|c| c.to_string())
317                        .collect::<Vec<_>>();
318                    components.iter().rev().collect::<Utf8PathBuf>()
319                })
320                .into();
321
322            ensure!(
323                artifact_path.exists(),
324                "Artifact build succeeded, but artifact not found in {}",
325                artifact_path.display()
326            );
327
328            artifact_path
329        } else {
330            artifact_path.into()
331        };
332
333        Ok(Artifact::new(artifact_path, package.clone()))
334    }
335}