1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
// Copyright (C) 2024 Intel Corporation
// SPDX-License-Identifier: Apache-2.0

//! An ISPM package which can be built from a subcommand invocation and output to a directory
//! on disk.

use crate::{Error, IspmMetadata, PackageArtifacts, PackageInfo, PackageSpec, Result};
use cargo_subcommand::Subcommand;
use flate2::{write::GzEncoder, Compression};
#[cfg(unix)]
use std::time::SystemTime;
use std::{
    fs::write,
    path::{Path, PathBuf},
};
use tar::{Builder, Header};
use typed_builder::TypedBuilder;

#[cfg(unix)]
/// The directory name for the linux host
pub const HOST_DIRNAME: &str = "linux64";

#[cfg(windows)]
/// The directory name for the windows host
pub const HOST_DIRNAME: &str = "win64";

#[derive(TypedBuilder, Debug, Clone)]
/// A package, which is built from a specification written in a cargo manifest and a set of
/// artifacts pulled from the target profile directory
pub struct Package {
    /// The specification, which is written in [package.metadata.simics] in the crate manifest
    /// of the crate to package
    pub spec: PackageSpec,
    /// The target profile directory from which to pull artifacts and output the built package
    pub target_profile_dir: PathBuf,
}

impl Package {
    /// The name of the inner package file which decompresses to the package directory
    pub const INNER_PACKAGE_FILENAME: &'static str = "package.tar.gz";
    /// The name of the file containing metadata for ISPM to use when installing the package
    pub const METADATA_FILENAME: &'static str = "ispm-metadata";
    /// The name of an addon package type
    pub const ADDON_TYPE: &'static str = "addon";
    /// Default level used by simics
    pub const COMPRESSION_LEVEL: u32 = 6;

    /// Instantiate a package from a cargo subcommand input, which is parsed from command line
    /// arguments
    pub fn from_subcommand(subcommand: &Subcommand) -> Result<Self> {
        let target_profile_dir = subcommand.build_dir(subcommand.target());

        let spec = PackageSpec::from_subcommand(subcommand)?
            .with_artifacts(&PackageArtifacts::from_subcommand(subcommand)?);

        Ok(Self {
            spec,
            target_profile_dir,
        })
    }

    /// Construct the directory name of the package after expansion. It is an error to build a
    /// Rust crate package into any type other than an addon package (simics base is not a Rust
    /// package)
    pub fn package_dirname(&self) -> Result<String> {
        if self.spec.typ == Self::ADDON_TYPE {
            Ok(format!(
                "simics-{}-{}",
                self.spec.package_name, self.spec.version
            ))
        } else {
            Err(Error::NonAddonPackage)
        }
    }

    /// Construct the full package name, which includes the host directory name
    pub fn full_package_name(&self) -> String {
        format!("{}-{}", self.spec.package_name, self.spec.host)
    }

    /// Construct the package name, which is the package number and version, without an
    /// extension
    pub fn package_name(&self) -> String {
        format!(
            "simics-pkg-{}-{}",
            self.spec.package_number, self.spec.version
        )
    }

    /// Construct the package name with the host directory name
    pub fn package_name_with_host(&self) -> String {
        format!("{}-{}", self.package_name(), self.spec.host)
    }

    /// Construct the filename for the output of this ISPM package
    pub fn package_filename(&self) -> String {
        format!("{}.ispm", self.package_name_with_host())
    }

    #[cfg(unix)]
    /// Set common options on a tar header. On Unix, the modified time is set to the current
    /// time and the uid/gid are set to the current user.
    pub fn set_header_common(header: &mut Header) -> Result<()> {
        use libc::{getgid, getpwuid, getuid};
        use std::ffi::CStr;

        header.set_mtime(
            SystemTime::now()
                .duration_since(SystemTime::UNIX_EPOCH)?
                .as_secs(),
        );
        header.set_uid(unsafe { getuid() } as u64);
        header.set_gid(unsafe { getgid() } as u64);
        let username = unsafe {
            CStr::from_ptr(
                getpwuid(getuid())
                    .as_ref()
                    .ok_or_else(|| Error::PackageMetadataFieldNotFound {
                        field_name: "username".to_string(),
                    })?
                    .pw_name,
            )
        }
        .to_str()?
        .to_string();
        let groupname = unsafe {
            CStr::from_ptr(
                getpwuid(getuid())
                    .as_ref()
                    .ok_or_else(|| Error::PackageMetadataFieldNotFound {
                        field_name: "groupname".to_string(),
                    })?
                    .pw_name,
            )
        }
        .to_str()?
        .to_string();
        header.set_username(&username)?;
        header.set_groupname(&groupname)?;
        header.set_mode(0o755);

        Ok(())
    }

