Skip to main content

simics_package/package/
mod.rs

1// Copyright (C) 2024 Intel Corporation
2// SPDX-License-Identifier: Apache-2.0
3
4//! An ISPM package which can be built from a subcommand invocation and output to a directory
5//! on disk.
6
7use crate::{Error, IspmMetadata, PackageArtifacts, PackageInfo, PackageSpec, Result};
8use cargo_subcommand::Subcommand;
9use flate2::{write::GzEncoder, Compression};
10#[cfg(unix)]
11use std::time::SystemTime;
12use std::{
13    fs::write,
14    path::{Path, PathBuf},
15};
16use tar::{Builder, Header};
17use typed_builder::TypedBuilder;
18
19#[cfg(unix)]
20/// The directory name for the linux host
21pub const HOST_DIRNAME: &str = "linux64";
22
23#[cfg(windows)]
24/// The directory name for the windows host
25pub const HOST_DIRNAME: &str = "win64";
26
27#[derive(TypedBuilder, Debug, Clone)]
28/// A package, which is built from a specification written in a cargo manifest and a set of
29/// artifacts pulled from the target profile directory
30pub struct Package {
31    /// The specification, which is written in [package.metadata.simics] in the crate manifest
32    /// of the crate to package
33    pub spec: PackageSpec,
34    /// The target profile directory from which to pull artifacts and output the built package
35    pub target_profile_dir: PathBuf,
36}
37
38impl Package {
39    /// The name of the inner package file which decompresses to the package directory
40    pub const INNER_PACKAGE_FILENAME: &'static str = "package.tar.gz";
41    /// The name of the file containing metadata for ISPM to use when installing the package
42    pub const METADATA_FILENAME: &'static str = "ispm-metadata";
43    /// The name of an addon package type
44    pub const ADDON_TYPE: &'static str = "addon";
45    /// Default level used by simics
46    pub const COMPRESSION_LEVEL: u32 = 6;
47
48    /// Instantiate a package from a cargo subcommand input, which is parsed from command line
49    /// arguments
50    pub fn from_subcommand(subcommand: &Subcommand) -> Result<Self> {
51        let target_profile_dir = subcommand.build_dir(subcommand.target());
52
53        let spec = PackageSpec::from_subcommand(subcommand)?
54            .with_artifacts(&PackageArtifacts::from_subcommand(subcommand)?);
55
56        Ok(Self {
57            spec,
58            target_profile_dir,
59        })
60    }
61
62    /// Construct the directory name of the package after expansion. It is an error to build a
63    /// Rust crate package into any type other than an addon package (simics base is not a Rust
64    /// package)
65    pub fn package_dirname(&self) -> Result<String> {
66        if self.spec.typ == Self::ADDON_TYPE {
67            Ok(format!(
68                "simics-{}-{}",
69                self.spec.package_name, self.spec.version
70            ))
71        } else {
72            Err(Error::NonAddonPackage)
73        }
74    }
75
76    /// Construct the full package name, which includes the host directory name
77    pub fn full_package_name(&self) -> String {
78        format!("{}-{}", self.spec.package_name, self.spec.host)
79    }
80
81    /// Construct the package name, which is the package number and version, without an
82    /// extension
83    pub fn package_name(&self) -> String {
84        format!(
85            "simics-pkg-{}-{}",
86            self.spec.package_number, self.spec.version
87        )
88    }
89
90    /// Construct the package name with the host directory name
91    pub fn package_name_with_host(&self) -> String {
92        format!("{}-{}", self.package_name(), self.spec.host)
93    }
94
95    /// Construct the filename for the output of this ISPM package
96    pub fn package_filename(&self) -> String {
97        format!("{}.ispm", self.package_name_with_host())
98    }
99
100    #[cfg(unix)]
101    /// Set common options on a tar header. On Unix, the modified time is set to the current
102    /// time and the uid/gid are set to the current user.
103    pub fn set_header_common(header: &mut Header) -> Result<()> {
104        use libc::{getgid, getpwuid, getuid};
105        use std::ffi::CStr;
106
107        header.set_mtime(
108            SystemTime::now()
109                .duration_since(SystemTime::UNIX_EPOCH)?
110                .as_secs(),
111        );
112        header.set_uid(unsafe { getuid() } as u64);
113        header.set_gid(unsafe { getgid() } as u64);
114        let username = unsafe {
115            CStr::from_ptr(
116                getpwuid(getuid())
117                    .as_ref()
118                    .ok_or_else(|| Error::PackageMetadataFieldNotFound {
119                        field_name: "username".to_string(),
120                    })?
121                    .pw_name,
122            )
123        }
124        .to_str()?
125        .to_string();
126        let groupname = unsafe {
127            CStr::from_ptr(
128                getpwuid(getuid())
129                    .as_ref()
130                    .ok_or_else(|| Error::PackageMetadataFieldNotFound {
131                        field_name: "groupname".to_string(),
132                    })?
133                    .pw_name,
134            )
135        }
136        .to_str()?
137        .to_string();
138        header.set_username(&username)?;
139        header.set_groupname(&groupname)?;
140        header.set_mode(0o755);
141
142        Ok(())
143    }
144
145    #[cfg(windows)]
146    /// On windows, no additional options need to be set for headers and this method is a no-op
147    pub fn set_header_common(_header: &mut Header) -> Result<()> {
148        Ok(())
149    }
150
151    /// Create the inner package.tar.gz tarball which expands to the simics package.
152    pub fn create_inner_tarball(&self) -> Result<(Vec<u8>, usize)> {
153        let tar_gz = Vec::new();
154        let encoder = GzEncoder::new(tar_gz, Compression::new(Self::COMPRESSION_LEVEL));
155        let mut tar = Builder::new(encoder);
156        // The uncompressed size is used by simics, and must be calculated the way simics
157        // expects
158        let mut uncompressed_size = 0;
159
160        // Add the packageinfo to the inner package tarball
161        let package_info = PackageInfo::from(&self.spec);
162        let package_info_string = serde_yaml::to_string(&package_info)? + &package_info.files();
163        let package_info_data = package_info_string.as_bytes();
164        uncompressed_size += package_info_data.len();
165        let mut metadata_header = Header::new_gnu();
166        metadata_header.set_size(package_info_data.len() as u64);
167        Self::set_header_common(&mut metadata_header)?;
168        tar.append_data(
169            &mut metadata_header,
170            PathBuf::from(self.package_dirname()?)
171                .join("packageinfo")
172                .join(self.full_package_name()),
173            package_info_data,
174        )?;
175        self.spec.files.iter().try_for_each(|(pkg_loc, src_loc)| {
176            let src_path = PathBuf::from(src_loc);
177            uncompressed_size += src_path.metadata()?.len() as usize;
178            tar.append_path_with_name(src_path, pkg_loc)?;
179            Ok::<(), Error>(())
180        })?;
181
182        tar.finish()?;
183
184        Ok((tar.into_inner()?.finish()?, uncompressed_size))
185    }
186
187    /// Create the outer tarball (actually an ISPM package) containing the inner package and a
188    /// metadata file used by ISPM
189    pub fn create_tarball(&self) -> Result<Vec<u8>> {
190        let tar_gz = Vec::new();
191        let encoder = GzEncoder::new(tar_gz, Compression::new(Self::COMPRESSION_LEVEL));
192        let mut tar = Builder::new(encoder);
193        let (inner_tarball, uncompressed_size) = self.create_inner_tarball()?;
194
195        let mut ispm_metadata = IspmMetadata::from(&self.spec);
196        // This size should be exactly equal to the total size of the files in the inner tarball
197        // (equal to the size given by du -sb <extracted-tarball-dir>) and does not include the
198        // size of the ispm-metadata file itself
199        ispm_metadata.uncompressed_size = uncompressed_size;
200
201        let ispm_metadata_string = serde_json::to_string(&ispm_metadata)?;
202        let ispm_metadata_data = ispm_metadata_string.as_bytes();
203        let mut ispm_metadata_header = Header::new_gnu();
204        ispm_metadata_header.set_size(ispm_metadata_data.len() as u64);
205        Self::set_header_common(&mut ispm_metadata_header)?;
206        tar.append_data(
207            &mut ispm_metadata_header,
208            Self::METADATA_FILENAME,
209            ispm_metadata_data,
210        )?;
211
212        let mut inner_tarball_header = Header::new_gnu();
213        inner_tarball_header.set_size(inner_tarball.len() as u64);
214        Self::set_header_common(&mut inner_tarball_header)?;
215        tar.append_data(
216            &mut inner_tarball_header,
217            Self::INNER_PACKAGE_FILENAME,
218            inner_tarball.as_slice(),
219        )?;
220
221        tar.finish()?;
222
223        Ok(tar.into_inner()?.finish()?)
224    }
225
226    /// Build the package, writing it to the directory specified by `output` and returning
227    /// the path to the package
228    pub fn build<P>(&mut self, output: P) -> Result<PathBuf>
229    where
230        P: AsRef<Path>,
231    {
232        let package_dirname = PathBuf::from(self.package_dirname()?);
233
234        // Rewrite the in-package paths of the spec's files so they begin with the package
235        // directory name. This must be done *before* creating the inner tarball and before
236        // the package info structure is created because it needs these prefix paths to be
237        // present
238        self.spec.files.iter_mut().try_for_each(|pkg_src_loc| {
239            pkg_src_loc.0 = package_dirname
240                .join(&pkg_src_loc.0)
241                .to_str()
242                .ok_or_else(|| Error::PathConversionError {
243                    path: package_dirname.join(&pkg_src_loc.0),
244                })?
245                .to_string();
246            Ok::<(), Error>(())
247        })?;
248
249        let tarball = self.create_tarball()?;
250        let path = output.as_ref().join(self.package_filename());
251
252        write(&path, tarball).map_err(|e| Error::WritePackageError {
253            path: path.clone(),
254            source: e,
255        })?;
256
257        Ok(path)
258    }
259}