Skip to content

Commit bcff432

Browse files
committed
Make cargo --list print description for custom subcommands
1 parent 5181f99 commit bcff432

File tree

6 files changed

+192
-0
lines changed

6 files changed

+192
-0
lines changed

Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,11 @@ itertools = "0.10.0"
7373
# for more information.
7474
rustc-workspace-hack = "1.0.0"
7575

76+
[target.'cfg(target_os = "linux")'.dependencies]
77+
cargo-subcommand-metadata = { path = "crates/cargo-subcommand-metadata", version = "0.1.0" }
78+
memmap = "0.7"
79+
object = "0.28"
80+
7681
[target.'cfg(windows)'.dependencies]
7782
fwdansi = "1.1.0"
7883

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[package]
2+
name = "cargo-subcommand-metadata"
3+
version = "0.1.0"
4+
edition = "2021"
5+
license = "MIT OR Apache-2.0"
6+
repository = "https://github.com/rust-lang/cargo"
7+
description = "Embed metadata into a Cargo subcommand, so that `cargo --list` can show a description of the subcommand"
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/// Cargo's name for the purpose of ELF notes.
2+
///
3+
/// The `name` field of an ELF note is designated to hold the entry's "owner" or
4+
/// "originator". No formal mechanism exists for avoiding name conflicts. By
5+
/// convention, vendors use their own name such as "XYZ Computer Company".
6+
pub const ELF_NOTE_NAME: &str = "rust-lang/cargo";
7+
8+
/// Values used by Cargo as the `type` of its ELF notes.
9+
///
10+
/// Each originator controls its own note types. Multiple interpretations of a
11+
/// single type value can exist. A program must recognize both the `name` and
12+
/// the `type` to understand a descriptor.
13+
#[repr(i32)]
14+
#[non_exhaustive]
15+
pub enum ElfNoteType {
16+
// DESCRIP
17+
Description = 0xDE5C819,
18+
}
19+
20+
/// Embed a description into a compiled Cargo subcommand, to be shown by `cargo
21+
/// --list`.
22+
///
23+
/// The following restrictions apply to a subcommand description:
24+
///
25+
/// - String length can be at most 280 bytes in UTF-8, although much shorter is
26+
/// better.
27+
/// - Must not contain the characters `\n`, `\r`, or `\x1B` (ESC).
28+
///
29+
/// Please consider running `cargo --list` and following the style of the
30+
/// existing descriptions of the built-in Cargo subcommands.
31+
///
32+
/// # Example
33+
///
34+
/// ```
35+
/// // subcommand's main.rs
36+
///
37+
/// cargo_subcommand_metadata::description! {
38+
/// "Draw a spiffy visualization of things"
39+
/// }
40+
///
41+
/// fn main() {
42+
/// /* … */
43+
/// }
44+
/// ```
45+
#[macro_export]
46+
macro_rules! description {
47+
($description:expr) => {
48+
const _: () = {
49+
const CARGO_SUBCOMMAND_DESCRIPTION: &str = $description;
50+
51+
assert!(
52+
CARGO_SUBCOMMAND_DESCRIPTION.len() <= 280,
53+
"subcommand description too long, must be at most 280",
54+
);
55+
56+
#[cfg(target_os = "linux")]
57+
const _: () = {
58+
#[repr(C)]
59+
struct ElfNote {
60+
namesz: u32,
61+
descsz: u32,
62+
ty: $crate::ElfNoteType,
63+
64+
name: [u8; $crate::ELF_NOTE_NAME.len()],
65+
// At least 1 to nul-terminate the string as is convention
66+
// (though not required), plus zero padding to a multiple of 4
67+
// bytes.
68+
name_padding: [$crate::private::Padding;
69+
1 + match ($crate::ELF_NOTE_NAME.len() + 1) % 4 {
70+
0 => 0,
71+
r => 4 - r,
72+
}],
73+
74+
desc: [u8; CARGO_SUBCOMMAND_DESCRIPTION.len()],
75+
// Zero padding to a multiple of 4 bytes.
76+
desc_padding: [$crate::private::Padding;
77+
match CARGO_SUBCOMMAND_DESCRIPTION.len() % 4 {
78+
0 => 0,
79+
r => 4 - r,
80+
}],
81+
}
82+
83+
#[used]
84+
#[link_section = ".note.cargo.subcommand"]
85+
static ELF_NOTE: ElfNote = {
86+
ElfNote {
87+
namesz: $crate::ELF_NOTE_NAME.len() as u32 + 1,
88+
descsz: CARGO_SUBCOMMAND_DESCRIPTION.len() as u32,
89+
ty: $crate::ElfNoteType::Description,
90+
name: unsafe { *$crate::ELF_NOTE_NAME.as_ptr().cast() },
91+
name_padding: $crate::private::padding(),
92+
desc: unsafe { *CARGO_SUBCOMMAND_DESCRIPTION.as_ptr().cast() },
93+
desc_padding: $crate::private::padding(),
94+
}
95+
};
96+
};
97+
};
98+
};
99+
}
100+
101+
// Implementation details. Not public API.
102+
#[doc(hidden)]
103+
pub mod private {
104+
#[derive(Copy, Clone)]
105+
#[repr(u8)]
106+
pub enum Padding {
107+
Zero = 0,
108+
}
109+
110+
pub const fn padding<const N: usize>() -> [Padding; N] {
111+
[Padding::Zero; N]
112+
}
113+
}

