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}