    #[cfg(windows)]
    /// On windows, no additional options need to be set for headers and this method is a no-op
    pub fn set_header_common(_header: &mut Header) -> Result<()> {
        Ok(())
    }

    /// Create the inner package.tar.gz tarball which expands to the simics package.
    pub fn create_inner_tarball(&self) -> Result<(Vec<u8>, usize)> {
        let tar_gz = Vec::new();
        let encoder = GzEncoder::new(tar_gz, Compression::new(Self::COMPRESSION_LEVEL));
        let mut tar = Builder::new(encoder);
        // The uncompressed size is used by simics, and must be calculated the way simics
        // expects
        let mut uncompressed_size = 0;

        // Add the packageinfo to the inner package tarball
        let package_info = PackageInfo::from(&self.spec);
        let package_info_string = serde_yaml::to_string(&package_info)? + &package_info.files();
        let package_info_data = package_info_string.as_bytes();
        uncompressed_size += package_info_data.len();
        let mut metadata_header = Header::new_gnu();
        metadata_header.set_size(package_info_data.len() as u64);
        Self::set_header_common(&mut metadata_header)?;
        tar.append_data(
            &mut metadata_header,
            PathBuf::from(self.package_dirname()?)
                .join("packageinfo")
                .join(self.full_package_name()),
            package_info_data,
        )?;
        self.spec.files.iter().try_for_each(|(pkg_loc, src_loc)| {
            let src_path = PathBuf::from(src_loc);
            uncompressed_size += src_path.metadata()?.len() as usize;
            tar.append_path_with_name(src_path, pkg_loc)?;
            Ok::<(), Error>(())
        })?;

        tar.finish()?;

        Ok((tar.into_inner()?.finish()?, uncompressed_size))
    }

    /// Create the outer tarball (actually an ISPM package) containing the inner package and a
    /// metadata file used by ISPM
    pub fn create_tarball(&self) -> Result<Vec<u8>> {
        let tar_gz = Vec::new();
        let encoder = GzEncoder::new(tar_gz, Compression::new(Self::COMPRESSION_LEVEL));
        let mut tar = Builder::new(encoder);
        let (inner_tarball, uncompressed_size) = self.create_inner_tarball()?;

        let mut ispm_metadata = IspmMetadata::from(&self.spec);
        // This size should be exactly equal to the total size of the files in the inner tarball
        // (equal to the size given by du -sb <extracted-tarball-dir>) and does not include the
        // size of the ispm-metadata file itself
        ispm_metadata.uncompressed_size = uncompressed_size;

        let ispm_metadata_string = serde_json::to_string(&ispm_metadata)?;
        let ispm_metadata_data = ispm_metadata_string.as_bytes();
        let mut ispm_metadata_header = Header::new_gnu();
        ispm_metadata_header.set_size(ispm_metadata_data.len() as u64);
        Self::set_header_common(&mut ispm_metadata_header)?;
        tar.append_data(
            &mut ispm_metadata_header,
            Self::METADATA_FILENAME,
            ispm_metadata_data,
        )?;

        let mut inner_tarball_header = Header::new_gnu();
        inner_tarball_header.set_size(inner_tarball.len() as u64);
        Self::set_header_common(&mut inner_tarball_header)?;
        tar.append_data(
            &mut inner_tarball_header,
            Self::INNER_PACKAGE_FILENAME,
            inner_tarball.as_slice(),
        )?;

        tar.finish()?;

        Ok(tar.into_inner()?.finish()?)
    }

    /// Build the package, writing it to the directory specified by `output` and returning
    /// the path to the package
    pub fn build<P>(&mut self, output: P) -> Result<PathBuf>
    where
        P: AsRef<Path>,
    {
        let package_dirname = PathBuf::from(self.package_dirname()?);

        // Rewrite the in-package paths of the spec's files so they begin with the package
        // directory name. This must be done *before* creating the inner tarball and before
        // the package info structure is created because it needs these prefix paths to be
        // present
        self.spec.files.iter_mut().try_for_each(|pkg_src_loc| {
            pkg_src_loc.0 = package_dirname
                .join(&pkg_src_loc.0)
                .to_str()
                .ok_or_else(|| Error::PathConversionError {
                    path: package_dirname.join(&pkg_src_loc.0),
                })?
                .to_string();
            Ok::<(), Error>(())
        })?;

        let tarball = self.create_tarball()?;
        let path = output.as_ref().join(self.package_filename());

        write(&path, tarball).map_err(|e| Error::WritePackageError {
            path: path.clone(),
            source: e,
        })?;

        Ok(path)
    }
}