spack/
metadata_spec.rs

1/* Copyright 2022-2023 Danny McClanahan */
2/* SPDX-License-Identifier: (Apache-2.0 OR MIT) */
3
4pub mod spec {
5  use base64ct::{Base64Url, Encoding};
6  use displaydoc::Display;
7  use indexmap::{IndexMap, IndexSet};
8  use serde::Deserialize;
9  use sha3::{Digest, Sha3_256};
10  use thiserror::Error;
11
12  use std::{path::PathBuf, str};
13
14  #[derive(Debug, Clone, Deserialize)]
15  pub struct Dep {
16    pub r#type: String,
17    pub lib_names: Vec<String>,
18  }
19
20  #[derive(Debug, Clone, Deserialize)]
21  pub struct FeatureLayout {
22    pub needed: Option<IndexSet<String>>,
23    pub conflicting: Option<IndexSet<String>>,
24  }
25
26  #[derive(Debug, Clone, Deserialize)]
27  pub struct Env {
28    pub spec: String,
29    pub deps: IndexMap<String, Dep>,
30    pub features: Option<FeatureLayout>,
31  }
32
33  #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize)]
34  pub struct Repo {
35    pub path: PathBuf,
36  }
37
38  /// This is deserialized from the output of `cargo metadata --format-version
39  /// 1` with [`serde_json`].
40  ///
41  /// Cargo packages can declare these attributes in their `Cargo.toml` in the
42  /// `[package.metadata.spack]` section as follows:
43  ///```toml
44  /// [package.metadata.spack]
45  /// envs = [{
46  ///   label = "re2",
47  ///   spec = "re2@2023-11-01+shared",
48  ///   deps = { re2 = { type = "dynamic", lib_names = ["re2"] } }
49  /// }]
50  /// ```
51  #[derive(Debug, Clone, Deserialize)]
52  pub struct LabelledPackageMetadata {
53    pub envs: IndexMap<String, Env>,
54    pub repo: Option<Repo>,
55  }
56
57  #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
58  pub struct Label(pub String);
59
60  #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
61  pub struct Spec(pub String);
62
63  /// Name of a package from the [`Spec`] resolver.
64  #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
65  pub struct PackageName(pub String);
66
67  /// Name of a cargo package.
68  #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
69  pub struct CrateName(pub String);
70
71  #[derive(Debug, Display, Error)]
72  pub enum SpecError {
73    /// error parsing: {0}
74    Parsing(String),
75  }
76
77  #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
78  pub enum LibraryType {
79    /// link against this library statically
80    Static,
81    /// link against this library dynamically, and set -Wl,-rpath
82    DynamicWithRpath,
83  }
84
85  impl str::FromStr for LibraryType {
86    type Err = SpecError;
87
88    fn from_str(s: &str) -> Result<Self, SpecError> {
89      match s {
90        "static" => Ok(Self::Static),
91        "dynamic" => Ok(Self::DynamicWithRpath),
92        s => Err(SpecError::Parsing(format!(
93          "dep type only accepts \"static\" or \"dynamic\"; was {:?}",
94          s
95        ))),
96      }
97    }
98  }
99
100  #[derive(Debug, Clone)]
101  pub struct SubDep {
102    pub pkg_name: PackageName,
103    pub r#type: LibraryType,
104    pub lib_names: Vec<String>,
105  }
106
107  #[derive(Debug, Clone)]
108  pub struct Recipe {
109    pub sub_deps: Vec<SubDep>,
110  }
111
112  #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
113  pub struct EnvInstructions {
114    pub specs: Vec<Spec>,
115    pub repo: Option<Repo>,
116  }
117
118  #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
119  #[repr(transparent)]
120  pub struct EnvHash(pub [u8; 32]);
121
122  impl EnvHash {
123    fn base64_encoded(&self) -> String { Base64Url::encode_string(&self.0[..]) }
124
125    pub fn hashed_env_name(&self, readable_name: &str) -> String {
126      format!(
127        "{}-{}",
128        readable_name,
129        self.base64_encoded().strip_suffix('=').unwrap()
130      )
131    }
132  }
133
134  impl EnvInstructions {
135    pub fn compute_digest(&self) -> EnvHash {
136      let mut hasher = Sha3_256::new();
137      for Spec(s) in self.specs.iter() {
138        hasher.update(s);
139      }
140      if let Some(ref repo) = self.repo {
141        hasher.update(repo.path.as_os_str().as_encoded_bytes());
142      }
143      EnvHash(hasher.finalize().into())
144    }
145  }
146
147  #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
148  pub struct CargoFeature(pub String);
149
150  impl CargoFeature {
151    /// This [environment variable] will be set in the current package's build
152    /// script if the feature is activated.
153    ///
154    /// [environment variable]: https://doc.rust-lang.org/cargo/reference/environment-variables.html.
155    pub fn to_env_var_name(&self) -> String {
156      format!("CARGO_FEATURE_{}", self.0.to_uppercase().replace('-', "_"))
157    }
158  }
159
160  #[derive(Debug, Clone, Default)]
161  pub struct FeatureMap {
162    pub needed: IndexSet<CargoFeature>,
163    pub conflicting: IndexSet<CargoFeature>,
164  }
165
166  impl FeatureMap {
167    pub fn evaluate(&self, features: &FeatureSet) -> bool {
168      let Self {
169        needed,
170        conflicting,
171      } = self;
172      for n in needed.iter() {
173        if !features.0.contains(n) {
174          return false;
175        }
176      }
177      for c in conflicting.iter() {
178        if features.0.contains(c) {
179          return false;
180        }
181      }
182      true
183    }
184  }
185
186  #[derive(Debug, Clone, Default)]
187  pub struct FeatureSet(pub IndexSet<CargoFeature>);
188
189  #[derive(Debug, Clone)]
190  pub struct DisjointResolves {
191    pub by_label: IndexMap<Label, EnvInstructions>,
192    pub recipes: IndexMap<CrateName, IndexMap<Label, (Recipe, FeatureMap)>>,
193    pub declared_features_by_package: IndexMap<CrateName, Vec<CargoFeature>>,
194  }
195}