Skip to content

Commit b8b3105

Browse files
authored
feat: implement cmd/version (#117)
* feat: implement cmd/version * fix: implement the dependencies list in cli/version * test: add test for cli/version * fix: address comments
1 parent 82986ef commit b8b3105

9 files changed

Lines changed: 243 additions & 7 deletions

File tree

Cargo.lock

Lines changed: 70 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ publish = false
2424

2525
[workspace.dependencies]
2626
alloy = { version = "1.3", features = ["essentials"] }
27-
built = { version = "0.8.0", features = ["git2", "chrono"] }
27+
built = { version = "0.8.0", features = ["git2", "chrono", "cargo-lock"] }
2828
blst = "0.3"
2929
anyhow = "1"
3030
cancellation = "0.1.0"

crates/charon-cli/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ thiserror.workspace = true
1616
k256.workspace = true
1717
hex.workspace = true
1818
tokio.workspace = true
19+
charon-core.workspace = true
1920
charon-p2p.workspace = true
2021
charon-eth2.workspace = true
2122
charon-k1util.workspace = true

crates/charon-cli/src/cli.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
33
use clap::{Parser, Subcommand};
44

5-
use crate::commands::enr::EnrArgs;
5+
use crate::commands::{enr::EnrArgs, version::VersionArgs};
66

77
/// Pluto - Proof of Stake Ethereum Distributed Validator Client
88
#[derive(Parser)]
@@ -26,8 +26,10 @@ pub enum Commands {
2626
long_about = "Prints an Ethereum Node Record (ENR) from this client's charon-enr-private-key. This serves as a public key that identifies this client to its peers."
2727
)]
2828
Enr(EnrArgs),
29+
30+
#[command(about = "Print version and exit", long_about = "Output version info")]
31+
Version(VersionArgs),
2932
// Future commands will be added here:
30-
// Version(VersionArgs),
3133
// Run(RunArgs),
3234
// Create(CreateArgs),
3335
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
pub mod enr;
2+
pub mod version;
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
use std::io::{self, Write};
2+
3+
use crate::error::Result;
4+
5+
/// Arguments for the version command.
6+
#[derive(clap::Args)]
7+
pub struct VersionArgs {
8+
#[arg(
9+
long = "verbose",
10+
help = "Includes detailed module version info and supported protocols."
11+
)]
12+
pub verbose: bool,
13+
}
14+
15+
/// Runs the version command.
16+
pub fn run(args: VersionArgs) -> Result<()> {
17+
run_with_writer(args, &mut io::stdout())
18+
}
19+
20+
/// Runs the version command with a custom writer (used for testing).
21+
fn run_with_writer<W: Write>(args: VersionArgs, writer: &mut W) -> Result<()> {
22+
let (hash, timestamp) = charon_core::version::git_commit();
23+
writeln!(
24+
writer,
25+
"{} [git_commit_hash={},git_commit_time={}]",
26+
*charon_core::version::VERSION,
27+
hash,
28+
timestamp
29+
)?;
30+
31+
if !args.verbose {
32+
return Ok(());
33+
}
34+
35+
writeln!(writer, "Package: {}", env!("CARGO_PKG_NAME"))?;
36+
writeln!(writer, "Dependencies:")?;
37+
38+
for dependency in charon_core::version::dependencies() {
39+
writeln!(writer, "\t{dependency}")?;
40+
}
41+
42+
writeln!(writer, "Consensus protocols:")?;
43+
for protocol in charon_core::consensus::protocols::protocols() {
44+
writeln!(writer, "\t{}", protocol)?;
45+
}
46+
47+
Ok(())
48+
}
49+
50+
#[cfg(test)]
51+
mod tests {
52+
use super::*;
53+
54+
#[test]
55+
fn test_run_version_cmd_default() {
56+
let mut buf = Vec::new();
57+
let args = VersionArgs { verbose: false };
58+
59+
let result = run_with_writer(args, &mut buf);
60+
assert!(result.is_ok());
61+
62+
let output = String::from_utf8(buf).expect("valid UTF-8 output");
63+
64+
// Check that output contains git info
65+
assert!(
66+
output.contains("git_commit_hash"),
67+
"Output should contain git_commit_hash"
68+
);
69+
assert!(
70+
output.contains("git_commit_time"),
71+
"Output should contain git_commit_time"
72+
);
73+
74+
// Check that verbose-only content is NOT present
75+
assert!(
76+
!output.contains("Package:"),
77+
"Default output should not contain Package:"
78+
);
79+
80+
// Parse the version from output
81+
// Format: "v1.7.1
82+
// [git_commit_hash=abc1234,git_commit_time=2024-01-01T00:00:00Z]\n"
83+
let parts: Vec<&str> = output.split_whitespace().collect();
84+
assert_eq!(
85+
parts.len(),
86+
2,
87+
"Default output should have exactly 2 space-separated parts"
88+
);
89+
90+
// Parse the version string
91+
let version_str = parts[0];
92+
let parsed_version =
93+
charon_core::version::SemVer::parse(version_str).expect("valid semver");
94+
assert_eq!(
95+
parsed_version,
96+
*charon_core::version::VERSION,
97+
"Parsed version should match VERSION constant"
98+
);
99+
}
100+
101+
#[test]
102+
fn test_run_version_cmd_verbose() {
103+
let mut buf = Vec::new();
104+
let args = VersionArgs { verbose: true };
105+
106+
let result = run_with_writer(args, &mut buf);
107+
assert!(result.is_ok());
108+
109+
let output = String::from_utf8(buf).expect("valid UTF-8 output");
110+
111+
// Check that output contains git info
112+
assert!(
113+
output.contains("git_commit_hash"),
114+
"Output should contain git_commit_hash"
115+
);
116+
assert!(
117+
output.contains("git_commit_time"),
118+
"Output should contain git_commit_time"
119+
);
120+
121+
// Check that verbose content is present
122+
assert!(
123+
output.contains("Package:"),
124+
"Verbose output should contain Package:"
125+
);
126+
assert!(
127+
output.contains("Dependencies:"),
128+
"Verbose output should contain Dependencies:"
129+
);
130+
assert!(
131+
output.contains("Consensus protocols:"),
132+
"Verbose output should contain Consensus protocols:"
133+
);
134+
135+
// Check that the first protocol is listed
136+
let protocols = charon_core::consensus::protocols::protocols();
137+
assert!(!protocols.is_empty(), "Should have at least one protocol");
138+
let first_protocol = protocols[0].to_string();
139+
assert!(
140+
output.contains(&first_protocol),
141+
"Verbose output should contain the first protocol: {}",
142+
first_protocol
143+
);
144+
}
145+
}

