Skip to main content

cuenv_cas/
message.rs

1//! Action / Command / Directory / ActionResult messages.
2//!
3//! These mirror the equivalent protobuf messages from the Bazel Remote
4//! Execution API v2. We use plain serde-serialized structs for now so the
5//! crate stays tonic-free; when the remote backend lands we switch to the
6//! `bazel-remote-apis` generated types and these become conversion targets.
7
8use crate::digest::Digest;
9use chrono::{DateTime, Utc};
10use serde::{Deserialize, Serialize};
11use std::collections::BTreeMap;
12
13/// Execution platform properties (CPU, OS, tooling version, etc.).
14///
15/// Mirrors `build.bazel.remote.execution.v2.Platform`.
16#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
17pub struct Platform {
18    /// Key/value properties. Stored in a `BTreeMap` for canonical ordering.
19    pub properties: BTreeMap<String, String>,
20}
21
22/// A command to execute inside an action.
23///
24/// Mirrors `build.bazel.remote.execution.v2.Command`.
25#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
26pub struct Command {
27    /// Argv vector. `arguments[0]` is the executable.
28    pub arguments: Vec<String>,
29    /// Environment variables visible to the command.
30    pub environment_variables: BTreeMap<String, String>,
31    /// Files the action is expected to produce, relative to the working dir.
32    pub output_files: Vec<String>,
33    /// Directories the action is expected to produce, relative to the working dir.
34    pub output_directories: Vec<String>,
35    /// Working directory relative to the input root.
36    pub working_directory: String,
37}
38
39/// A file inside a [`Directory`].
40#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
41pub struct FileNode {
42    /// Base file name (no path separators).
43    pub name: String,
44    /// Content digest in the CAS.
45    pub digest: Digest,
46    /// Whether the file should be executable when materialized.
47    pub is_executable: bool,
48}
49
50/// A subdirectory entry inside a [`Directory`].
51#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
52pub struct DirectoryNode {
53    /// Base directory name (no path separators).
54    pub name: String,
55    /// Digest of the sub-`Directory` message in the CAS.
56    pub digest: Digest,
57}
58
59/// A symlink entry inside a [`Directory`].
60#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
61pub struct SymlinkNode {
62    /// Base name of the symlink.
63    pub name: String,
64    /// Raw symlink target.
65    pub target: String,
66}
67
68/// A Merkle-tree directory message.
69///
70/// Mirrors `build.bazel.remote.execution.v2.Directory`. Children are stored
71/// in sorted order so the canonical encoding is stable.
72#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
73pub struct Directory {
74    /// Files in this directory, sorted by `name`.
75    pub files: Vec<FileNode>,
76    /// Subdirectories, sorted by `name`.
77    pub directories: Vec<DirectoryNode>,
78    /// Symlinks, sorted by `name`.
79    pub symlinks: Vec<SymlinkNode>,
80}
81
82/// An action to execute.
83///
84/// Mirrors `build.bazel.remote.execution.v2.Action`. The [`Digest`] of this
85/// struct (under canonical encoding) is the action cache key.
86#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
87pub struct Action {
88    /// Digest of the [`Command`] to run.
89    pub command_digest: Digest,
90    /// Digest of the root [`Directory`] forming the input tree.
91    pub input_root_digest: Digest,
92    /// Execution platform.
93    pub platform: Platform,
94    /// cuenv-specific salt. Bumping this invalidates every entry; useful
95    /// when the execution semantics of cuenv itself change.
96    pub cuenv_version: String,
97}
98
99/// A file produced by an action.
100///
101/// Mirrors `build.bazel.remote.execution.v2.OutputFile`.
102#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
103pub struct OutputFile {
104    /// Path relative to the action's working directory.
105    pub path: String,
106    /// Content digest.
107    pub digest: Digest,
108    /// Executable bit.
109    pub is_executable: bool,
110}
111
112/// A directory produced by an action, stored as a digest of a [`Directory`]
113/// Merkle tree.
114///
115/// Mirrors `build.bazel.remote.execution.v2.OutputDirectory`.
116#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
117pub struct OutputDirectory {
118    /// Path relative to the action's working directory.
119    pub path: String,
120    /// Digest of the root [`Directory`] describing the tree.
121    pub tree_digest: Digest,
122}
123
124/// Metadata captured while the action executed.
125#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
126pub struct ExecutionMetadata {
127    /// Identifier of the worker that ran the action ("local", hostname, etc).
128    pub worker: String,
129    /// Wall-clock duration in milliseconds.
130    pub duration_ms: u128,
131    /// UTC timestamp when the result was recorded.
132    pub created_at: DateTime<Utc>,
133}
134
135impl Default for ExecutionMetadata {
136    fn default() -> Self {
137        Self {
138            worker: String::new(),
139            duration_ms: 0,
140            created_at: DateTime::<Utc>::from_timestamp(0, 0).unwrap_or_else(Utc::now),
141        }
142    }
143}
144
145/// The result of executing an action.
146///
147/// Mirrors `build.bazel.remote.execution.v2.ActionResult`.
148#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
149pub struct ActionResult {
150    /// Output files produced by the action.
151    pub output_files: Vec<OutputFile>,
152    /// Output directories produced by the action.
153    pub output_directories: Vec<OutputDirectory>,
154    /// Exit code of the command.
155    pub exit_code: i32,
156    /// Digest of stdout in the CAS, if captured.
157    pub stdout_digest: Option<Digest>,
158    /// Digest of stderr in the CAS, if captured.
159    pub stderr_digest: Option<Digest>,
160    /// Execution metadata.
161    pub execution_metadata: ExecutionMetadata,
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167    use crate::digest::digest_of;
168
169    #[test]
170    fn action_digest_is_order_invariant_on_platform_properties() {
171        let mut a_props = BTreeMap::new();
172        a_props.insert("os".into(), "linux".into());
173        a_props.insert("arch".into(), "x86_64".into());
174        let a = Action {
175            command_digest: Digest::of_bytes(b"cmd"),
176            input_root_digest: Digest::of_bytes(b"root"),
177            platform: Platform {
178                properties: a_props,
179            },
180            cuenv_version: "0.30.8".into(),
181        };
182
183        let mut b_props = BTreeMap::new();
184        // inserted in different order
185        b_props.insert("arch".into(), "x86_64".into());
186        b_props.insert("os".into(), "linux".into());
187        let b = Action {
188            platform: Platform {
189                properties: b_props,
190            },
191            ..a.clone()
192        };
193
194        assert_eq!(digest_of(&a).unwrap(), digest_of(&b).unwrap());
195    }
196
197    #[test]
198    fn action_digest_changes_with_command_digest() {
199        let base = Action {
200            command_digest: Digest::of_bytes(b"cmd-1"),
201            input_root_digest: Digest::of_bytes(b"root"),
202            platform: Platform::default(),
203            cuenv_version: "0.30.8".into(),
204        };
205        let other = Action {
206            command_digest: Digest::of_bytes(b"cmd-2"),
207            ..base.clone()
208        };
209        assert_ne!(digest_of(&base).unwrap(), digest_of(&other).unwrap());
210    }
211
212    #[test]
213    fn action_digest_changes_with_input_root() {
214        let base = Action {
215            command_digest: Digest::of_bytes(b"cmd"),
216            input_root_digest: Digest::of_bytes(b"root-1"),
217            platform: Platform::default(),
218            cuenv_version: "0.30.8".into(),
219        };
220        let other = Action {
221            input_root_digest: Digest::of_bytes(b"root-2"),
222            ..base.clone()
223        };
224        assert_ne!(digest_of(&base).unwrap(), digest_of(&other).unwrap());
225    }
226
227    #[test]
228    fn action_digest_changes_with_cuenv_version() {
229        let base = Action {
230            command_digest: Digest::of_bytes(b"cmd"),
231            input_root_digest: Digest::of_bytes(b"root"),
232            platform: Platform::default(),
233            cuenv_version: "0.30.8".into(),
234        };
235        let other = Action {
236            cuenv_version: "0.31.0".into(),
237            ..base.clone()
238        };
239        assert_ne!(digest_of(&base).unwrap(), digest_of(&other).unwrap());
240    }
241}