Skip to content

Support for descriptions on third-party subcommands in cargo --list #10663

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ path = "src/cargo/lib.rs"
atty = "0.2"
bytesize = "1.0"
cargo-platform = { path = "crates/cargo-platform", version = "0.1.2" }
cargo-subcommand-metadata = { path = "crates/cargo-subcommand-metadata", version = "0.1.0", features = ["parse"] }
cargo-util = { path = "crates/cargo-util", version = "0.2.3" }
crates-io = { path = "crates/crates-io", version = "0.35.0" }
curl = { version = "0.4.44", features = ["http2"] }
Expand Down
14 changes: 14 additions & 0 deletions crates/cargo-subcommand-metadata/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[package]
name = "cargo-subcommand-metadata"
version = "0.1.0"
edition = "2021"
license = "MIT OR Apache-2.0"
repository = "https://github.com/rust-lang/cargo"
description = "Embed metadata into a Cargo subcommand, so that `cargo --list` can show a description of the subcommand"

[target.'cfg(target_os = "linux")'.dependencies]
memmap = { version = "0.7", optional = true }
object = { version = "0.28", optional = true }

[features]
parse = ["memmap", "object"]
114 changes: 114 additions & 0 deletions crates/cargo-subcommand-metadata/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
#[cfg(feature = "parse")]
pub mod parse;

/// Cargo's name for the purpose of ELF notes.
///
/// The `name` field of an ELF note is designated to hold the entry's "owner" or
/// "originator". No formal mechanism exists for avoiding name conflicts. By
/// convention, vendors use their own name such as "XYZ Computer Company".
pub const ELF_NOTE_NAME: &str = "rust-lang/cargo";

/// Values used by Cargo as the `type` of its ELF notes.
///
/// Each originator controls its own note types. Multiple interpretations of a
/// single type value can exist. A program must recognize both the `name` and
/// the `type` to understand a descriptor.
#[repr(i32)]
#[non_exhaustive]
pub enum ElfNoteType {
// DESCRIP
Description = 0xDE5C819,
}

/// Embed a description into a compiled Cargo subcommand, to be shown by `cargo
/// --list`.
///
/// The following restrictions apply to a subcommand description:
///
/// - String length can be at most 280 bytes in UTF-8, although much shorter is
/// better.
/// - Must not contain the characters `\n`, `\r`, or `\x1B` (ESC).
///
/// Please consider running `cargo --list` and following the style of the
/// existing descriptions of the built-in Cargo subcommands.
///
/// # Example
///
/// ```
/// // subcommand's main.rs
///
/// cargo_subcommand_metadata::description! {
/// "Draw a spiffy visualization of things"
/// }
///
/// fn main() {
/// /* … */
/// }
/// ```
#[macro_export]
macro_rules! description {
($description:expr) => {
Comment on lines +49 to +50
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note to self: maybe cargo_subcommand_metadata::description!(); with no string argument could embed env!(CARGO_PKG_DESCRIPTION) by default.

const _: () = {
const CARGO_SUBCOMMAND_DESCRIPTION: &str = $description;

assert!(
CARGO_SUBCOMMAND_DESCRIPTION.len() <= 280,
"subcommand description too long, must be at most 280",
);

#[cfg(target_os = "linux")]
const _: () = {
#[repr(C)]
struct ElfNote {
namesz: u32,
descsz: u32,
ty: $crate::ElfNoteType,

name: [u8; $crate::ELF_NOTE_NAME.len()],
// At least 1 to nul-terminate the string as is convention
// (though not required), plus zero padding to a multiple of 4
// bytes.
name_padding: [$crate::private::Padding;
1 + match ($crate::ELF_NOTE_NAME.len() + 1) % 4 {
0 => 0,
r => 4 - r,
}],

desc: [u8; CARGO_SUBCOMMAND_DESCRIPTION.len()],
// Zero padding to a multiple of 4 bytes.
desc_padding: [$crate::private::Padding;
match CARGO_SUBCOMMAND_DESCRIPTION.len() % 4 {
0 => 0,
r => 4 - r,
}],
}

#[used]
#[link_section = ".note.cargo.subcommand"]
static ELF_NOTE: ElfNote = ElfNote {
namesz: $crate::ELF_NOTE_NAME.len() as u32 + 1,
descsz: CARGO_SUBCOMMAND_DESCRIPTION.len() as u32,
ty: $crate::ElfNoteType::Description,
name: unsafe { *$crate::ELF_NOTE_NAME.as_ptr().cast() },
name_padding: $crate::private::padding(),
desc: unsafe { *CARGO_SUBCOMMAND_DESCRIPTION.as_ptr().cast() },
desc_padding: $crate::private::padding(),
};
};
};
};
}

