Skip to main content

opys_dev/
contribution.rs

1//! What a plugin hands back to the engine.
2
3use std::collections::HashMap;
4
5use opys_bundle::{blob_id, blob_id_of, BlobSource, Blobs};
6use opys_core::{encode_short_ruleset, Artifact, MojangRuleset, Source, Val, ValDefs, Valset};
7use serde::{Deserialize, Serialize};
8use serde_json::{Map, Value};
9
10/// An artifact as a plugin hands it over: a manifest's own, except that its
11/// `source` may also say where the bytes are on this machine — `{ file }`,
12/// or `{ bytes }` a plugin made.
13///
14/// A manifest cannot say either, so neither outlives the build: [`resolve`]
15/// reads the bytes, and what comes back is an [`Artifact`] that names them
16/// as a blob, with the bytes beside it for whoever writes the bundle. There
17/// is no table for a plugin to keep and no id for it to compute.
18///
19/// Everything but the source is kept as written and decoded by the
20/// manifest's own reader when the artifact is resolved, so a build-time
21/// artifact has no second spelling of `rules`, `extract` or anything else.
22///
23/// [`resolve`]: BuildArtifact::resolve
24#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
25#[serde(try_from = "Map<String, Value>", into = "Map<String, Value>")]
26pub struct BuildArtifact {
27    /// Every field but `source`, as written.
28    fields: Map<String, Value>,
29    source: BuildSource,
30}
31
32/// Where a build-time artifact's bytes are.
33#[derive(Debug, Clone, PartialEq, Eq)]
34enum BuildSource {
35    /// As a manifest says it: a `url`, or a blob already named.
36    Named(Source),
37    /// On this machine, to be carried in the bundle.
38    Carried(BlobSource),
39}
40
41#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
42pub enum BuildArtifactError {
43    #[error("an artifact has no `source`")]
44    NoSource,
45    #[error(
46        "an artifact's source is `{{ url }}`, `{{ blob }}`, `{{ file }}` or `{{ bytes }}`: {0}"
47    )]
48    Source(String),
49    #[error("cannot read {path}: {reason}")]
50    File { path: String, reason: String },
51    #[error("{0}")]
52    Artifact(String),
53}
54
55impl TryFrom<Map<String, Value>> for BuildArtifact {
56    type Error = BuildArtifactError;
57
58    fn try_from(mut fields: Map<String, Value>) -> Result<Self, Self::Error> {
59        let source = fields
60            .remove("source")
61            .ok_or(BuildArtifactError::NoSource)?;
62        let carried = source
63            .as_object()
64            .is_some_and(|s| s.contains_key("file") || s.contains_key("bytes"));
65        let wrong = |e: serde_json::Error| BuildArtifactError::Source(e.to_string());
66        let source = if carried {
67            BuildSource::Carried(serde_json::from_value(source).map_err(wrong)?)
68        } else {
69            BuildSource::Named(serde_json::from_value(source).map_err(wrong)?)
70        };
71        Ok(BuildArtifact { fields, source })
72    }
73}
74
75impl From<BuildArtifact> for Map<String, Value> {
76    fn from(artifact: BuildArtifact) -> Self {
77        let source = match &artifact.source {
78            BuildSource::Named(source) => serde_json::to_value(source),
79            BuildSource::Carried(source) => serde_json::to_value(source),
80        };
81        let mut fields = artifact.fields;
82        // Both are maps of strings, which always serialise.
83        fields.insert("source".to_owned(), source.unwrap_or(Value::Null));
84        fields
85    }
86}
87
88impl From<Artifact> for BuildArtifact {
89    fn from(artifact: Artifact) -> Self {
90        let source = artifact.source.clone();
91        let mut fields = match serde_json::to_value(artifact) {
92            Ok(Value::Object(fields)) => fields,
93            // An artifact serialises as an object, always.
94            _ => Map::new(),
95        };
96        fields.remove("source");
97        BuildArtifact {
98            fields,
99            source: BuildSource::Named(source),
100        }
101    }
102}
103
104impl BuildArtifact {
105    fn carried_at(path: &str, source: BlobSource) -> Self {
106        let mut fields = Map::new();
107        fields.insert("path".to_owned(), Value::String(path.to_owned()));
108        BuildArtifact {
109            fields,
110            source: BuildSource::Carried(source),
111        }
112    }
113
114    /// A file on this machine, installed at `path`.
115    pub fn file(path: &str, file: impl Into<std::path::PathBuf>) -> Self {
116        Self::carried_at(path, BlobSource::File(file.into()))
117    }
118
119    /// Bytes a plugin made, installed at `path`.
120    pub fn bytes(path: &str, bytes: Vec<u8>) -> Self {
121        Self::carried_at(path, BlobSource::Bytes(bytes))
122    }
123
124    /// The same artifact, installed only where `rules` pass.
125    pub fn with_rules(mut self, rules: &MojangRuleset) -> Self {
126        if !rules.is_empty() {
127            // The spelling a manifest's own artifact is written in.
128            if let Ok(rules) = serde_json::to_value(encode_short_ruleset(rules)) {
129                self.fields.insert("rules".to_owned(), rules);
130            }
131        }
132        self
133    }
134
135    /// The same artifact, with free data for whoever reads the manifest.
136    pub fn with_metadata(mut self, metadata: Option<&Value>) -> Self {
137        if let Some(metadata) = metadata {
138            self.fields.insert("metadata".to_owned(), metadata.clone());
139        }
140        self
141    }
142
143    /// Where it is installed, as written.
144    pub fn path(&self) -> Option<&str> {
145        self.fields.get("path").and_then(Value::as_str)
146    }
147
148    /// Artifacts that name blobs, with the bytes of each put back on the
149    /// artifact itself. For a producer that has already hashed what it
150    /// carries and holds the two apart.
151    pub fn carrying(artifacts: Vec<Artifact>, blobs: &Blobs) -> Vec<BuildArtifact> {
152        artifacts
153            .into_iter()
154            .map(|artifact| {
155                let held = artifact.blob_id().and_then(|id| blobs.get(id)).cloned();
156                let mut built = BuildArtifact::from(artifact);
157                if let Some(source) = held {
158                    built.source = BuildSource::Carried(source);
159                }
160                built
161            })
162            .collect()
163    }
164
165    /// The manifest's artifact, and — for one that is carried — the id of
166    /// its blob and where the bytes are. The one impure step: a file is read
167    /// to be named by its content.
168    pub fn resolve(self) -> Result<(Artifact, Option<(String, BlobSource)>), BuildArtifactError> {
169        let mut fields = self.fields;
170        let (source, held) = match self.source {
171            BuildSource::Named(source) => (source, None),
172            BuildSource::Carried(carried) => {
173                let (id, size) = match &carried {
174                    BlobSource::Bytes(bytes) => (blob_id(bytes), bytes.len() as u64),
175                    BlobSource::File(path) => {
176                        let read = |e: std::io::Error| BuildArtifactError::File {
177                            path: path.display().to_string(),
178                            reason: e.to_string(),
179                        };
180                        blob_id_of(std::fs::File::open(path).map_err(read)?).map_err(read)?
181                    }
182                };
183                // The length that was hashed, whatever was written.
184                fields.insert("size".to_owned(), Value::from(size));
185                (Source::Blob { blob: id.clone() }, Some((id, carried)))
186            }
187        };
188        let source = serde_json::to_value(source)
189            .map_err(|e| BuildArtifactError::Artifact(e.to_string()))?;
190        fields.insert("source".to_owned(), source);
191        let artifact = serde_json::from_value(Value::Object(fields))
192            .map_err(|e| BuildArtifactError::Artifact(e.to_string()))?;
193        Ok((artifact, held))
194    }
195}
196
197/// One named launch fragment a plugin exposes — `jvmArgs` (a `Valset`),
198/// `mainClass` (a `Val`), `bin` (a bare string). A config names one on its
199/// launch line as `@plugin.group`, and [`crate::assemble`] puts it there.
200///
201/// Untagged on the wire, and the order matters: a bare string is matched
202/// before `One`, whose own bare-string form would otherwise swallow it.
203#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
204#[serde(untagged)]
205pub enum LaunchFragment {
206    Text(String),
207    One(Val),
208    Many(Valset),
209}
210
211/// A plugin's named launch fragments, looked up by name and never iterated
212/// for output — hence an unordered map.
213pub type LaunchGroups = HashMap<String, LaunchFragment>;
214
215/// A plugin's build output. Every field is optional in spirit; an empty
216/// default contributes nothing.
217#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
218#[serde(default)]
219pub struct Contribution {
220    /// Artifacts to download/copy/extract. One whose bytes are on this
221    /// machine says so in its own `source`.
222    pub artifacts: Vec<BuildArtifact>,
223    /// Manifest vars this plugin owns.
224    pub vars: ValDefs,
225    /// Named launch fragments, which a config references by name.
226    pub launch: LaunchGroups,
227    /// Launch environment variables this plugin sets by default.
228    pub envs: ValDefs,
229}
230
231/// One plugin's output, tagged with the plugin that produced it. The name is
232/// what collision warnings point at, so it travels with the contribution
233/// rather than being recovered from a parallel list.
234#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
235pub struct PluginOutput {
236    pub name: String,
237    #[serde(default)]
238    pub contribution: Contribution,
239}