Skip to main content

nu_cmd_lang/core_commands/
version.rs

1use std::{borrow::Cow, sync::OnceLock};
2
3use itertools::Itertools;
4use nu_engine::command_prelude::*;
5use nu_protocol::engine::StateWorkingSet;
6use shadow_rs::shadow;
7
8shadow!(build);
9
10/// Static container for the version shown by the `version` command.
11///
12/// By default, this falls back to `nu_cmd_lang`'s own crate version
13/// (`env!("CARGO_PKG_VERSION")`).
14///
15/// If you're embedding Nushell (or wiring `nu_cmd_lang` into another binary),
16/// set this static before calling `version` so it reports your host binary's version.
17///
18/// ```no_run
19/// let version = env!("CARGO_PKG_VERSION")
20///     .parse()
21///     .expect("cargo sets valid version");
22///
23/// nu_cmd_lang::VERSION
24///     .set(version)
25///     .expect("VERSION is unset");
26/// ```
27pub static VERSION: OnceLock<semver::Version> = OnceLock::new();
28
29/// Static container for the cargo features used by the `version` command.
30///
31/// This `OnceLock` holds the features from `nu`.
32/// When you build `nu_cmd_lang`, Cargo doesn't pass along the same features that `nu` itself uses.
33/// By setting this static before calling `version`, you make it show `nu`'s features instead
34/// of `nu_cmd_lang`'s.
35///
36/// Embedders can set this to any feature list they need, but in most cases you'll probably want to
37/// pass the cargo features of your host binary.
38///
39/// # How to get cargo features in your build script
40///
41/// In your binary's build script:
42/// ```no_run
43/// // Re-export CARGO_CFG_FEATURE to the main binary.
44/// // It holds all the features that cargo sets for your binary as a comma-separated list.
45/// println!(
46///     "cargo:rustc-env=NU_FEATURES={}",
47///     std::env::var("CARGO_CFG_FEATURE").expect("set by cargo")
48/// );
49/// ```
50///
51/// Then, before you call `version`:
52/// ```ignore
53/// // This uses static strings, but since we're using `Cow`, you can also pass owned strings.
54/// let features = env!("NU_FEATURES")
55///     .split(',')
56///     .map(Cow::Borrowed)
57///     .collect();
58///
59/// nu_cmd_lang::VERSION_NU_FEATURES
60///     .set(features)
61///     .expect("couldn't set VERSION_NU_FEATURES");
62/// ```
63pub static VERSION_NU_FEATURES: OnceLock<Vec<Cow<'static, str>>> = OnceLock::new();
64
65#[derive(Clone)]
66pub struct Version;
67
68impl Command for Version {
69    fn name(&self) -> &str {
70        "version"
71    }
72
73    fn signature(&self) -> Signature {
74        Signature::build("version")
75            .input_output_types(vec![(Type::Nothing, Type::record())])
76            .allow_variants_without_examples(true)
77            .category(Category::Core)
78    }
79
80    fn description(&self) -> &str {
81        "Display Nu version, and its build configuration."
82    }
83
84    fn is_const(&self) -> bool {
85        true
86    }
87
88    fn run(
89        &self,
90        engine_state: &EngineState,
91        _stack: &mut Stack,
92        call: &Call,
93        _input: PipelineData,
94    ) -> Result<PipelineData, ShellError> {
95        version(engine_state, call.head)
96    }
97
98    fn run_const(
99        &self,
100        working_set: &StateWorkingSet,
101        call: &Call,
102        _input: PipelineData,
103    ) -> Result<PipelineData, ShellError> {
104        version(working_set.permanent(), call.head)
105    }
106
107    fn examples(&self) -> Vec<Example<'_>> {
108        vec![Example {
109            description: "Display Nu version.",
110            example: "version",
111            result: None,
112        }]
113    }
114}
115
116fn push_non_empty(record: &mut Record, name: &str, value: &str, span: Span) {
117    if !value.is_empty() {
118        record.push(name, Value::string(value, span))
119    }
120}
121
122fn load_version() -> Cow<'static, semver::Version> {
123    match VERSION.get() {
124        Some(version) => Cow::Borrowed(version),
125        None => Cow::Owned(
126            semver::Version::parse(env!("CARGO_PKG_VERSION")).expect("cargo sets valid version"),
127        ),
128    }
129}
130
131pub fn version(engine_state: &EngineState, span: Span) -> Result<PipelineData, ShellError> {
132    // Pre-allocate the arrays in the worst case:
133    const VERSION_MAX_ROWS: usize = [
134        "version",
135        "major",
136        "minor",
137        "patch",
138        "pre",
139        "build",
140        "branch",
141        "commit_hash",
142        "build_os",
143        "build_target",
144        "rust_version",
145        "rust_channel",
146        "cargo_version",
147        "build_time",
148        "build_rust_channel",
149        "allocator",
150        "features",
151        "installed_plugins",
152    ]
153    .len();
154
155    let mut record = Record::with_capacity(VERSION_MAX_ROWS);
156
157    let version = load_version();
158    record.push("version", Value::string(version.to_string(), span));
159    record.push("major", Value::int(version.major as i64, span));
160    record.push("minor", Value::int(version.minor as i64, span));
161    record.push("patch", Value::int(version.patch as i64, span));
162    push_non_empty(&mut record, "pre", version.pre.as_str(), span);
163    push_non_empty(&mut record, "build", version.build.as_str(), span);
164
165    record.push("branch", Value::string(build::BRANCH, span));
166
167    if let Some(commit_hash) = option_env!("NU_COMMIT_HASH") {
168        record.push("commit_hash", Value::string(commit_hash, span));
169    }
170
171    push_non_empty(&mut record, "build_os", build::BUILD_OS, span);
172    push_non_empty(&mut record, "build_target", build::BUILD_TARGET, span);
173    push_non_empty(&mut record, "rust_version", build::RUST_VERSION, span);
174    push_non_empty(&mut record, "rust_channel", build::RUST_CHANNEL, span);
175    push_non_empty(&mut record, "cargo_version", build::CARGO_VERSION, span);
176    push_non_empty(&mut record, "build_time", build::BUILD_TIME, span);
177    push_non_empty(
178        &mut record,
179        "build_rust_channel",
180        build::BUILD_RUST_CHANNEL,
181        span,
182    );
183
184    record.push("allocator", Value::string(global_allocator(), span));
185
186    record.push(
187        "features",
188        Value::string(
189            VERSION_NU_FEATURES
190                .get()
191                .as_ref()
192                .map(|v| v.as_slice())
193                .unwrap_or_default()
194                .iter()
195                .filter(|f| !f.starts_with("dep:"))
196                .join(", "),
197            span,
198        ),
199    );
200
201    #[cfg(not(feature = "plugin"))]
202    let _ = engine_state;
203
204    #[cfg(feature = "plugin")]
205    {
206        // Get a list of plugin names and versions if present
207        let installed_plugins = engine_state
208            .plugins()
209            .iter()
210            .map(|x| {
211                let name = x.identity().name();
212                if let Some(version) = x.metadata().and_then(|m| m.version) {
213                    format!("{name} {version}")
214                } else {
215                    name.into()
216                }
217            })
218            .collect::<Vec<_>>();
219
220        record.push(
221            "installed_plugins",
222            Value::string(installed_plugins.join(", "), span),
223        );
224    }
225
226    record.push(
227        "experimental_options",
228        Value::string(
229            nu_experimental::ALL
230                .iter()
231                .map(|option| format!("{}={}", option.identifier(), option.get()))
232                .join(", "),
233            span,
234        ),
235    );
236
237    Ok(Value::record(record, span).into_pipeline_data())
238}
239
240fn global_allocator() -> &'static str {
241    "standard"
242}
243
244#[cfg(test)]
245mod test {
246    #[test]
247    fn test_examples() -> nu_test_support::Result {
248        use super::Version;
249        nu_test_support::test().examples(Version)
250    }
251}