feat: Add a module for C projects (#3631)

Co-authored-by: David Knaack <davidkna@users.noreply.github.com>
This commit is contained in:
David Cantrell 2022-03-25 04:10:19 +00:00 committed by GitHub
parent b90d1164f5
commit 0863146f07
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 371 additions and 0 deletions

View File

@ -1,6 +1,9 @@
[aws]
format = '\[[$symbol($profile)(\($region\))(\[$duration\])]($style)\]'
[c]
format = '\[[$symbol($version(-$name))]($style)\]'
[cmake]
format = '\[[$symbol($version)]($style)\]'

View File

@ -4,6 +4,9 @@ symbol = " "
[buf]
symbol = " "
[c]
symbol = " "
[conda]
symbol = " "

View File

@ -16,6 +16,9 @@ deleted = "x"
[aws]
symbol = "aws "
[c]
symbol = "C "
[cobol]
symbol = "cobol "

View File

@ -207,6 +207,7 @@ $hg_branch\
$docker_context\
$package\
$buf\
$c\
$cmake\
$cobol\
$container\
@ -496,6 +497,54 @@ The `buf` module shows the currently installed version of [Buf](https://buf.buil
symbol = "🦬 "
```
## C
The `c` module shows some information about your C compiler. By default
the module will be shown if the current directory contains a `.c` or `.h`
file.
### Options
| Option | Default | Description |
| ------------------- | --------------------------------------------------------------------------- | ------------------------------------------------------------------------- |
| `format` | `"via [$symbol($version(-$name) )]($style)"` | The format string for the module. |
| `version_format` | `"v${raw}"` | The version format. Available vars are `raw`, `major`, `minor`, & `patch` |
| `symbol` | `"C "` | The symbol used before displaying the compiler details |
| `detect_extensions` | `["c", "h"]` | Which extensions should trigger this module. |
| `detect_files` | `[]` | Which filenames should trigger this module. |
| `detect_folders` | `[]` | Which folders should trigger this module. |
| `commands` | [ [ "cc", "--version" ], [ "gcc", "--version" ], [ "clang", "--version" ] ] | How to detect what the compiler is |
| `style` | `"bold 149"` | The style for the module. |
| `disabled` | `false` | Disables the `c` module. |
### Variables
| Variable | Example | Description |
| -------- | ------- | ------------------------------------ |
| name | clang | The name of the compiler |
| version | 13.0.0 | The version of the compiler |
| symbol | | Mirrors the value of option `symbol` |
| style | | Mirrors the value of option `style` |
NB that `version` is not in the default format.
### Commands
The `commands` option accepts a list of commands to determine the compiler version and name.
Each command is represented as a list of the executable name, followed by its arguments, usually something like `["mycc", "--version"]`. Starship will try executing each command until it gets a result on STDOUT.
If a C compiler is not supported by this module, you can request it by [raising an issue on GitHub](https://github.com/starship/starship/).
### Example
```toml
# ~/.config/starship.toml
[c]
format = "via [$name $version]($style)"
```
## Character
The `character` module shows a character (usually an arrow) beside where the text

40
src/configs/c.rs Normal file
View File

@ -0,0 +1,40 @@
use crate::config::ModuleConfig;
use serde::Serialize;
use starship_module_config_derive::ModuleConfig;
#[derive(Clone, ModuleConfig, Serialize)]
pub struct CConfig<'a> {
pub format: &'a str,
pub version_format: &'a str,
pub style: &'a str,
pub symbol: &'a str,
pub disabled: bool,
pub detect_extensions: Vec<&'a str>,
pub detect_files: Vec<&'a str>,
pub detect_folders: Vec<&'a str>,
pub commands: Vec<Vec<&'a str>>,
}
impl<'a> Default for CConfig<'a> {
fn default() -> Self {
CConfig {
format: "via [$symbol($version(-$name) )]($style)",
version_format: "v${raw}",
style: "149 bold",
symbol: "C ",
disabled: false,
detect_extensions: vec!["c", "h"],
detect_files: vec![],
detect_folders: vec![],
commands: vec![
// the compiler is usually cc, and --version works on gcc and clang
vec!["cc", "--version"],
// but on some platforms gcc is installed as *gcc*, not cc
vec!["gcc", "--version"],
// for completeness, although I've never seen a clang that wasn't cc
vec!["clang", "--version"],
],
}
}
}

View File

@ -7,6 +7,7 @@ pub mod aws;
pub mod azure;
pub mod battery;
pub mod buf;
pub mod c;
pub mod character;
pub mod cmake;
pub mod cmd_duration;
@ -93,6 +94,7 @@ pub struct FullConfig<'a> {
azure: azure::AzureConfig<'a>,
battery: battery::BatteryConfig<'a>,
buf: buf::BufConfig<'a>,
c: c::CConfig<'a>,
character: character::CharacterConfig<'a>,
cmake: cmake::CMakeConfig<'a>,
cmd_duration: cmd_duration::CmdDurationConfig<'a>,
@ -176,6 +178,7 @@ impl<'a> Default for FullConfig<'a> {
azure: Default::default(),
battery: Default::default(),
buf: Default::default(),
c: Default::default(),
character: Default::default(),
cmake: Default::default(),
cmd_duration: Default::default(),

View File

@ -36,6 +36,7 @@ pub const PROMPT_ORDER: &[&str] = &[
"package",
// ↓ Toolchain version modules ↓
// (Let's keep these sorted alphabetically)
"c",
"cmake",
"cobol",
"dart",

View File

@ -335,6 +335,13 @@ impl<'a> Context<'a> {
Duration::from_millis(self.root_config.command_timeout),
)
}
/// Attempt to execute several commands with exec_cmd, return the results of the first that works
pub fn exec_cmds_return_first(&self, commands: Vec<Vec<&str>>) -> Option<CommandOutput> {
commands
.iter()
.find_map(|attempt| self.exec_cmd(attempt[0], &attempt[1..]))
}
}
#[derive(Debug)]

View File

@ -13,6 +13,7 @@ pub const ALL_MODULES: &[&str] = &[
#[cfg(feature = "battery")]
"battery",
"buf",
"c",
"character",
"cmake",
"cmd_duration",

234
src/modules/c.rs Normal file
View File

@ -0,0 +1,234 @@
use super::{Context, Module, RootModuleConfig};
use crate::configs::c::CConfig;
use crate::formatter::StringFormatter;
use crate::formatter::VersionFormatter;
use once_cell::sync::Lazy;
use semver::Version;
use std::borrow::Cow;
use std::ops::Deref;
/// Creates a module with the current C compiler and version
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
let mut module = context.new_module("c");
let config: CConfig = CConfig::try_load(module.config);
let is_c_project = context
.try_begin_scan()?
.set_extensions(&config.detect_extensions)
.set_files(&config.detect_files)
.set_folders(&config.detect_folders)
.is_match();
if !is_c_project {
return None;
}
let parsed = StringFormatter::new(config.format).and_then(|formatter| {
let c_compiler_info = Lazy::new(|| context.exec_cmds_return_first(config.commands));
formatter
.map_meta(|var, _| match var {
"symbol" => Some(config.symbol),
_ => None,
})
.map_style(|variable| match variable {
"style" => Some(Ok(config.style)),
_ => None,
})
.map(|variable| match variable {
"name" => {
let c_compiler_info = &c_compiler_info.deref().as_ref()?.stdout;
let c_compiler = if c_compiler_info.contains("clang") {
"clang"
} else if c_compiler_info.contains("Free Software Foundation") {
"gcc"
} else {
return None;
};
Some(c_compiler).map(Cow::Borrowed).map(Ok)
}
"version" => {
let c_compiler_info = &c_compiler_info.deref().as_ref()?.stdout;
// Clang says ...
// Apple clang version 13.0.0 ...\n
// OpenBSD clang version 11.1.0\n...
// FreeBSD clang version 11.0.1 ...\n
// so we always want the first semver-ish whitespace-
// separated "word".
// gcc says ...
// gcc (OmniOS 151036/9.3.0-il-1) 9.3.0\n...
// gcc (Debian 10.2.1-6) 10.2.1 ...\n
// cc (GCC) 3.3.5 (Debian 1:3.3.5-13)\n...
// so again we always want the first semver-ish word.
VersionFormatter::format_module_version(
module.get_name(),
c_compiler_info.split_whitespace().find_map(
|word| match Version::parse(word) {
Ok(_v) => Some(word),
Err(_e) => None,
},
)?,
config.version_format,
)
.map(Cow::Owned)
.map(Ok)
}
_ => None,
})
.parse(None, Some(context))
});
module.set_segments(match parsed {
Ok(segments) => segments,
Err(error) => {
log::warn!("Error in module `c`:\n{}", error);
return None;
}
});
Some(module)
}
#[cfg(test)]
mod tests {
use crate::{test::ModuleRenderer, utils::CommandOutput};
use ansi_term::Color;
use std::fs::File;
use std::io;
#[test]
fn folder_without_c_files() -> io::Result<()> {
let dir = tempfile::tempdir()?;
let actual = ModuleRenderer::new("c").path(dir.path()).collect();
let expected = None;
assert_eq!(expected, actual);
dir.close()
}
#[test]
fn folder_with_c_file() -> io::Result<()> {
let dir = tempfile::tempdir()?;
File::create(dir.path().join("any.c"))?.sync_all()?;
// What happens when `cc --version` says it's modern gcc?
// The case when it claims to be clang is covered in folder_with_h_file,
// and uses the mock in src/test/mod.rs.
let actual = ModuleRenderer::new("c")
.cmd(
"cc --version",
Some(CommandOutput {
stdout: String::from(
"\
cc (Debian 10.2.1-6) 10.2.1 20210110
Copyright (C) 2020 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.",
),
stderr: String::default(),
}),
)
.path(dir.path())
.collect();
let expected = Some(format!(
"via {}",
Color::Fixed(149).bold().paint("C v10.2.1-gcc ")
));
assert_eq!(expected, actual);
// What happens when `cc --version` says it's ancient gcc?
let actual = ModuleRenderer::new("c")
.cmd(
"cc --version",
Some(CommandOutput {
stdout: String::from(
"\
cc (GCC) 3.3.5 (Debian 1:3.3.5-13)
Copyright (C) 2003 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.",
),
stderr: String::default(),
}),
)
.path(dir.path())
.collect();
let expected = Some(format!(
"via {}",
Color::Fixed(149).bold().paint("C v3.3.5-gcc ")
));
assert_eq!(expected, actual);
// What happens with an unknown C compiler? Needless to say, we're
// not running on a Z80 so we're never going to see this one in reality!
let actual = ModuleRenderer::new("c")
.cmd(
"cc --version",
Some(CommandOutput {
stdout: String::from("HISOFT-C Compiler V1.2\nCopyright © 1984 HISOFT"),
stderr: String::default(),
}),
)
.path(dir.path())
.collect();
let expected = Some(format!("via {}", Color::Fixed(149).bold().paint("C ")));
assert_eq!(expected, actual);
// What happens when 'cc --version' doesn't work, but 'gcc --version' does?
// This stubs out `cc` but we'll fall back to `gcc --version` as defined in
// src/test/mod.rs.
// Also we don't bother to redefine the config for this one, as we've already
// proved we can parse its version.
let actual = ModuleRenderer::new("c")
.cmd("cc --version", None)
.path(dir.path())
.collect();
let expected = Some(format!(
"via {}",
Color::Fixed(149).bold().paint("C v10.2.1-gcc ")
));
assert_eq!(expected, actual);
// Now with both 'cc' and 'gcc' not working, this should fall back to 'clang --version'
let actual = ModuleRenderer::new("c")
.cmd("cc --version", None)
.cmd("gcc --version", None)
.path(dir.path())
.collect();
let expected = Some(format!(
"via {}",
Color::Fixed(149).bold().paint("C v11.1.0-clang ")
));
assert_eq!(expected, actual);
// What happens when we can't find any of cc, gcc or clang?
let actual = ModuleRenderer::new("c")
.cmd("cc --version", None)
.cmd("gcc --version", None)
.cmd("clang --version", None)
.path(dir.path())
.collect();
let expected = Some(format!("via {}", Color::Fixed(149).bold().paint("C ")));
assert_eq!(expected, actual);
dir.close()
}
#[test]
fn folder_with_h_file() -> io::Result<()> {
let dir = tempfile::tempdir()?;
File::create(dir.path().join("any.h"))?.sync_all()?;
let actual = ModuleRenderer::new("c").path(dir.path()).collect();
let expected = Some(format!(
"via {}",
Color::Fixed(149).bold().paint("C v11.0.1-clang ")
));
assert_eq!(expected, actual);
dir.close()
}
}

View File

@ -2,6 +2,7 @@
mod aws;
mod azure;
mod buf;
mod c;
mod character;
mod cmake;
mod cmd_duration;
@ -93,6 +94,7 @@ pub fn handle<'a>(module: &str, context: &'a Context) -> Option<Module<'a>> {
#[cfg(feature = "battery")]
"battery" => battery::module(context),
"buf" => buf::module(context),
"c" => c::module(context),
"character" => character::module(context),
"cmake" => cmake::module(context),
"cmd_duration" => cmd_duration::module(context),
@ -184,6 +186,7 @@ pub fn description(module: &str) -> &'static str {
"azure" => "The current Azure subscription",
"battery" => "The current charge of the device's battery and its current charging status",
"buf" => "The currently installed version of the Buf CLI",
"c" => "Your C compiler type",
"character" => {
"A character (usually an arrow) beside where the text is entered in your terminal"
}

View File

@ -133,6 +133,30 @@ pub fn mock_cmd<T: AsRef<OsStr> + Debug, U: AsRef<OsStr> + Debug>(
stdout: String::from("1.0.0"),
stderr: String::default(),
}),
"cc --version" => Some(CommandOutput {
stdout: String::from("\
FreeBSD clang version 11.0.1 (git@github.com:llvm/llvm-project.git llvmorg-11.0.1-0-g43ff75f2c3fe)
Target: x86_64-unknown-freebsd13.0
Thread model: posix
InstalledDir: /usr/bin"),
stderr: String::default(),
}),
"gcc --version" => Some(CommandOutput {
stdout: String::from("\
cc (Debian 10.2.1-6) 10.2.1 20210110
Copyright (C) 2020 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE."),
stderr: String::default(),
}),
"clang --version" => Some(CommandOutput {
stdout: String::from("\
OpenBSD clang version 11.1.0
Target: amd64-unknown-openbsd7.0
Thread model: posix
InstalledDir: /usr/bin"),
stderr: String::default(),
}),
"cobc -version" => Some(CommandOutput {
stdout: String::from("\
cobc (GnuCOBOL) 3.1.2.0