Skip to main content

cargo_simics_build/
lib.rs

1// Copyright (C) 2024 Intel Corporation
2// SPDX-License-Identifier: Apache-2.0
3
4// #![deny(missing_docs)]
5
6use artifact_dependency::ARTIFACT_NAMEPARTS;
7use cargo_subcommand::{Args, Subcommand};
8use clap::Parser;
9use command_ext::CommandExtCheck;
10use ispm_wrapper::ispm::{self, GlobalOptions};
11use itertools::Itertools;
12use simics_package::Package;
13use simics_sign::Sign;
14use std::{
15    env::var,
16    fs::{copy, read_dir},
17    io::BufRead,
18    path::PathBuf,
19    process::Command,
20    time::SystemTime,
21};
22
23#[derive(Debug, thiserror::Error)]
24/// An error raised during build
25pub enum Error {
26    #[error("env.SIMICS_BASE variable set in config.toml but could not be parsed from {output:?}")]
27    /// Raised when the SIMICS_BASE environment variable is set but could not be parsed
28    SimicsBaseParseError { output: Option<String> },
29    #[error("The SIMICS_BASE environment variable was not set or present in config.toml, and no packages were installed.")]
30    /// Raised when the SIMICS_BASE environment variable was not set or present in config.toml, and no base package was installed
31    NoInstalledPackages,
32    #[error("The SIMICS_BASE environment variable was not set or present in config.toml, and no base package was installed.")]
33    /// Raised when the SIMICS_BASE environment variable was not set or present in config.toml, and no base package was installed
34    NoBasePackage,
35    #[error("Base package found, but no paths registered for package number 1000")]
36    /// Raised when a base package is found, but no paths are registered for package number 1000
37    NoPathsForBasePackage,
38    #[error("Base package directory {path:?} does not exist")]
39    /// Raised when the base package directory does not exist
40    BasePackageDirectoryDoesNotExist { path: PathBuf },
41    #[error("No cdylib crate artifact found for package {package}. Ensure the build succeeded and there is a [lib] entry in Cargo.toml with crate-type 'cdylib'.")]
42    /// Raised when no cdylib crate artifact is found for a package
43    NoCdylibArtifact { package: String },
44    #[error("No parent directory found for {path:?}")]
45    /// Raised when no parent directory is found for a path
46    NoParentDirectory { path: PathBuf },
47    #[error("No filename found for {path:?}")]
48    /// Raised when no filename is found for a path
49    NoFilename { path: PathBuf },
50    #[error("Failed to copy library from {from:?} to {to:?}: {source:?}")]
51    /// An error occurred while copying a library
52    CopyLibrary {
53        /// The source path
54        from: PathBuf,
55        /// The destination path
56        to: PathBuf,
57        /// The underlying error
58        source: std::io::Error,
59    },
60    #[error("Failed to read directory {path:?}: {source}")]
61    /// An error occurred while reading a directory
62    ReadDirectory {
63        /// The path to the directory that could not be read
64        path: PathBuf,
65        /// The underlying error
66        source: std::io::Error,
67    },
68    #[error(transparent)]
69    /// A wrapped std::io::Error
70    IoError(#[from] std::io::Error),
71    #[error(transparent)]
72    /// Any wrapped other error
73    Other(#[from] anyhow::Error),
74    #[error(transparent)]
75    /// A wrapped std::env::VarError
76    VarError(#[from] std::env::VarError),
77    #[error(transparent)]
78    /// A wrapped subcommand error
79    SubcommandError(#[from] cargo_subcommand::Error),
80    #[error(transparent)]
81    /// A wrapped CommandExt error
82    CommandExtError(#[from] command_ext::CommandExtError),
83    #[error(transparent)]
84    /// A wrapped signature error
85    SignatureError(#[from] simics_sign::Error),
86    #[error(transparent)]
87    /// A wrapped package error
88    PackageError(#[from] simics_package::Error),
89    #[error(transparent)]
90    /// A wrapped from utf8 error
91    FromUtf8Error(#[from] std::string::FromUtf8Error),
92}
93
94#[derive(Parser, Debug, Clone)]
95pub struct Cmd {
96    #[clap(subcommand)]
97    pub simics_build: SimicsBuildCmd,
98}
99
100#[derive(clap::Subcommand, Debug, Clone)]
101pub enum SimicsBuildCmd {
102    /// Helps cargo build apks for Android
103    SimicsBuild {
104        #[clap(flatten)]
105        args: Args,
106        #[clap(long)]
107        simics_base: Option<PathBuf>,
108    },
109}
110
111pub struct App;
112
113type Result<T> = std::result::Result<T, Error>;
114
115impl App {
116    pub fn run(cmd: Cmd) -> Result<PathBuf> {
117        let SimicsBuildCmd::SimicsBuild { args, simics_base } = cmd.simics_build;
118
119        let subcommand = Subcommand::new(args)?;
120        let cargo = var("CARGO")?;
121
122        // First, check if `env.SIMICS_BASE` is set:
123        let simics_base = if let Some(simics_base) = simics_base {
124            simics_base.clone()
125        } else if let Ok(output) = Command::new(&cargo)
126            .arg("-Zunstable-options")
127            .arg("config")
128            .arg("get")
129            .arg("env.SIMICS_BASE")
130            .check()
131        {
132            let line = output.stdout.lines().next().transpose()?;
133            line.clone()
134                .and_then(|l| l.split('=').last().map(|s| s.trim().replace('"', "")))
135                .map(PathBuf::from)
136                .ok_or_else(|| Error::SimicsBaseParseError { output: line })?
137        } else if let Ok(simics_base) = var("SIMICS_BASE") {
138            PathBuf::from(simics_base)
139        } else {
140            if !subcommand.quiet() {
141                println!("No SIMICS_BASE variable set, using the latest installed package with package number 1000")
142            }
143
144            let mut packages = ispm::packages::list(&GlobalOptions::default())?;
145            packages.sort();
146            let Some(installed) = packages.installed_packages.as_ref() else {
147                return Err(Error::NoInstalledPackages);
148            };
149            let Some(base) = installed.iter().find(|p| p.package_number == 1000) else {
150                return Err(Error::NoBasePackage);
151            };
152            base.paths
153                .first()
154                .ok_or_else(|| Error::NoPathsForBasePackage)?
155                .clone()
156        };
157
158        if !simics_base.exists() {
159            return Err(Error::BasePackageDirectoryDoesNotExist { path: simics_base });
160        }
161
162        // Clean the package's release profile
163        if !subcommand.quiet() {
164            println!("Building package {}", subcommand.package());
165        }
166
167        // Build the package
168        let mut build_cmd = Command::new(&cargo);
169        build_cmd.arg("rustc");
170        build_cmd.env("SIMICS_BASE", simics_base);
171        // Remove SIMICS_BINDINGS_NOCLEAN from cargo simics-build outputs to reduce the size
172        // of the sys bindings as much as possible
173        build_cmd.env_remove("SIMICS_BINDINGS_NOCLEAN");
174        subcommand.args().apply(&mut build_cmd);
175        #[cfg(unix)]
176        build_cmd.args(["--", "-C", "link-args=-Wl,--gc-sections"]);
177        build_cmd.check()?;
178
179        // Get the module cdylib
180        let module_cdylib = subcommand
181            .artifacts()
182            .map(|a| {
183                subcommand.build_dir(subcommand.target()).join(format!(
184                    "{}{}{}",
185                    ARTIFACT_NAMEPARTS.0,
186                    a.name.replace('-', "_"),
187                    ARTIFACT_NAMEPARTS.1
188                ))
189            })
190            .find(|p| p.exists())
191            .ok_or_else(|| Error::NoCdylibArtifact {
192                package: subcommand.package().to_string(),
193            })?;
194
195        // Sign the module cdylib
196        if !subcommand.quiet() {
197            println!("Signing module {module_cdylib:?}");
198        }
199
200        let mut signed = Sign::new(&module_cdylib)?;
201
202        let signed_module_cdylib = module_cdylib
203            .parent()
204            .ok_or_else(|| Error::NoParentDirectory {
205                path: module_cdylib.to_path_buf(),
206            })?
207            .join({
208                let file_name = module_cdylib
209                    .file_name()
210                    .ok_or_else(|| Error::NoFilename {
211                        path: module_cdylib.to_path_buf(),
212                    })?
213                    .to_str()
214                    .ok_or_else(|| Error::NoFilename {
215                        path: module_cdylib.to_path_buf(),
216                    })?;
217                let module_cdylib_dir =
218                    module_cdylib
219                        .parent()
220                        .ok_or_else(|| Error::NoParentDirectory {
221                            path: module_cdylib.to_path_buf(),
222                        })?;
223
224                module_cdylib_dir
225                    .join(file_name.replace('_', "-"))
226                    .to_str()
227                    .ok_or_else(|| Error::NoFilename {
228                        path: module_cdylib.to_path_buf(),
229                    })?
230            });
231
232        signed.write(&signed_module_cdylib)?;
233
234        let target_profile_build_dir = subcommand.build_dir(subcommand.target()).join("build");
235
236        // Find interfaces
237        let target_profile_build_subdirs = read_dir(&target_profile_build_dir)
238            .map_err(|e| Error::ReadDirectory {
239                path: target_profile_build_dir,
240                source: e,
241            })?
242            .filter_map(|rd| rd.ok())
243            .map(|de| de.path())
244            .filter(|p| {
245                p.is_dir()
246                    && p.file_name().is_some_and(|n| {
247                        n.to_str()
248                            .is_some_and(|ns| ns.starts_with(subcommand.package()))
249                    })
250                    && !p
251                        .join(format!("build-script-build{}", ARTIFACT_NAMEPARTS.4))
252                        .exists()
253                    && p.join("out").is_dir()
254            })
255            .collect::<Vec<_>>();
256
257        // Source, Destination of interface libraries
258        let cdylib_out_artifacts = target_profile_build_subdirs
259            .iter()
260            .map(|bd| bd.join("out"))
261            .map(|od| {
262                read_dir(&od)
263                    .map_err(|e| Error::ReadDirectory {
264                        path: od.clone(),
265                        source: e,
266                    })
267                    .map(|rd| {
268                        Ok(rd
269                            .filter_map(|rd| rd.ok())
270                            .map(|de| de.path())
271                            .filter(|p| {
272                                p.file_name().is_some_and(|n| {
273                                    n.to_str().is_some_and(|ns| {
274                                        ns.starts_with(ARTIFACT_NAMEPARTS.0)
275                                            && ns.ends_with(ARTIFACT_NAMEPARTS.1)
276                                    })
277                                })
278                            })
279                            .collect::<Vec<_>>())
280                    })?
281            })
282            .collect::<Result<Vec<_>>>()?
283            .into_iter()
284            .flatten()
285            .filter_map(|p| {
286                p.clone()
287                    .file_name()
288                    .and_then(|n| n.to_str().map(|n| (p, n.to_string())))
289            })
290            .sorted_by(|(_, a), (_, b)| a.cmp(b))
291            .chunk_by(|(_, n)| n.clone())
292            // Get the newest one
293            .into_iter()
294            .filter_map(|(_, g)| {
295                g.max_by_key(|(p, _)| {
296                    p.metadata()
297                        .map(|m| m.modified().unwrap_or(SystemTime::UNIX_EPOCH))
298                        .unwrap_or_else(|_| SystemTime::UNIX_EPOCH)
299                })
300            })
301            .map(|(a, _)| {
302                a.file_name()
303                    .and_then(|n| n.to_str())
304                    .ok_or_else(|| Error::NoFilename { path: a.clone() })
305                    .map(|t| (a.clone(), subcommand.build_dir(subcommand.target()).join(t)))
306            })
307            .collect::<Result<Vec<_>>>()?;
308
309        // Sign all the interfaces, re-signing if needed, and copy them to the build directory
310        cdylib_out_artifacts.iter().try_for_each(|(a, t)| {
311            if !subcommand.quiet() {
312                println!("Copying interface library {a:?}");
313            }
314
315            copy(a, t).map_err(|e| Error::CopyLibrary {
316                from: a.clone(),
317                to: t.clone(),
318                source: e,
319            })?;
320
321            Ok::<(), anyhow::Error>(())
322        })?;
323
324        let package = Package::from_subcommand(&subcommand)?
325            .build(subcommand.build_dir(subcommand.target()))?;
326
327        if !subcommand.quiet() {
328            println!("Built ISPM package {:?}", package);
329        }
330
331        Ok(package)
332    }
333}