crates/charon-cli/src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,6 @@ fn main() -> Result<()> {
1818

1919
match cli.command {
2020
Commands::Enr(args) => commands::enr::run(args),
21+
Commands::Version(args) => commands::version::run(args),
2122
}
2223
}

crates/charon-core/build.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use std::io::Result;
77
fn main() -> Result<()> {
88
charon_build_proto::compile_protos("src/corepb/v1")?;
99
built::write_built_file()?;
10+
println!("cargo:rerun-if-changed=../../Cargo.lock");
1011

1112
Ok(())
1213
}

crates/charon-core/src/version.rs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,31 @@ pub fn git_commit() -> (String, String) {
4949
include!(concat!(env!("OUT_DIR"), "/built.rs"));
5050
}
5151

52-
let hash = built_info::GIT_COMMIT_HASH.unwrap_or("unknown").into();
52+
let hash = built_info::GIT_COMMIT_HASH
53+
.map(|h| h.chars().take(7).collect())
54+
.unwrap_or_else(|| "unknown".into());
55+
5356
let timestamp = chrono::DateTime::parse_from_rfc2822(built_info::BUILT_TIME_UTC)
54-
.map(|dt| dt.timestamp().to_string())
57+
.map(|dt| dt.to_rfc3339_opts(chrono::SecondsFormat::Secs, true))
5558
.unwrap_or_else(|_| "unknown".into());
5659

5760
(hash, timestamp)
5861
}
5962

63+
/// Dependency list from build info in `name v{version}` format.
64+
pub fn dependencies() -> Vec<String> {
65+
mod built_info {
66+
include!(concat!(env!("OUT_DIR"), "/built.rs"));
67+
}
68+
69+
let mut deps: Vec<String> = built_info::DEPENDENCIES
70+
.iter()
71+
.map(|(name, version)| format!("{name} v{version}"))
72+
.collect();
73+
deps.sort_unstable();
74+
deps
75+
}
76+
6077
/// The type of semantic version, i.e., minor, patch, or pre-release.
6178
#[derive(Eq, PartialEq, Debug, Copy, Clone)]
6279
pub enum SemVerType {

0 commit comments

Comments
 (0)