on master help sign in
Credit syntax definition and theme authors with new `--acknowledgements` option (#1971)

The text that is printed is generated when building assets, by analyzing LICENSE and NOTICE files that comes with syntaxes and themes. We take this opportunity to also add a NOTICE file as defined by Apache License 2.0.

commita3ea7982466941d746a09bafc8bf386711c48686 authorMartin Nordholts <enselic@gmail.com> dateSat 11 Dec 2021, 14:00 (5 years ago) parent63ad538
12 files changed+325−7
CHANGELOG.md+2−1 Cargo.lock+2 Cargo.toml+3−1 NOTICE+6 assets/acknowledgements.bin assets/create.sh+1−1 src/assets.rs+10 src/assets/build_assets.rs+23−1 src/assets/build_assets/acknowledgements.rs+219 src/bin/bat/clap_app.rs+12 src/bin/bat/main.rs+10−3 tests/integration_tests.rs+37
CHANGELOG.md+2−1
@@ -6,6 +6,7 @@
66- Support for `--ignored-suffix` argument. See #1892 (@bojan88)
77- `$BAT_CONFIG_DIR` is now a recognized environment variable. It has precedence over `$XDG_CONFIG_HOME`, see #1727 (@billrisher)
88- Support for `x:+delta` syntax in line ranges (e.g. `20:+10`). See #1810 (@bojan88)
9+- Add new `--acknowledgements` option that gives credit to theme and syntax definition authors. See #1971 (@Enselic)
910
1011## Bugfixes
1112
@@ -47,7 +48,7 @@
4748## `bat` as a library
4849
4950- Deprecate `HighlightingAssets::syntaxes()` and `HighlightingAssets::syntax_for_file_name()`. Use `HighlightingAssets::get_syntaxes()` and `HighlightingAssets::get_syntax_for_path()` instead. They return a `Result` which is needed for upcoming lazy-loading work to improve startup performance. They also return which `SyntaxSet` the returned `SyntaxReference` belongs to. See #1747, #1755, #1776, #1862 (@Enselic)
50−- Remove `HighlightingAssets::from_files` and `HighlightingAssets::save_to_cache`. Instead of calling the former and then the latter you now make a single call to `bat::assets::build`. See #1802 (@Enselic)
51+- Remove `HighlightingAssets::from_files` and `HighlightingAssets::save_to_cache`. Instead of calling the former and then the latter you now make a single call to `bat::assets::build`. See #1802, #1971 (@Enselic)
5152- Replace the `error::Error(error::ErrorKind, _)` struct and enum with an `error::Error` enum. `Error(ErrorKind::UnknownSyntax, _)` becomes `Error::UnknownSyntax`, etc. Also remove the `error::ResultExt` trait. These changes stem from replacing `error-chain` with `thiserror`. See #1820 (@Enselic)
5253- Add new `MappingTarget` enum variant `MapExtensionToUnknown`. Refer to its docummentation for more information. Clients are adviced to treat `MapExtensionToUnknown` the same as `MapToUnknown` in exhaustive matches. See #1703 (@cbolgiano)
5354
Cargo.lock+2
@@ -96,6 +96,7 @@ dependencies = [
9696 "once_cell",
9797 "path_abs",
9898 "predicates",
99+ "regex",
99100 "semver",
100101 "serde",
101102 "serde_yaml",
@@ -106,6 +107,7 @@ dependencies = [
106107 "thiserror",
107108 "unicode-width",
108109 "wait-timeout",
110+ "walkdir",
109111 "wild",
110112]
111113
Cargo.toml+3−1
@@ -34,7 +34,7 @@ minimal-application = [
3434git = ["git2"] # Support indicating git modifications
3535paging = ["shell-words", "grep-cli"] # Support applying a pager on the output
3636# Add "syntect/plist-load" when https://github.com/trishume/syntect/pull/345 reaches us
37−build-assets = ["syntect/yaml-load", "syntect/dump-create"]
37+build-assets = ["syntect/yaml-load", "syntect/dump-create", "regex", "walkdir"]
3838
3939# You need to use one of these if you depend on bat as a library:
4040regex-onig = ["syntect/regex-onig"] # Use the "oniguruma" regex engine
@@ -63,6 +63,8 @@ clircle = "0.3"
6363bugreport = { version = "0.4", optional = true }
6464dirs-next = { version = "2.0.0", optional = true }
6565grep-cli = { version = "0.1.6", optional = true }
66+regex = { version = "1.0", optional = true }
67+walkdir = { version = "2.0", optional = true }
6668
6769[dependencies.git2]
6870version = "0.13"
NOTICEnew file+6
@@ -0,0 +1,6 @@
1+Copyright (c) 2018-2021 bat-developers (https://github.com/sharkdp/bat).
2+
3+bat is made available under the terms of either the MIT License or the Apache
4+License 2.0, at your option.
5+
6+See the LICENSE-APACHE and LICENSE-MIT files for license details.
assets/acknowledgements.binnew file

Binary file not shown.

assets/create.sh+1−1
@@ -53,7 +53,7 @@ bat cache --clear
5353 done
5454)
5555
56−bat cache --build --blank --source="$ASSET_DIR" --target="$ASSET_DIR"
56+bat cache --build --blank --acknowledgements --source="$ASSET_DIR" --target="$ASSET_DIR"
5757
5858(
5959 cd "$ASSET_DIR"
src/assets.rs+10
@@ -55,6 +55,9 @@ pub(crate) const COMPRESS_THEMES: bool = false;
5555/// performance due to lazy-loading
5656pub(crate) const COMPRESS_LAZY_THEMES: bool = true;
5757
58+/// Compress for size of ~10 kB instead of ~120 kB
59+pub(crate) const COMPRESS_ACKNOWLEDGEMENTS: bool = true;
60+
5861impl HighlightingAssets {
5962 fn new(serialized_syntax_set: SerializedSyntaxSet, theme_set: LazyThemeSet) -> Self {
6063 HighlightingAssets {
@@ -305,6 +308,13 @@ pub(crate) fn get_integrated_themeset() -> LazyThemeSet {
305308 from_binary(include_bytes!("../assets/themes.bin"), COMPRESS_THEMES)
306309}
307310
311+pub fn get_acknowledgements() -> String {
312+ from_binary(
313+ include_bytes!("../assets/acknowledgements.bin"),
314+ COMPRESS_ACKNOWLEDGEMENTS,
315+ )
316+}
317+
308318pub(crate) fn from_binary<T: serde::de::DeserializeOwned>(v: &[u8], compressed: bool) -> T {
309319 asset_from_contents(v, "n/a", compressed)
310320 .expect("data integrated in binary is never faulty, but make sure `compressed` is in sync!")
src/assets/build_assets.rs+23−1
@@ -5,10 +5,14 @@ use syntect::highlighting::ThemeSet;
55use syntect::parsing::{SyntaxSet, SyntaxSetBuilder};
66
77use crate::assets::*;
8+use acknowledgements::build_acknowledgements;
9+
10+mod acknowledgements;
811
912pub fn build(
1013 source_dir: &Path,
1114 include_integrated_assets: bool,
15+ include_acknowledgements: bool,
1216 target_dir: &Path,
1317 current_version: &str,
1418) -> Result<()> {
@@ -18,9 +22,17 @@ pub fn build(
1822
1923 let syntax_set = syntax_set_builder.build();
2024
25+ let acknowledgements = build_acknowledgements(source_dir, include_acknowledgements)?;
26+
2127 print_unlinked_contexts(&syntax_set);
2228
23− write_assets(&theme_set, &syntax_set, target_dir, current_version)
29+ write_assets(
30+ &theme_set,
31+ &syntax_set,
32+ &acknowledgements,
33+ target_dir,
34+ current_version,
35+ )
2436}
2537
2638fn build_theme_set(source_dir: &Path, include_integrated_assets: bool) -> Result<LazyThemeSet> {
@@ -89,6 +101,7 @@ fn print_unlinked_contexts(syntax_set: &SyntaxSet) {
89101fn write_assets(
90102 theme_set: &LazyThemeSet,
91103 syntax_set: &SyntaxSet,
104+ acknowledgements: &Option<String>,
92105 target_dir: &Path,
93106 current_version: &str,
94107) -> Result<()> {
@@ -106,6 +119,15 @@ fn write_assets(
106119 COMPRESS_SYNTAXES,
107120 )?;
108121
122+ if let Some(acknowledgements) = acknowledgements {
123+ asset_to_cache(
124+ acknowledgements,
125+ &target_dir.join("acknowledgements.bin"),
126+ "acknowledgements",
127+ COMPRESS_ACKNOWLEDGEMENTS,
128+ )?;
129+ }
130+
109131 print!(
110132 "Writing metadata to folder {} ... ",
111133 target_dir.to_string_lossy()
src/assets/build_assets/acknowledgements.rsnew file+219
@@ -0,0 +1,219 @@
1+use std::fs::read_to_string;
2+use std::path::{Path, PathBuf};
3+
4+use walkdir::DirEntry;
5+
6+use crate::error::*;
7+
8+struct PathAndStem {
9+ path: PathBuf,
10+ stem: String,
11+ relative_path: String,
12+}
13+
14+/// Looks for LICENSE and NOTICE files in `source_dir`, does some rudimentary
15+/// analysis, and compiles them together in a single string that is meant to be
16+/// used in the output to `--acknowledgements`
17+pub fn build_acknowledgements(
18+ source_dir: &Path,
19+ include_acknowledgements: bool,
20+) -> Result<Option<String>> {
21+ if !include_acknowledgements {
22+ return Ok(None);
23+ }
24+
25+ let mut acknowledgements = format!("{}\n\n", include_str!("../../../NOTICE"));
26+
27+ // Sort entries so the order is stable over time
28+ let entries = walkdir::WalkDir::new(source_dir).sort_by(|a, b| a.path().cmp(b.path()));
29+ for path_and_stem in entries
30+ .into_iter()
31+ .flatten()
32+ .flat_map(|entry| to_path_and_stem(source_dir, entry))
33+ {
34+ if let Some(license_text) = handle_file(&path_and_stem)? {
35+ append_to_acknowledgements(
36+ &mut acknowledgements,
37+ &path_and_stem.relative_path,
38+ &license_text,
39+ )
40+ }
41+ }
42+
43+ Ok(Some(acknowledgements))
44+}
45+
46+fn to_path_and_stem(source_dir: &Path, entry: DirEntry) -> Option<PathAndStem> {
47+ let path = entry.path();
48+
49+ Some(PathAndStem {
50+ path: path.to_owned(),
51+ stem: path.file_stem().map(|s| s.to_string_lossy().to_string())?,
52+ relative_path: path
53+ .strip_prefix(source_dir)
54+ .map(|p| p.to_string_lossy().to_string())
55+ .ok()?,
56+ })
57+}
58+
59+fn handle_file(path_and_stem: &PathAndStem) -> Result<Option<String>> {
60+ if path_and_stem.stem == "NOTICE" {
61+ handle_notice(&path_and_stem.path)
62+ } else if path_and_stem.stem.to_ascii_uppercase() == "LICENSE" {
63+ handle_license(&path_and_stem.path)
64+ } else {
65+ Ok(None)
66+ }
67+}
68+
69+fn handle_notice(path: &Path) -> Result<Option<String>> {
70+ // Assume NOTICE as defined by Apache License 2.0. These must be part of acknowledgements.
71+ Ok(Some(read_to_string(path)?))
72+}
73+
74+fn handle_license(path: &Path) -> Result<Option<String>> {
75+ let license_text = read_to_string(path)?;
76+
77+ if include_license_in_acknowledgments(&license_text) {
78+ Ok(Some(license_text))
79+ } else if license_not_needed_in_acknowledgements(&license_text) {
80+ Ok(None)
81+ } else {
82+ Err(format!("ERROR: License is of unknown type: {:?}", path).into())
83+ }
84+}
85+
86+fn include_license_in_acknowledgments(license_text: &str) -> bool {
87+ let markers = vec![
88+ // MIT
89+ "The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.",
90+
91+ // BSD
92+ "Redistributions in binary form must reproduce the above copyright notice,",
93+
94+ // Apache 2.0
95+ "Apache License Version 2.0, January 2004 http://www.apache.org/licenses/",
96+ "Licensed under the Apache License, Version 2.0 (the \"License\");",
97+ ];
98+
99+ license_contains_marker(license_text, &markers)
100+}
101+
102+fn license_not_needed_in_acknowledgements(license_text: &str) -> bool {
103+ let markers = vec![
104+ // Public domain
105+ "This is free and unencumbered software released into the public domain.",
106+
107+ // Special license of assets/syntaxes/01_Packages/LICENSE
108+ "Permission to copy, use, modify, sell and distribute this software is granted. This software is provided \"as is\" without express or implied warranty, and with no claim as to its suitability for any purpose."
109+ ];
110+
111+ license_contains_marker(license_text, &markers)
112+}
113+
114+fn license_contains_marker(license_text: &str, markers: &[&str]) -> bool {
115+ let normalized_license_text = normalize_license_text(license_text);
116+ markers.iter().any(|m| normalized_license_text.contains(m))
117+}
118+
119+fn append_to_acknowledgements(
120+ acknowledgements: &mut String,
121+ relative_path: &str,
122+ license_text: &str,
123+) {
124+ acknowledgements.push_str(&format!("## {}\n\n{}", relative_path, license_text));
125+
126+ // Make sure the last char is a newline to not mess up formatting later
127+ if acknowledgements
128+ .chars()
129+ .last()
130+ .expect("acknowledgements is not the empty string")
131+ != '\n'
132+ {
133+ acknowledgements.push('\n');
134+ }
135+
136+ // Add two more newlines to make it easy to distinguish where this text ends
137+ // and the next starts
138+ acknowledgements.push_str("\n\n");
139+}
140+
141+/// Replaces newlines with a space character, and replaces multiple spaces with one space.
142+/// This makes the text easier to analyze.
143+fn normalize_license_text(license_text: &str) -> String {
144+ use regex::Regex;
145+
146+ let whitespace_and_newlines = Regex::new(r"\s").unwrap();
147+ let as_single_line = whitespace_and_newlines.replace_all(license_text, " ");
148+
149+ let many_spaces = Regex::new(" +").unwrap();
150+ many_spaces.replace_all(&as_single_line, " ").to_string()
151+}
152+
153+#[cfg(test)]
154+mod tests {
155+ #[cfg(test)]
156+ use super::*;
157+
158+ #[test]
159+ fn test_normalize_license_text() {
160+ let license_text = "This is a license text with these terms:
161+ * Complicated multi-line
162+ term with indentation";
163+
164+ assert_eq!(
165+ "This is a license text with these terms: * Complicated multi-line term with indentation".to_owned(),
166+ normalize_license_text(license_text),
167+ );
168+ }
169+
170+ #[test]
171+ fn test_normalize_license_text_with_windows_line_endings() {
172+ let license_text = "This license text includes windows line endings\r
173+and we need to handle that.";
174+
175+ assert_eq!(
176+ "This license text includes windows line endings and we need to handle that."
177+ .to_owned(),
178+ normalize_license_text(license_text),
179+ );
180+ }
181+
182+ #[test]
183+ fn test_append_to_acknowledgements_adds_newline_if_missing() {
184+ let mut acknowledgements = "preamble\n\n\n".to_owned();
185+
186+ append_to_acknowledgements(&mut acknowledgements, "some/path", "line without newline");
187+ assert_eq!(
188+ "preamble
189+
190+
191+## some/path
192+
193+line without newline
194+
195+
196+",
197+ acknowledgements
198+ );
199+
200+ append_to_acknowledgements(&mut acknowledgements, "another/path", "line with newline\n");
201+ assert_eq!(
202+ "preamble
203+
204+
205+## some/path
206+
207+line without newline
208+
209+
210+## another/path
211+
212+line with newline
213+
214+
215+",
216+ acknowledgements
217+ );
218+ }
219+}
src/bin/bat/clap_app.rs+12
@@ -508,6 +508,12 @@ pub fn build_app(interactive_output: bool) -> ClapApp<'static, 'static> {
508508 .hidden_short_help(true)
509509 .help("Show diagnostic information for bug reports.")
510510 )
511+ .arg(
512+ Arg::with_name("acknowledgements")
513+ .long("acknowledgements")
514+ .hidden_short_help(true)
515+ .help("Show acknowledgements."),
516+ )
511517 .arg(
512518 Arg::with_name("ignored-suffix")
513519 .number_of_values(1)
@@ -578,6 +584,12 @@ pub fn build_app(interactive_output: bool) -> ClapApp<'static, 'static> {
578584 "Create completely new syntax and theme sets \
579585 (instead of appending to the default sets).",
580586 ),
587+ )
588+ .arg(
589+ Arg::with_name("acknowledgements")
590+ .long("acknowledgements")
591+ .requires("build")
592+ .help("Build acknowledgements.bin."),
581593 ),
582594 )
583595 }
src/bin/bat/main.rs+10−3
@@ -47,9 +47,13 @@ fn build_assets(matches: &clap::ArgMatches) -> Result<()> {
4747 .map(Path::new)
4848 .unwrap_or_else(|| PROJECT_DIRS.cache_dir());
4949
50− let blank = matches.is_present("blank");
51−
52− bat::assets::build(source_dir, !blank, target_dir, clap::crate_version!())
50+ bat::assets::build(
51+ source_dir,
52+ !matches.is_present("blank"),
53+ matches.is_present("acknowledgements"),
54+ target_dir,
55+ clap::crate_version!(),
56+ )
5357}
5458
5559fn run_cache_subcommand(matches: &clap::ArgMatches) -> Result<()> {
@@ -324,6 +328,9 @@ fn run() -> Result<bool> {
324328 } else if app.matches.is_present("cache-dir") {
325329 writeln!(io::stdout(), "{}", cache_dir())?;
326330 Ok(true)
331+ } else if app.matches.is_present("acknowledgements") {
332+ writeln!(io::stdout(), "{}", bat::assets::get_acknowledgements())?;
333+ Ok(true)
327334 } else {
328335 run_controller(inputs, &config)
329336 }
tests/integration_tests.rs+37
@@ -1,4 +1,5 @@
11use assert_cmd::cargo::CommandCargoExt;
2+use predicates::boolean::PredicateBooleanExt;
23use predicates::{prelude::predicate, str::PredicateStrExt};
34use serial_test::serial;
45use std::path::Path;
@@ -1275,3 +1276,39 @@ fn ignored_suffix_arg() {
12751276 .stdout("\u{1b}[38;5;231m{\"test\": \"value\"}\u{1b}[0m")
12761277 .stderr("");
12771278}
1279+
1280+#[test]
1281+fn acknowledgements() {
1282+ bat()
1283+ .arg("--acknowledgements")
1284+ .assert()
1285+ .success()
1286+ .stdout(
1287+ // Just some sanity checking that avoids names of persons, except our own Keith Hall :)
1288+ predicate::str::contains(
1289+ "Copyright (c) 2018-2021 bat-developers (https://github.com/sharkdp/bat).",
1290+ )
1291+ .and(predicate::str::contains(
1292+ "Copyright (c) 2012-2020 The Sublime CMake authors",
1293+ ))
1294+ .and(predicate::str::contains(
1295+ "Copyright 2014-2015 SaltStack Team",
1296+ ))
1297+ .and(predicate::str::contains(
1298+ "Copyright (c) 2013-present Dracula Theme",
1299+ ))
1300+ .and(predicate::str::contains(
1301+ "## syntaxes/01_Packages/Rust/LICENSE.txt",
1302+ ))
1303+ .and(predicate::str::contains(
1304+ "## syntaxes/02_Extra/http-request-response/LICENSE",
1305+ ))
1306+ .and(predicate::str::contains(
1307+ "## themes/dracula-sublime/LICENSE",
1308+ ))
1309+ .and(predicate::str::contains("Copyright (c) 2017 b123400"))
1310+ .and(predicate::str::contains("Copyright (c) 2021 Keith Hall"))
1311+ .and(predicate::str::contains("Copyright 2014 Clams")),
1312+ )
1313+ .stderr("");
1314+}