// Implementation details. Not public API.
#[doc(hidden)]
pub mod private {
#[derive(Copy, Clone)]
#[repr(u8)]
pub enum Padding {
Zero = 0,
}

pub const fn padding<const N: usize>() -> [Padding; N] {
[Padding::Zero; N]
}
}
62 changes: 62 additions & 0 deletions crates/cargo-subcommand-metadata/src/parse.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
use std::path::Path;

pub fn description(path: &Path) -> Option<String> {
implementation::description(path)
}

#[cfg(target_os = "linux")]
mod implementation {
use memmap::Mmap;
use object::endian::LittleEndian;
use object::read::elf::{ElfFile64, FileHeader, SectionHeader};
use std::fs::File;
use std::path::Path;
use std::str;

pub(super) fn description(path: &Path) -> Option<String> {
let executable_file = File::open(path).ok()?;
let data = &*unsafe { Mmap::map(&executable_file) }.ok()?;
let elf = ElfFile64::<LittleEndian>::parse(data).ok()?;
let endian = elf.endian();
let file_header = elf.raw_header();
let section_headers = file_header.section_headers(endian, data).ok()?;
let string_table = file_header
.section_strings(endian, data, section_headers)
.ok()?;

let mut description = None;
for section_header in section_headers {
if section_header.name(endian, string_table).ok() == Some(b".note.cargo.subcommand") {
if let Ok(Some(mut notes)) = section_header.notes(endian, data) {
while let Ok(Some(note)) = notes.next() {
if note.name() == crate::ELF_NOTE_NAME.as_bytes()
&& note.n_type(endian) == crate::ElfNoteType::Description as u32
{
if description.is_some() {
return None;
}
description = Some(note.desc());
}
}
}
}
}

let description: &[u8] = description?;
let description: &str = str::from_utf8(description).ok()?;
if description.len() > 280 || description.contains(&['\n', '\r', '\x1B']) {
return None;
}

Some(description.to_owned())
}
}

#[cfg(not(target_os = "linux"))]
mod implementation {
use std::path::Path;

pub(super) fn description(_path: &Path) -> Option<String> {
None
}
}
3 changes: 3 additions & 0 deletions src/bin/cargo/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use anyhow::anyhow;
use cargo::core::shell::Shell;
use cargo::core::{features, CliUnstable};
use cargo::{self, drop_print, drop_println, CliResult, Config};
use cargo_subcommand_metadata as subcommand_metadata;
use clap::{Arg, ArgMatches};
use itertools::Itertools;
use std::collections::HashMap;
Expand Down Expand Up @@ -121,6 +122,8 @@ Run with 'cargo -Z [FLAG] [COMMAND]'",
CommandInfo::External { path } => {
if let Some(desc) = known_external_desc {
drop_println!(config, " {:<20} {}", name, desc);
} else if let Some(desc) = subcommand_metadata::parse::description(&path) {
drop_println!(config, " {:<20} {}", name, desc);
} else if is_verbose {
drop_println!(config, " {:<20} {}", name, path.display());
} else {
Expand Down