From 93d34dee923a8b829ef12f3943a4fdf78e947468 Mon Sep 17 00:00:00 2001 From: Jonathan Pallant Date: Tue, 30 Jul 2024 19:39:06 +0100 Subject: [PATCH] generate-copyright: use rinja to format the output I can't find a way to derive rinja::Template for Node - I think because it is a recursive type. So I rendered it manually using html_escape. --- src/tools/generate-copyright/Cargo.toml | 6 +- .../generate-copyright/src/cargo_metadata.rs | 12 +- src/tools/generate-copyright/src/main.rs | 226 +++++++----------- .../templates/COPYRIGHT.html | 54 +++++ 4 files changed, 146 insertions(+), 152 deletions(-) create mode 100644 src/tools/generate-copyright/templates/COPYRIGHT.html diff --git a/src/tools/generate-copyright/Cargo.toml b/src/tools/generate-copyright/Cargo.toml index c94cc35fb5036..c00292cf33108 100644 --- a/src/tools/generate-copyright/Cargo.toml +++ b/src/tools/generate-copyright/Cargo.toml @@ -8,8 +8,10 @@ description = "Produces a manifest of all the copyrighted materials in the Rust [dependencies] anyhow = "1.0.65" +cargo_metadata = "0.18.1" +html-escape = "0.2.13" +rinja = "0.2.0" serde = { version = "1.0.147", features = ["derive"] } serde_json = "1.0.85" -thiserror = "1" tempfile = "3" -cargo_metadata = "0.18.1" +thiserror = "1" diff --git a/src/tools/generate-copyright/src/cargo_metadata.rs b/src/tools/generate-copyright/src/cargo_metadata.rs index 655d73715e036..d02b9eeb6f922 100644 --- a/src/tools/generate-copyright/src/cargo_metadata.rs +++ b/src/tools/generate-copyright/src/cargo_metadata.rs @@ -1,7 +1,7 @@ //! Gets metadata about a workspace from Cargo use std::collections::BTreeMap; -use std::ffi::{OsStr, OsString}; +use std::ffi::OsStr; use std::path::Path; /// Describes how this module can fail @@ -36,7 +36,9 @@ pub struct PackageMetadata { /// A list of important files from the package, with their contents. /// /// This includes *COPYRIGHT*, *NOTICE*, *AUTHOR*, *LICENSE*, and *LICENCE* files, case-insensitive. - pub notices: BTreeMap, + pub notices: BTreeMap, + /// If this is true, this dep is in the Rust Standard Library + pub is_in_libstd: Option, } /// Use `cargo metadata` and `cargo vendor` to get a list of dependencies and their license data. @@ -101,6 +103,7 @@ pub fn get_metadata( license: package.license.unwrap_or_else(|| String::from("Unspecified")), authors: package.authors, notices: BTreeMap::new(), + is_in_libstd: None, }, ); } @@ -161,8 +164,9 @@ fn load_important_files( if metadata.is_dir() { // scoop up whole directory } else if metadata.is_file() { - println!("Scraping {}", filename.to_string_lossy()); - dep.notices.insert(filename.to_owned(), std::fs::read_to_string(path)?); + let filename = filename.to_string_lossy(); + println!("Scraping {}", filename); + dep.notices.insert(filename.to_string(), std::fs::read_to_string(path)?); } } } diff --git a/src/tools/generate-copyright/src/main.rs b/src/tools/generate-copyright/src/main.rs index af69ab8c8bf36..03b789b739298 100644 --- a/src/tools/generate-copyright/src/main.rs +++ b/src/tools/generate-copyright/src/main.rs @@ -1,37 +1,17 @@ use std::collections::BTreeMap; -use std::io::Write; use std::path::{Path, PathBuf}; use anyhow::Error; +use rinja::Template; mod cargo_metadata; -static TOP_BOILERPLATE: &str = r##" - - - - - Copyright notices for The Rust Toolchain - - - -

Copyright notices for The Rust Toolchain

- -

This file describes the copyright and licensing information for the source -code within The Rust Project git tree, and the third-party dependencies used -when building the Rust toolchain (including the Rust Standard Library).

- -

Table of Contents

