diff --git a/Cargo.lock b/Cargo.lock index 1408862..822848f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + [[package]] name = "aho-corasick" version = "1.1.3" @@ -127,12 +133,29 @@ dependencies = [ "nom", ] +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + [[package]] name = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "basic-toml" version = "0.1.8" @@ -142,6 +165,15 @@ dependencies = [ "serde", ] +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -223,7 +255,9 @@ name = "blue-build-utils" version = "0.8.4" dependencies = [ "anyhow", + "atty", "chrono", + "clap", "colored", "directories", "env_logger", @@ -233,6 +267,7 @@ dependencies = [ "serde", "serde_json", "serde_yaml 0.9.32", + "syntect", "which", ] @@ -400,6 +435,15 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +[[package]] +name = "crc32fast" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" +dependencies = [ + "cfg-if", +] + [[package]] name = "crossterm" version = "0.25.0" @@ -522,6 +566,22 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" +[[package]] +name = "flate2" +version = "1.0.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4556222738635b7a3417ae6130d8f52201e45a0c4d1a907f0826383adb5f85e7" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -610,6 +670,15 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + [[package]] name = "home" version = "0.5.9" @@ -790,6 +859,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "line-wrap" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd1bc4d24ad230d21fb898d1116b1801d7adfc449d42026475862ab48b11e70e" + [[package]] name = "linked-hash-map" version = "0.5.6" @@ -846,6 +921,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "miniz_oxide" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" +dependencies = [ + "adler", +] + [[package]] name = "mio" version = "0.8.10" @@ -910,6 +994,28 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "onig" +version = "6.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c4b31c8722ad9171c6d77d3557db078cab2bd50afcc9d09c8b315c59df8ca4f" +dependencies = [ + "bitflags 1.3.2", + "libc", + "once_cell", + "onig_sys", +] + +[[package]] +name = "onig_sys" +version = "69.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b829e3d7e9cc74c7e315ee8edb185bf4190da5acde74afd7fc59c35b1f086e7" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "open" version = "5.0.1" @@ -979,6 +1085,20 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +[[package]] +name = "plist" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9d34169e64b3c7a80c8621a48adaf44e0cf62c78a9b25dd9dd35f1881a17cf9" +dependencies = [ + "base64", + "indexmap 2.2.3", + "line-wrap", + "quick-xml", + "serde", + "time", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -1005,6 +1125,15 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "quick-xml" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33" +dependencies = [ + "memchr", +] + [[package]] name = "quote" version = "1.0.35" @@ -1156,6 +1285,15 @@ version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -1316,6 +1454,28 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syntect" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "874dcfa363995604333cf947ae9f751ca3af4522c60886774c4963943b4746b1" +dependencies = [ + "bincode", + "bitflags 1.3.2", + "flate2", + "fnv", + "once_cell", + "onig", + "plist", + "regex-syntax", + "serde", + "serde_derive", + "serde_json", + "thiserror", + "walkdir", + "yaml-rust", +] + [[package]] name = "tempfile" version = "3.10.0" @@ -1601,6 +1761,16 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -1690,6 +1860,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" +dependencies = [ + "windows-sys 0.52.0", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" diff --git a/Cargo.toml b/Cargo.toml index 4197f6d..0e665ea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ version = "0.8.4" [workspace.dependencies] anyhow = "1" chrono = "0.4.35" +clap = { version = "4", features = ["derive", "cargo", "unicode"] } colored = "2.1.0" env_logger = "0.11" format_serde_error = "0.3.0" @@ -54,7 +55,6 @@ pre-release-replacements = [ blue-build-recipe = { version = "=0.8.4", path = "./recipe" } blue-build-template = { version = "=0.8.4", path = "./template" } blue-build-utils = { version = "=0.8.4", path = "./utils" } -clap = { version = "4", features = ["derive", "cargo", "unicode"] } clap-verbosity-flag = "2" clap_complete = "4" clap_complete_nushell = "4" @@ -71,6 +71,7 @@ users = "0.11.0" # Workspace dependencies anyhow.workspace = true chrono.workspace = true +clap.workspace = true colored.workspace = true env_logger.workspace = true log.workspace = true diff --git a/recipe/src/recipe.rs b/recipe/src/recipe.rs index 7145169..833db05 100644 --- a/recipe/src/recipe.rs +++ b/recipe/src/recipe.rs @@ -30,7 +30,7 @@ pub struct Recipe<'a> { #[builder(setter(into))] pub image_version: Cow<'a, str>, - #[serde(alias = "blue-build-tag")] + #[serde(alias = "blue-build-tag", skip_serializing_if = "Option::is_none")] #[builder(default, setter(into, strip_option))] pub blue_build_tag: Option>, diff --git a/src/commands/template.rs b/src/commands/template.rs index bf8a85f..440bf4b 100644 --- a/src/commands/template.rs +++ b/src/commands/template.rs @@ -6,9 +6,12 @@ use std::{ use anyhow::Result; use blue_build_recipe::Recipe; use blue_build_template::{ContainerFileTemplate, Template}; -use blue_build_utils::constants::{ - CI_PROJECT_NAME, CI_PROJECT_NAMESPACE, CI_REGISTRY, CONFIG_PATH, GITHUB_REPOSITORY_OWNER, - RECIPE_FILE, RECIPE_PATH, +use blue_build_utils::{ + constants::{ + CI_PROJECT_NAME, CI_PROJECT_NAMESPACE, CI_REGISTRY, CONFIG_PATH, GITHUB_REPOSITORY_OWNER, + RECIPE_FILE, RECIPE_PATH, + }, + syntax_highlighting::{self, DefaultThemes}, }; use clap::Args; use log::{debug, info, trace, warn}; @@ -46,6 +49,23 @@ pub struct TemplateCommand { #[builder(default, setter(into, strip_option))] registry_namespace: Option, + /// Instead of creating a Containerfile, display + /// the full recipe after traversing all `from-file` properties. + /// + /// This can be used to help debug the order + /// you defined your recipe. + #[arg(short, long)] + #[builder(default)] + display_full_recipe: bool, + + /// Choose a theme for the syntax highlighting + /// for the Containerfile or Yaml. + /// + /// The default is `mocha-dark`. + #[arg(short = 't', long)] + #[builder(default, setter(strip_option))] + syntax_theme: Option, + #[clap(flatten)] #[builder(default)] drivers: DriverArgs, @@ -78,12 +98,21 @@ impl TemplateCommand { } }); - info!("Templating for recipe at {}", recipe_path.display()); - debug!("Deserializing recipe"); let recipe_de = Recipe::parse(&recipe_path)?; trace!("recipe_de: {recipe_de:#?}"); + if self.display_full_recipe { + if let Some(output) = self.output.as_ref() { + std::fs::write(output, serde_yaml::to_string(&recipe_de)?)?; + } else { + syntax_highlighting::print_ser(&recipe_de, "yml", self.syntax_theme)?; + } + return Ok(()); + } + + info!("Templating for recipe at {}", recipe_path.display()); + let template = ContainerFileTemplate::builder() .os_version(Driver::get_os_version(&recipe_de)?) .build_id(Driver::get_build_id()) @@ -101,10 +130,9 @@ impl TemplateCommand { std::fs::write(output, output_str)?; } else { debug!("Templating to stdout"); - println!("{output_str}"); + syntax_highlighting::print(&output_str, "Dockerfile", self.syntax_theme)?; } - info!("Finished templating Containerfile"); Ok(()) } diff --git a/utils/Cargo.toml b/utils/Cargo.toml index f406eed..6a763e4 100644 --- a/utils/Cargo.toml +++ b/utils/Cargo.toml @@ -9,8 +9,10 @@ license.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +atty = "0.2.14" directories = "5" process_control = { version = "4.0.3", features = ["crossbeam-channel"] } +syntect = "5.2.0" which = "6" anyhow.workspace = true @@ -23,6 +25,13 @@ serde.workspace = true serde_yaml.workspace = true serde_json.workspace = true +[dependencies.clap] +workspace = true +features = ["derive"] + +[build-dependencies] +syntect = "5.2.0" + [lints] workspace = true diff --git a/utils/build.rs b/utils/build.rs new file mode 100644 index 0000000..563466f --- /dev/null +++ b/utils/build.rs @@ -0,0 +1,24 @@ +use std::env; +use std::path::PathBuf; +use syntect::dumps; +use syntect::parsing::syntax_definition::SyntaxDefinition; +use syntect::parsing::SyntaxSetBuilder; + +fn main() { + let mut ssb = SyntaxSetBuilder::new(); + ssb.add( + SyntaxDefinition::load_from_str( + include_str!("highlights/Dockerfile.sublime-syntax"), + true, + None, + ) + .unwrap(), + ); + let ss = ssb.build(); + + dumps::dump_to_uncompressed_file( + &ss, + PathBuf::from(env::var("OUT_DIR").unwrap()).join("docker_syntax.bin"), + ) + .unwrap(); +} diff --git a/utils/highlights/Dockerfile.sublime-syntax b/utils/highlights/Dockerfile.sublime-syntax new file mode 100644 index 0000000..735c9ff --- /dev/null +++ b/utils/highlights/Dockerfile.sublime-syntax @@ -0,0 +1,141 @@ +%YAML 1.2 +# The MIT License (MIT) +# +# Copyright 2014 Asbjorn Enge +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# https://github.com/asbjornenge/Docker.tmbundle +--- +# http://www.sublimetext.com/docs/3/syntax.html +name: Dockerfile +scope: source.dockerfile + +file_extensions: + - Dockerfile + - dockerfile + +hidden_file_extensions: + - .Dockerfile + +first_line_match: ^\s*(?i:(from(?!\s+\S+\s+import)|arg))\s+ + +variables: + onbuild_directive: (?i:(onbuild)\s+)? + onbuild_commands_directive: + "{{onbuild_directive}}(?i:add|arg|env|expose|healthcheck|label|run|shell|stopsignal|user|volume|workdir)" + nononbuild_commands_directive: (?i:maintainer) + runtime_directive: "{{onbuild_directive}}(?i:cmd|entrypoint)" + from_directive: (?i:(from))\s+[^\s:@]+(?:[:@](\S+))?(?:\s+(?i:(as))\s+(\S+))? + copy_directive: ({{onbuild_directive}}(?i:copy))(?:\s+--from=(\S+))? + +contexts: + main: + - include: comments + - match: ^(?i:arg)\s + scope: keyword.control.dockerfile + - include: from + + from: + - match: ^{{from_directive}} + captures: + 1: keyword.control.dockerfile + 2: entity.name.enum.tag-digest + 3: keyword.control.dockerfile + 4: variable.stage-name + push: body + + body: + - include: comments + - include: directives + - include: invalid + - include: from + + directives: + - match: ^\s*{{onbuild_commands_directive}}\s + captures: + 0: keyword.control.dockerfile + 1: keyword.other.special-method.dockerfile + push: args + - match: ^\s*{{nononbuild_commands_directive}}\s + scope: keyword.control.dockerfile + push: args + - match: ^\s*{{copy_directive}}\s + captures: + 1: keyword.control.dockerfile + 2: keyword.other.special-method.dockerfile + 3: variable.stage-name + push: args + - match: ^\s*{{runtime_directive}}\s + captures: + 0: keyword.operator.dockerfile + 1: keyword.other.special-method.dockerfile + push: args + + escaped-char: + - match: \\. + scope: constant.character.escaped.dockerfile + + args: + - include: comments + - include: escaped-char + - match: ^\s*$ + - match: \\\s+$ + - match: \n + pop: true + - match: '"' + scope: punctuation.definition.string.begin.dockerfile + push: double_quote_string + - match: "'" + scope: punctuation.definition.string.begin.dockerfile + push: single_quote_string + + double_quote_string: + - meta_scope: string.quoted.double.dockerfile + - include: escaped-char + - match: ^\s*$ + - match: \\\s+$ + - match: \n + set: invalid + - match: '"' + scope: punctuation.definition.string.end.dockerfile + pop: true + + single_quote_string: + - meta_scope: string.quoted.single.dockerfile + - include: escaped-char + - match: ^\s*$ + - match: \\\s+$ + - match: \n + set: invalid + - match: "'" + scope: punctuation.definition.string.end.dockerfile + pop: true + + comments: + - match: ^(\s*)((#).*$\n?) + comment: comment.line + captures: + 1: punctuation.whitespace.comment.leading.dockerfile + 2: comment.dockerfile + 3: punctuation.definition.comment.dockerfile + + invalid: + - match: ^[^A-Z\n](.*)$ + scope: invalid + set: body diff --git a/utils/src/lib.rs b/utils/src/lib.rs index 2900d0d..a9a61c6 100644 --- a/utils/src/lib.rs +++ b/utils/src/lib.rs @@ -1,6 +1,7 @@ pub mod command_output; pub mod constants; pub mod logging; +pub mod syntax_highlighting; use std::{ffi::OsStr, io::Write, path::PathBuf, process::Command, thread, time::Duration}; diff --git a/utils/src/syntax_highlighting.rs b/utils/src/syntax_highlighting.rs new file mode 100644 index 0000000..f3f3c0e --- /dev/null +++ b/utils/src/syntax_highlighting.rs @@ -0,0 +1,85 @@ +use anyhow::{anyhow, Result}; +use clap::ValueEnum; +use log::trace; +use serde::ser::Serialize; +use syntect::{dumps, easy::HighlightLines, highlighting::ThemeSet, parsing::SyntaxSet}; + +#[derive(Debug, Default, Clone, Copy, ValueEnum)] +pub enum DefaultThemes { + #[default] + MochaDark, + OceanDark, + OceanLight, + EightiesDark, + InspiredGithub, + SolarizedDark, + SolarizedLight, +} + +impl std::fmt::Display for DefaultThemes { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match *self { + Self::MochaDark => "base16-mocha.dark", + Self::OceanDark => "base16-ocean.dark", + Self::OceanLight => "base16-ocean.light", + Self::EightiesDark => "base16-eighties.dark", + Self::InspiredGithub => "InspiredGithub", + Self::SolarizedDark => "Solarized (dark)", + Self::SolarizedLight => "Solarized (light)", + }) + } +} + +/// Prints the file with syntax highlighting. +/// +/// # Errors +/// Will error if the theme doesn't exist, the syntax doesn't exist, or the file +/// failed to serialize. +pub fn print(file: &str, file_type: &str, theme: Option) -> Result<()> { + trace!("syntax_highlighting::print({file}, {file_type}, {theme:?})"); + + if atty::is(atty::Stream::Stdout) { + let ss: SyntaxSet = if file_type == "dockerfile" || file_type == "Dockerfile" { + dumps::from_uncompressed_data(include_bytes!(concat!( + env!("OUT_DIR"), + "/docker_syntax.bin" + )))? + } else { + SyntaxSet::load_defaults_newlines() + }; + let ts = ThemeSet::load_defaults(); + + let syntax = ss + .find_syntax_by_extension(file_type) + .ok_or_else(|| anyhow!("Failed to get syntax"))?; + let mut h = HighlightLines::new( + syntax, + ts.themes + .get(theme.unwrap_or_default().to_string().as_str()) + .ok_or_else(|| anyhow!("Failed to get highlight theme"))?, + ); + for line in file.lines() { + let ranges = h.highlight_line(line, &ss)?; + let escaped = syntect::util::as_24_bit_terminal_escaped(&ranges, false); + println!("{escaped}"); + } + println!("\x1b[0m"); + } else { + println!("{file}"); + } + Ok(()) +} + +/// Takes a serializable struct and prints it out with syntax highlighting. +/// +/// # Errors +/// Will error if the theme doesn't exist, the syntax doesn't exist, or the file +/// failed to serialize. +pub fn print_ser( + file: &T, + file_type: &str, + theme: Option, +) -> Result<()> { + print(serde_yaml::to_string(file)?.as_str(), file_type, theme)?; + Ok(()) +}