src/bin/cargo/cli.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use std::fmt::Write;
1212
use super::commands;
1313
use super::list_commands;
1414
use crate::command_prelude::*;
15+
use crate::subcommand_metadata;
1516
use cargo::core::features::HIDDEN;
1617

1718
lazy_static::lazy_static! {
@@ -121,6 +122,8 @@ Run with 'cargo -Z [FLAG] [COMMAND]'",
121122
CommandInfo::External { path } => {
122123
if let Some(desc) = known_external_desc {
123124
drop_println!(config, " {:<20} {}", name, desc);
125+
} else if let Some(desc) = subcommand_metadata::description(&path) {
126+
drop_println!(config, " {:<20} {}", name, desc);
124127
} else if is_verbose {
125128
drop_println!(config, " {:<20} {}", name, path.display());
126129
} else {

src/bin/cargo/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use std::path::{Path, PathBuf};
1313

1414
mod cli;
1515
mod commands;
16+
mod subcommand_metadata;
1617

1718
use crate::command_prelude::*;
1819

src/bin/cargo/subcommand_metadata.rs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
use std::path::Path;
2+
3+
pub(crate) fn description(path: &Path) -> Option<String> {
4+
implementation::description(path)
5+
}
6+
7+
#[cfg(target_os = "linux")]
8+
mod implementation {
9+
use memmap::Mmap;
10+
use object::endian::LittleEndian;
11+
use object::read::elf::{ElfFile64, FileHeader, SectionHeader};
12+
use std::fs::File;
13+
use std::path::Path;
14+
use std::str;
15+
16+
pub(super) fn description(path: &Path) -> Option<String> {
17+
let executable_file = File::open(path).ok()?;
18+
let data = &*unsafe { Mmap::map(&executable_file) }.ok()?;
19+
let elf = ElfFile64::<LittleEndian>::parse(data).ok()?;
20+
let endian = elf.endian();
21+
let file_header = elf.raw_header();
22+
let section_headers = file_header.section_headers(endian, data).ok()?;
23+
let string_table = file_header
24+
.section_strings(endian, data, section_headers)
25+
.ok()?;
26+
27+
let mut description = None;
28+
for section_header in section_headers {
29+
if section_header.name(endian, string_table).ok() == Some(b".note.cargo.subcommand") {
30+
if let Ok(Some(mut notes)) = section_header.notes(endian, data) {
31+
while let Ok(Some(note)) = notes.next() {
32+
if note.name() == cargo_subcommand_metadata::ELF_NOTE_NAME.as_bytes()
33+
&& note.n_type(endian)
34+
== cargo_subcommand_metadata::ElfNoteType::Description as u32
35+
{
36+
if description.is_some() {
37+
return None;
38+
}
39+
description = Some(note.desc());
40+
}
41+
}
42+
}
43+
}
44+
}
45+
46+
let description: &[u8] = description?;
47+
let description: &str = str::from_utf8(description).ok()?;
48+
if description.len() > 280 || description.contains(&['\n', '\r', '\x1B']) {
49+
return None;
50+
}
51+
52+
Some(description.to_owned())
53+
}
54+
}
55+
56+
#[cfg(not(target_os = "linux"))]
57+
mod implementation {
58+
use std::path::Path;
59+
60+
pub(super) fn description(_path: &Path) -> Option<String> {
61+
None
62+
}
63+
}

0 commit comments

Comments
 (0)