- -"##; - -static BOTTOM_BOILERPLATE: &str = r#" - - -"#; +#[derive(Template)] +#[template(path = "COPYRIGHT.html")] +struct CopyrightTemplate { + in_tree: Node, + dependencies: BTreeMap, +} /// The entry point to the binary. /// @@ -53,150 +33,114 @@ fn main() -> Result<(), Error> { Path::new("./src/tools/cargo/Cargo.toml"), Path::new("./library/std/Cargo.toml"), ]; - let collected_cargo_metadata = + let mut collected_cargo_metadata = cargo_metadata::get_metadata_and_notices(&cargo, &out_dir, &root_path, &workspace_paths)?; let stdlib_set = cargo_metadata::get_metadata(&cargo, &root_path, &[Path::new("./library/std/Cargo.toml")])?; - let mut buffer = Vec::new(); + for (key, value) in collected_cargo_metadata.iter_mut() { + value.is_in_libstd = Some(stdlib_set.contains_key(key)); + } - writeln!(buffer, "{}", TOP_BOILERPLATE)?; + let template = CopyrightTemplate { + in_tree: collected_tree_metadata.files, + dependencies: collected_cargo_metadata, + }; - writeln!( - buffer, - r#"

In-tree files

The following licenses cover the in-tree source files that were used in this release:

"# - )?; - render_tree_recursive(&collected_tree_metadata.files, &mut buffer)?; + let output = template.render()?; - writeln!( - buffer, - r#"

Out-of-tree dependencies

The following licenses cover the out-of-tree crates that were used in this release:

"# - )?; - render_deps(&collected_cargo_metadata, &stdlib_set, &mut buffer)?; + std::fs::write(&dest_file, output)?; - writeln!(buffer, "{}", BOTTOM_BOILERPLATE)?; + Ok(()) +} - std::fs::write(&dest_file, &buffer)?; +/// Describes a tree of metadata for our filesystem tree +#[derive(serde::Deserialize)] +struct Metadata { + files: Node, +} +/// Describes one node in our metadata tree +#[derive(serde::Deserialize)] +#[serde(rename_all = "kebab-case", tag = "type")] +pub(crate) enum Node { + Root { children: Vec }, + Directory { name: String, children: Vec, license: Option }, + File { name: String, license: License }, + Group { files: Vec, directories: Vec, license: License }, +} + +fn with_box(fmt: &mut std::fmt::Formatter<'_>, inner: F) -> std::fmt::Result +where + F: FnOnce(&mut std::fmt::Formatter<'_>) -> std::fmt::Result, +{ + writeln!(fmt, r#"
"#)?; + inner(fmt)?; + writeln!(fmt, "
")?; Ok(()) } -/// Recursively draw the tree of files/folders we found on disk and their licenses, as -/// markdown, into the given Vec. -fn render_tree_recursive(node: &Node, buffer: &mut Vec) -> Result<(), Error> { - writeln!(buffer, r#"
"#)?; - match node { - Node::Root { children } => { - for child in children { - render_tree_recursive(child, buffer)?; +impl std::fmt::Display for Node { + fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Node::Root { children } => { + if children.len() > 1 { + with_box(fmt, |f| { + for child in children { + writeln!(f, "{child}")?; + } + Ok(()) + }) + } else { + for child in children { + writeln!(fmt, "{child}")?; + } + Ok(()) + } } - } - Node::Directory { name, children, license } => { - render_tree_license(std::iter::once(name), license.as_ref(), buffer)?; - if !children.is_empty() { - writeln!(buffer, "

Exceptions:

")?; - for child in children { - render_tree_recursive(child, buffer)?; + Node::Directory { name, children, license } => with_box(fmt, |f| { + render_tree_license(std::iter::once(name), license.as_ref(), f)?; + if !children.is_empty() { + writeln!(f, "

Exceptions:

")?; + for child in children { + writeln!(f, "{child}")?; + } } + Ok(()) + }), + Node::Group { files, directories, license } => with_box(fmt, |f| { + render_tree_license(directories.iter().chain(files.iter()), Some(license), f) + }), + Node::File { name, license } => { + with_box(fmt, |f| render_tree_license(std::iter::once(name), Some(license), f)) } } - Node::Group { files, directories, license } => { - render_tree_license(directories.iter().chain(files.iter()), Some(license), buffer)?; - } - Node::File { name, license } => { - render_tree_license(std::iter::once(name), Some(license), buffer)?; - } } - writeln!(buffer, "
")?; - - Ok(()) } -/// Draw a series of sibling files/folders, as markdown, into the given Vec. +/// Draw a series of sibling files/folders, as HTML, into the given formatter. fn render_tree_license<'a>( names: impl Iterator, license: Option<&License>, - buffer: &mut Vec, -) -> Result<(), Error> { - writeln!(buffer, "

File/Directory: ")?; + f: &mut std::fmt::Formatter<'_>, +) -> std::fmt::Result { + writeln!(f, "

