Skip to content

Commit a3ea798

Browse files
authored
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.
1 parent 63ad538 commit a3ea798

12 files changed

+325
-7
lines changed

CHANGELOG.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -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
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+3-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ minimal-application = [
3434
git = ["git2"] # Support indicating git modifications
3535
paging = ["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:
4040
regex-onig = ["syntect/regex-onig"] # Use the "oniguruma" regex engine
@@ -63,6 +63,8 @@ clircle = "0.3"
6363
bugreport = { version = "0.4", optional = true }
6464
dirs-next = { version = "2.0.0", optional = true }
6565
grep-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]
6870
version = "0.13"

NOTICE

+6
Original file line numberDiff line numberDiff line change
@@ -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.bin

9.43 KB
Binary file not shown.

assets/create.sh

+1-1
Original file line numberDiff line numberDiff line change
@@ -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
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ pub(crate) const COMPRESS_THEMES: bool = false;
5555
/// performance due to lazy-loading
5656
pub(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+
5861
impl 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+
308318
pub(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
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,14 @@ use syntect::highlighting::ThemeSet;
55
use syntect::parsing::{SyntaxSet, SyntaxSetBuilder};
66

77
use crate::assets::*;
8+
use acknowledgements::build_acknowledgements;
9+
10+
mod acknowledgements;
811

912
pub 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

2638
fn build_theme_set(source_dir: &Path, include_integrated_assets: bool) -> Result<LazyThemeSet> {
@@ -89,6 +101,7 @@ fn print_unlinked_contexts(syntax_set: &SyntaxSet) {
89101
fn 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()
+219
Original file line numberDiff line numberDiff line change
@@ -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
Original file line numberDiff line numberDiff line change
@@ -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
}

0 commit comments

Comments
 (0)