simics_package/spec/
mod.rs

1// Copyright (C) 2024 Intel Corporation
2// SPDX-License-Identifier: Apache-2.0
3
4//! Specifications for internal file formats used in the Simics packaging process
5
6use std::{env::var, iter::once, path::PathBuf};
7
8use crate::{Error, PackageArtifacts, Result, HOST_DIRNAME};
9use cargo_metadata::{MetadataCommand, Package};
10use cargo_subcommand::Subcommand;
11use serde::{Deserialize, Serialize};
12use serde_json::from_value;
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
15/// Implements the Schema for package-specs.json
16///
17/// {
18///     "$schema": "https://json-schema.org/draft/2020-12/schema",
19///     "type": "array",
20///     "title": "Simics Package Specification file",
21///     "items": {
22///         "type": "object",
23///         "required": [
24///             "package-name", "package-number", "name", "description",
25///             "host", "version", "build-id", "build-id-namespace",
26///             "confidentiality", "files"
27///         ],
28///         "properties": {
29///             "package-name": {
30///                 "type": "string"
31///             },
32///             "package-number": {
33///                 "anyOf": [{"type": "integer"}, {"type": "null"}]
34///             },
35///             "name": {
36///                 "type": "string"
37///             },
38///             "description": {
39///                 "type": "string"
40///             },
41///             "host": {
42///                 "type": "string"
43///             },
44///             "version": {
45///                 "type": "string"
46///             },
47///             "build-id": {
48///                 "type": "integer"
49///             },
50///             "build-id-namespace": {
51///                 "type": "string"
52///             },
53///             "confidentiality": {
54///                 "type": "string"
55///             },
56///             "files": {
57///                 "type": "object",
58///                 "patternProperties": {
59///                     "^[^\\:]*/$": {
60///                         "type": "object",
61///                         "properties": {
62///                             "source-directory": {
63///                                 "type": "string"
64///                             },
65///                             "file-list": {
66///                                 "type": "string"
67///                             },
68///                             "suffixes": {
69///                                 "type": "array",
70///                                 "items": {
71///                                     "type": "string"
72///                                 }
73///                             }
74///                         }
75///                     },
76///                     "^[^\\:]*[^/]$": {
77///                         "type": "string"
78///                     }
79///                 }
80///             },
81///             "type": {
82///                 "enum": ["addon", "base"]
83///             },
84///             "disabled": {
85///                 "type": "boolean"
86///             },
87///             "doc-title": {
88///                 "anyOf": [{"type": "string"}, {"type": "null"}]
89///             },
90///             "make-targets": {
91///                 "type": "array",
92///                 "items": {
93///                     "type": "string"
94///                 }
95///             }
96///         }
97///     }
98/// }
99pub struct PackageSpec {
100    #[serde(rename = "package-name")]
101    /// The one-word alphanumeric package name, e.g. 'TSFFS-Fuzzer' in Camel-Kebab-Case
102    pub package_name: String,
103    #[serde(rename = "package-number")]
104    /// The package number. This is the only field that must be included in the
105    /// crate metadata. It must be *globally* unique.
106    pub package_number: isize,
107    /// The human-readable name of the package e.g. 'TSFFS Fuzzer', the package name with
108    /// dashes replaced with spaces.
109    pub name: String,
110    /// A description of the package, e.g. 'TSFFS: The Target Software Fuzzer for SIMICS'
111    pub description: String,
112    /// The host this package is built for, either 'win64' or 'linux64'
113    pub host: String,
114    /// The version number for this package, e.g. '6.0.2' or '6.0.pre6'
115    pub version: String,
116    #[serde(rename = "build-id")]
117    /// The build ID for this package, later versions should have later IDs. This number should
118    /// monotonically increase and only has meaning between two packages with the same
119    /// `build_id_namespace`
120    pub build_id: isize,
121    #[serde(rename = "build-id-namespace")]
122    /// An identifier for the build ID, e.g. 'tsffs'
123    pub build_id_namespace: String,
124    /// The confidentiality of the package, e.g. 'Public', but can be any string value based on
125    /// the authors confidentiality requirements.
126    pub confidentiality: String,
127    #[serde(default)]
128    /// A mapping from the path in the package to the full path on disk of the file.
129    pub files: Vec<(String, String)>,
130    #[serde(rename = "type")]
131    /// Either "addon" or "base", all packages should be 'addon'
132    pub typ: String,
133    /// Whether the package is disabled, default is not disabled
134    pub disabled: bool,
135    #[serde(rename = "doc-title")]
136    /// The title used in documentation for the package
137    pub doc_title: String,
138    #[serde(rename = "make-targets")]
139    /// The list of targets to build for this package
140    pub make_targets: Vec<String>,
141    #[serde(rename = "include-release-notes")]
142    /// Whether release notes should be included in the package, not included by default
143    pub include_release_notes: bool,
144    #[serde(rename = "ip-plans")]
145    /// Plans for the IP of this package. Typically empty.
146    pub ip_plans: Vec<String>,
147    #[serde(rename = "legacy-doc-make-targets")]
148    /// Legacy support for doc make targets. Typically empty.
149    pub legacy_doc_make_targets: Vec<String>,
150    #[serde(rename = "release-notes")]
151    /// Release notes. Typically empty.
152    pub release_notes: Vec<String>,
153    #[serde(rename = "access-labels")]
154    /// Labels for managing package access, e.g. 'external-intel'
155    pub access_labels: Vec<String>,
156}
157
158impl PackageSpec {
159    /// Create a package spec by reading the manifest specified by a subcommand
160    pub fn from_subcommand(subcommand: &Subcommand) -> Result<Self> {
161        let manifest_spec = ManifestPackageSpec::from_subcommand(subcommand)?;
162        Ok(Self {
163            package_name: manifest_spec.package_name.ok_or_else(|| {
164                Error::PackageMetadataFieldNotFound {
165                    field_name: "package_name".to_string(),
166                }
167            })?,
168            package_number: manifest_spec.package_number.ok_or_else(|| {
169                Error::PackageMetadataFieldNotFound {
170                    field_name: "package_number".to_string(),
171                }
172            })?,
173            name: manifest_spec
174                .name
175                .ok_or_else(|| Error::PackageMetadataFieldNotFound {
176                    field_name: "name".to_string(),
177                })?,
178            description: manifest_spec.description.ok_or_else(|| {
179                Error::PackageMetadataFieldNotFound {
180                    field_name: "description".to_string(),
181                }
182            })?,
183            host: manifest_spec
184                .host
185                .ok_or_else(|| Error::PackageMetadataFieldNotFound {
186                    field_name: "host".to_string(),
187                })?,
188            version: manifest_spec
189                .version
190                .ok_or_else(|| Error::PackageMetadataFieldNotFound {
191                    field_name: "version".to_string(),
192                })?,
193            build_id: manifest_spec.build_id.ok_or_else(|| {
194                Error::PackageMetadataFieldNotFound {
195                    field_name: "build_id".to_string(),
196                }
197            })?,
198            build_id_namespace: manifest_spec.build_id_namespace.ok_or_else(|| {
199                Error::PackageMetadataFieldNotFound {
200                    field_name: "build_id_namespace".to_string(),
201                }
202            })?,
203            confidentiality: manifest_spec.confidentiality.ok_or_else(|| {
204                Error::PackageMetadataFieldNotFound {
205                    field_name: "confidentiality".to_string(),
206                }
207            })?,
208            files: manifest_spec.files.clone(),
209            typ: manifest_spec
210                .typ
211                .ok_or_else(|| Error::PackageMetadataFieldNotFound {
212                    field_name: "type".to_string(),
213                })?,
214            disabled: manifest_spec.disabled,
215            doc_title: manifest_spec.doc_title.ok_or_else(|| {
216                Error::PackageMetadataFieldNotFound {
217                    field_name: "doc_title".to_string(),
218                }
219            })?,
220            make_targets: manifest_spec.make_targets.clone(),
221            include_release_notes: manifest_spec.include_release_notes,
222            ip_plans: manifest_spec.ip_plans.clone(),
223            legacy_doc_make_targets: manifest_spec.legacy_doc_make_targets.clone(),
224            release_notes: manifest_spec.release_notes.clone(),
225            access_labels: manifest_spec.access_labels.clone(),
226        })
227    }
228
229    /// Add a set of artifacts (not specified in the manifest) to the specification
230    pub fn with_artifacts(mut self, artifacts: &PackageArtifacts) -> Self {
231        self.files = artifacts.files.clone();
232        self
233    }
234}
235
236#[derive(Debug, Clone, Serialize, Deserialize, Default)]
237/// A package specification deserialized from the
238///
239/// [package.metadata.simics]
240///
241/// field in Cargo.toml. This specification is used to generate the real specification, and many
242/// options left optional in the manifest are not optional to Simics. Sane defaults are provided
243/// for all options.
244pub struct ManifestPackageSpec {
245    #[serde(rename = "package-name", default)]
246    /// The one-word alphanumeric package name, e.g. 'TSFFS-Fuzzer' in Camel-Kebab-Case
247    package_name: Option<String>,
248    #[serde(rename = "package-number", default)]
249    /// The package number. This is the only field that must be included in the
250    /// crate metadata. It must be *globally* unique.
251    package_number: Option<isize>,
252    #[serde(default)]
253    /// The human-readable name of the package e.g. 'TSFFS Fuzzer', the package name with
254    /// dashes replaced with spaces.
255    name: Option<String>,
256    #[serde(default)]
257    /// A description of the package, e.g. 'TSFFS: The Target Software Fuzzer for SIMICS'
258    description: Option<String>,
259    #[serde(default)]
260    /// The host this package is built for, either 'win64' or 'linux64'
261    host: Option<String>,
262    #[serde(default)]
263    /// The version number for this package, e.g. '6.0.2' or '6.0.pre6'
264    version: Option<String>,
265    #[serde(rename = "build-id", default)]
266    /// The build ID for this package, later versions should have later IDs. This number should
267    /// monotonically increase and only has meaning between two packages with the same
268    /// `build_id_namespace`
269    build_id: Option<isize>,
270    #[serde(rename = "build-id-namespace", default)]
271    /// An identifier for the build ID, e.g. 'tsffs'
272    build_id_namespace: Option<String>,
273    #[serde(default)]
274    /// The confidentiality of the package, e.g. 'Public', but can be any string value based on
275    /// the authors confidentiality requirements.
276    confidentiality: Option<String>,
277    #[serde(default)]
278    /// A mapping from the path in the package to the full path on disk of the file.
279    files: Vec<(String, String)>,
280    #[serde(rename = "type", default)]
281    // Either "addon" or "base", all packages should be 'addon'
282    typ: Option<String>,
283    #[serde(default)]
284    /// Whether the package is disabled, default is not disabled
285    disabled: bool,
286    #[serde(rename = "doc-title", default)]
287    /// The title used in documentation for the package
288    doc_title: Option<String>,
289    #[serde(rename = "make-targets", default)]
290    /// The list of targets to build for this package
291    make_targets: Vec<String>,
292    #[serde(rename = "include-release-notes", default)]
293    /// Whether release notes should be included in the package, not included by default
294    include_release_notes: bool,
295    #[serde(rename = "ip-plans", default)]
296    ip_plans: Vec<String>,
297    #[serde(rename = "legacy-doc-make-targets", default)]
298    legacy_doc_make_targets: Vec<String>,
299    #[serde(rename = "release-notes", default)]
300    release_notes: Vec<String>,
301    #[serde(rename = "access-labels", default)]
302    /// Labels for managing package access, e.g. 'external-intel'
303    access_labels: Vec<String>,
304}
305
306impl ManifestPackageSpec {
307    /// Return the default type when deserializing
308    pub fn default_type() -> String {
309        "addon".to_string()
310    }
311}
312
313impl ManifestPackageSpec {
314    /// Create a specification from the package metadata returned from a cargo metadata
315    /// invocation
316    pub fn from_package(package: &Package) -> Result<Self> {
317        let mut spec: ManifestPackageSpec = if let Some(spec) = package.metadata.get("simics") {
318            from_value(spec.clone()).map_err(Error::from)?
319        } else {
320            ManifestPackageSpec::default()
321        };
322
323        if spec.package_number.is_none() {
324            // Zero is a safe default for package number, but it is not a valid package number
325            // so a real package must obtain a package number when it is published.
326            spec.package_number = Some(0);
327        }
328
329        if spec.package_name.is_none() {
330            spec.package_name = Some(package.name.clone());
331        }
332
333        if spec.name.is_none() {
334            spec.name = Some(package.name.clone());
335        }
336
337        if spec.description.is_none() {
338            spec.description = package.description.clone();
339        }
340
341        if spec.host.is_none() {
342            spec.host = Some(HOST_DIRNAME.to_string());
343        }
344
345        if spec.version.is_none() {
346            spec.version = Some(package.version.to_string());
347        }
348
349        if spec.build_id.is_none() {
350            spec.build_id = Some(
351                package
352                    .version
353                    .to_string()
354                    .chars()
355                    .filter(|c| c.is_numeric())
356                    .collect::<String>()
357                    .parse()
358                    .map_err(Error::from)?,
359            )
360        }
361
362        if spec.build_id_namespace.is_none() {
363            spec.build_id_namespace = Some(package.name.clone());
364        }
365
366        if spec.confidentiality.is_none() {
367            spec.confidentiality = Some("Public".to_string());
368        }
369
370        if spec.typ.is_none() {
371            spec.typ = Some("addon".to_string());
372        }
373
374        if spec.doc_title.is_none() {
375            spec.doc_title = Some(package.name.clone());
376        }
377
378        if let Ok(package_name) = var("SIMICS_PACKAGE_PACKAGE_NAME") {
379            spec.package_name = Some(package_name);
380        }
381
382        if let Ok(package_number) = var("SIMICS_PACKAGE_PACKAGE_NUMBER") {
383            spec.package_number = Some(package_number.parse().map_err(Error::from)?);
384        }
385
386        if let Ok(package_name) = var("SIMICS_PACKAGE_NAME") {
387            spec.name = Some(package_name);
388        }
389
390        if let Ok(description) = var("SIMICS_PACKAGE_DESCRIPTION") {
391            spec.description = Some(description);
392        }
393
394        if let Ok(host) = var("SIMICS_PACKAGE_HOST") {
395            spec.host = Some(host);
396        }
397
398        if let Ok(version) = var("SIMICS_PACKAGE_VERSION") {
399            spec.version = Some(version);
400        }
401
402        if let Ok(build_id) = var("SIMICS_PACKAGE_BUILD_ID") {
403            spec.build_id = Some(build_id.parse().map_err(Error::from)?);
404        }
405
406        if let Ok(build_id_namespace) = var("SIMICS_PACKAGE_BUILD_ID_NAMESPACE") {
407            spec.build_id_namespace = Some(build_id_namespace);
408        }
409
410        if let Ok(confidentiality) = var("SIMICS_PACKAGE_CONFIDENTIALITY") {
411            spec.confidentiality = Some(confidentiality);
412        }
413
414        if let Ok(typ) = var("SIMICS_PACKAGE_TYPE") {
415            spec.typ = Some(typ);
416        }
417
418        if let Ok(doc_title) = var("SIMICS_PACKAGE_DOC_TITLE") {
419            spec.doc_title = Some(doc_title);
420        }
421
422        Ok(spec)
423    }
424
425    /// Read the manifest specified by the subcommand and parse it into a package specification.
426    pub fn from_subcommand(subcommand: &Subcommand) -> Result<Self> {
427        Self::from_package(
428            MetadataCommand::new()
429                .manifest_path(subcommand.manifest())
430                .no_deps()
431                .exec()?
432                .packages
433                .iter()
434                .find(|p| p.name == subcommand.package())
435                .ok_or_else(|| Error::PackageNotFound {
436                    name: subcommand.package().to_string(),
437                })?,
438        )
439    }
440}
441
442#[derive(Debug, Clone, Serialize, Deserialize)]
443/// A list of package specifications. This data structure can be written to a package-specs.json
444/// file and consumed by Simics packaging utilities.
445pub struct PackageSpecs(pub Vec<PackageSpec>);
446
447impl PackageSpecs {
448    /// Generate the list of specifications from a subcommand input
449    pub fn from_subcommand(subcommand: &Subcommand) -> Result<Self> {
450        Ok(Self(vec![PackageSpec::from_subcommand(subcommand)?]))
451    }
452}
453
454#[derive(Debug, Clone, Serialize, Deserialize)]
455/// Output format for the ispm-metadata file at the top-level of the package.
456/// It contains a subset of the package spec information
457pub struct IspmMetadata {
458    /// The human-readable name of the package
459    pub name: String,
460    #[serde(rename = "packageNumber")]
461    /// The package number
462    pub package_number: isize,
463    /// The package version
464    pub version: String,
465    #[serde(rename = "packageName")]
466    /// The package name, which should be Camel-Kebab-Cased.
467    pub package_name: String,
468    /// The package kind, typically "addon"
469    pub kind: String,
470    /// The host supporting this package, either linux64 or win64
471    pub host: String,
472    /// The confidentiality setting of this package
473    pub confidentiality: String,
474    #[serde(rename = "buildId")]
475    /// The build ID of this package
476    pub build_id: String,
477    #[serde(rename = "buildIdNamespace")]
478    /// The namespace for which the build ID of this package is valid
479    pub build_id_namespace: String,
480    /// The description of this package
481    pub description: String,
482    #[serde(rename = "uncompressedSize")]
483    /// The size of the inner package.tar.gz file as given by du -sb <dir>
484    pub uncompressed_size: usize,
485}
486
487impl From<&PackageSpec> for IspmMetadata {
488    fn from(value: &PackageSpec) -> Self {
489        let value = value.clone();
490        Self {
491            name: value.name,
492            package_number: value.package_number,
493            version: value.version,
494            package_name: value.package_name,
495            kind: value.typ,
496            host: value.host,
497            confidentiality: value.confidentiality,
498            build_id: value.build_id.to_string(),
499            build_id_namespace: value.build_id_namespace,
500            description: value.description,
501            uncompressed_size: 0,
502        }
503    }
504}
505
506#[derive(Debug, Clone, Serialize, Deserialize, Default)]
507/// The package info file, which is a subset of the package spec and is added into the
508/// inner tarball at /package-dir-name/packageinfo/full_package_name
509pub struct PackageInfo {
510    /// The human-readable name of the package
511    pub name: String,
512    /// The description of the package
513    pub description: String,
514    /// The version of the package
515    pub version: String,
516    /// The host supporting this package, either linux64 or win64
517    pub host: String,
518    #[serde(rename = "package-name")]
519    /// The package name, which should be Camel-Kebab-Cased.
520    pub package_name: String,
521    #[serde(rename = "package-number")]
522    /// The package number
523    pub package_number: isize,
524    #[serde(rename = "build-id")]
525    /// The build ID of this package
526    pub build_id: isize,
527    #[serde(rename = "build-id-namespace")]
528    /// The namespace for which the build ID of this package is valid
529    pub build_id_namespace: String,
530    #[serde(rename = "type")]
531    /// The package kind, typically "addon"
532    pub typ: String,
533    #[serde(rename = "extra-version", default)]
534    /// An extra version string, usually empty
535    pub extra_version: String,
536    /// The confidentiality setting of this package
537    pub confidentiality: String,
538    #[serde(skip)]
539    // Files are skipped when serializing and must be serialized separately because the output
540    // format is not exactly YAML: it needs to output like:
541    // files:
542    //     top-level/file1
543    //     top-level/file2
544    //     top-level/dir1/file3
545    /// A list of files present in the package
546    pub files: Vec<String>,
547}
548
549impl From<&PackageSpec> for PackageInfo {
550    fn from(value: &PackageSpec) -> Self {
551        let dirname = format!("simics-{}-{}", value.package_name, value.version);
552        let self_file = PathBuf::from(dirname)
553            .join("packageinfo")
554            .join(format!("{}-{}", value.package_name, value.host));
555        Self {
556            name: value.name.clone(),
557            description: value.description.clone(),
558            version: value.version.clone(),
559            host: value.host.clone(),
560            package_name: value.package_name.clone(),
561            package_number: value.package_number,
562            build_id: value.build_id,
563            build_id_namespace: value.build_id_namespace.clone(),
564            typ: value.typ.clone(),
565            confidentiality: value.confidentiality.clone(),
566            files: value
567                .files
568                .iter()
569                .map(|f| f.0.clone())
570                .chain(once(self_file.to_str().unwrap_or_default().to_string()))
571                .collect(),
572            ..Default::default()
573        }
574    }
575}
576
577impl PackageInfo {
578    /// Get the list of files for this package info file. Because the file is not exactly YAML,
579    /// deserializing the `files` list returns a list like:
580    /// files:
581    /// - file1
582    /// - dir1/file2
583    ///
584    /// But it must actually be formatted like:
585    // files:
586    //     top-level/file1
587    //     top-level/file2
588    //     top-level/dir1/file3
589    ///
590    /// This method returns in the second format.
591    pub fn files(&self) -> String {
592        "files:\n".to_string()
593            + &self
594                .files
595                .iter()
596                .map(|f| format!("    {}", f))
597                .collect::<Vec<String>>()
598                .join("\n")
599            + "\n"
600    }
601}