File/Directory: ")?; for name in names { - writeln!(buffer, "{name}")?; + writeln!(f, "{}", html_escape::encode_text(&name))?; } - writeln!(buffer, "

")?; + writeln!(f, "

")?; if let Some(license) = license { - writeln!(buffer, "

License: {}

", license.spdx)?; + writeln!(f, "

License: {}

", html_escape::encode_text(&license.spdx))?; for copyright in license.copyright.iter() { - writeln!(buffer, "

Copyright: {copyright}

")?; + writeln!(f, "

Copyright: {}

", html_escape::encode_text(©right))?; } } Ok(()) } -/// Render a list of out-of-tree dependencies as markdown into the given Vec. -fn render_deps( - all_deps: &BTreeMap, - stdlib_set: &BTreeMap, - buffer: &mut Vec, -) -> Result<(), Error> { - for (package, metadata) in all_deps { - let authors_list = if metadata.authors.is_empty() { - "None Specified".to_owned() - } else { - metadata.authors.join(", ") - }; - let url = format!("https://crates.io/crates/{}/{}", package.name, package.version); - writeln!(buffer)?; - writeln!( - buffer, - r#"

📦 {name}-{version}

"#, - name = package.name, - version = package.version, - )?; - writeln!(buffer, r#"

URL: {url}

"#,)?; - writeln!( - buffer, - "

In libstd: {}

", - if stdlib_set.contains_key(package) { "Yes" } else { "No" } - )?; - writeln!(buffer, "

Authors: {}

", escape_html(&authors_list))?; - writeln!(buffer, "

License: {}

", escape_html(&metadata.license))?; - writeln!(buffer, "

Notices: ")?; - if metadata.notices.is_empty() { - writeln!(buffer, "None")?; - } else { - for (name, contents) in &metadata.notices { - writeln!( - buffer, - "

{}", - name.to_string_lossy() - )?; - writeln!(buffer, "
\n{}\n
", contents)?; - writeln!(buffer, "
")?; - } - } - writeln!(buffer, "

")?; - } - Ok(()) -} -/// Describes a tree of metadata for our filesystem tree -#[derive(serde::Deserialize)] -struct Metadata { - files: Node, -} - -/// Describes one node in our metadata tree -#[derive(serde::Deserialize)] -#[serde(rename_all = "kebab-case", tag = "type")] -pub(crate) enum Node { - Root { children: Vec }, - Directory { name: String, children: Vec, license: Option }, - File { name: String, license: License }, - Group { files: Vec, directories: Vec, license: License }, -} - /// A License has an SPDX license name and a list of copyright holders. #[derive(serde::Deserialize)] struct License { @@ -212,13 +156,3 @@ fn env_path(var: &str) -> Result { anyhow::bail!("missing environment variable {var}") } } - -/// Escapes any invalid HTML characters -fn escape_html(input: &str) -> String { - static MAPPING: [(char, &str); 3] = [('&', "&"), ('<', "<"), ('>', ">")]; - let mut output = input.to_owned(); - for (ch, s) in &MAPPING { - output = output.replace(*ch, s); - } - output -} diff --git a/src/tools/generate-copyright/templates/COPYRIGHT.html b/src/tools/generate-copyright/templates/COPYRIGHT.html new file mode 100644 index 0000000000000..ccb177a54d419 --- /dev/null +++ b/src/tools/generate-copyright/templates/COPYRIGHT.html @@ -0,0 +1,54 @@ + + + + + Copyright notices for The Rust Toolchain + + + +

Copyright notices for The Rust Toolchain

+ +

This file describes the copyright and licensing information for the source +code within The Rust Project git tree, and the third-party dependencies used +when building the Rust toolchain (including the Rust Standard Library).

+ +

Table of Contents

+ + +

In-tree files

+ +

The following licenses cover the in-tree source files that were used in this +release:

+ +{{ in_tree|safe }} + +

Out-of-tree dependencies

+ +

The following licenses cover the out-of-tree crates that were used in this +release:

+ +{% for (key, value) in dependencies %} +

📦 {{key.name}}-{{key.version}}

+

URL: https://crates.io/crates/{{ key.name }}/{{ key.version }}

+

In libstd: {% if value.is_in_libstd.unwrap() %} Yes {% else %} No {% endif %}

+

Authors: {{ value.authors|join(", ") }}

+

License: {{ value.license }}

+ {% let len = value.notices.len() %} + {% if len > 0 %} +

Notices: + {% for (notice_name, notice_text) in value.notices %} +

+ {{ notice_name }} +
+{{ notice_text }}
+                
+
+ {% endfor %} +

+ {% endif %} +{% endfor %} + + \ No newline at end of file