2024-04-09 13:35:08 -04:00
use std ::{
env , fs ,
io ::Write ,
path ::{ Path , PathBuf } ,
process ::{ Command , Stdio } ,
2025-01-20 19:44:59 -05:00
sync ::LazyLock ,
2024-04-09 13:35:08 -04:00
} ;
2024-02-21 11:47:59 -05:00
2024-12-24 19:16:19 -05:00
use anyhow ::Result ;
2024-04-09 17:53:37 -07:00
use regex ::{ Regex , RegexBuilder } ;
use semver ::Version ;
2025-01-21 01:59:24 -05:00
use serde ::{ Deserialize , Serialize } ;
use thiserror ::Error ;
2024-02-21 11:47:59 -05:00
2019-01-07 10:23:01 -08:00
mod build_tables ;
2019-07-19 12:39:24 -07:00
mod dedup ;
2024-02-21 11:47:59 -05:00
mod grammar_files ;
2019-01-07 10:23:01 -08:00
mod grammars ;
mod nfa ;
2019-03-26 14:42:32 -07:00
mod node_types ;
2019-04-23 14:29:46 -07:00
pub mod parse_grammar ;
2019-01-07 10:23:01 -08:00
mod prepare_grammar ;
mod render ;
mod rules ;
mod tables ;
2025-01-21 01:59:24 -05:00
use build_tables ::build_tables ;
2024-12-24 19:16:19 -05:00
pub use build_tables ::ParseTableBuilderError ;
2025-01-21 01:59:24 -05:00
use grammars ::InputGrammar ;
2025-03-11 17:21:33 -04:00
pub use node_types ::{ SuperTypeCycleError , VariableInfoError } ;
2025-01-21 01:59:24 -05:00
use parse_grammar ::parse_grammar ;
pub use parse_grammar ::ParseGrammarError ;
use prepare_grammar ::prepare_grammar ;
pub use prepare_grammar ::PrepareGrammarError ;
use render ::render_c_code ;
2025-01-26 12:31:52 -05:00
pub use render ::{ ABI_VERSION_MAX , ABI_VERSION_MIN } ;
2024-12-24 19:16:19 -05:00
2025-01-20 19:44:59 -05:00
static JSON_COMMENT_REGEX : LazyLock < Regex > = LazyLock ::new ( | | {
RegexBuilder ::new ( " ^ \\ s*//.* " )
2019-01-11 13:30:45 -08:00
. multi_line ( true )
. build ( )
2025-01-20 19:44:59 -05:00
. unwrap ( )
} ) ;
2019-01-11 13:30:45 -08:00
2019-02-12 11:06:18 -08:00
struct GeneratedParser {
c_code : String ,
2019-03-26 13:43:10 -07:00
node_types_json : String ,
2019-02-12 11:06:18 -08:00
}
2024-10-04 00:47:30 -04:00
pub const ALLOC_HEADER : & str = include_str! ( " templates/alloc.h " ) ;
pub const ARRAY_HEADER : & str = include_str! ( " templates/array.h " ) ;
2024-03-10 16:38:53 -04:00
2024-12-24 19:16:19 -05:00
pub type GenerateResult < T > = Result < T , GenerateError > ;
#[ derive(Debug, Error, Serialize) ]
pub enum GenerateError {
#[ error( " Error with specified path -- {0} " ) ]
GrammarPath ( String ) ,
#[ error( " {0} " ) ]
IO ( String ) ,
#[ error(transparent) ]
LoadGrammarFile ( #[ from ] LoadGrammarError ) ,
#[ error(transparent) ]
ParseGrammar ( #[ from ] ParseGrammarError ) ,
#[ error(transparent) ]
Prepare ( #[ from ] PrepareGrammarError ) ,
#[ error(transparent) ]
VariableInfo ( #[ from ] VariableInfoError ) ,
#[ error(transparent) ]
BuildTables ( #[ from ] ParseTableBuilderError ) ,
2025-01-21 01:59:24 -05:00
#[ error(transparent) ]
ParseVersion ( #[ from ] ParseVersionError ) ,
2025-03-11 17:21:33 -04:00
#[ error(transparent) ]
SuperTypeCycle ( #[ from ] SuperTypeCycleError ) ,
2024-12-24 19:16:19 -05:00
}
impl From < std ::io ::Error > for GenerateError {
fn from ( value : std ::io ::Error ) -> Self {
Self ::IO ( value . to_string ( ) )
}
}
pub type LoadGrammarFileResult < T > = Result < T , LoadGrammarError > ;
#[ derive(Debug, Error, Serialize) ]
pub enum LoadGrammarError {
#[ error( " Path to a grammar file with `.js` or `.json` extension is required " ) ]
InvalidPath ,
#[ error( " Failed to load grammar.js -- {0} " ) ]
LoadJSGrammarFile ( #[ from ] JSError ) ,
#[ error( " Failed to load grammar.json -- {0} " ) ]
IO ( String ) ,
#[ error( " Unknown grammar file extension: {0:?} " ) ]
FileExtension ( PathBuf ) ,
}
impl From < std ::io ::Error > for LoadGrammarError {
fn from ( value : std ::io ::Error ) -> Self {
Self ::IO ( value . to_string ( ) )
}
}
2025-01-21 01:59:24 -05:00
#[ derive(Debug, Error, Serialize) ]
pub enum ParseVersionError {
#[ error( " {0} " ) ]
Version ( String ) ,
#[ error( " {0} " ) ]
JSON ( String ) ,
#[ error( " {0} " ) ]
IO ( String ) ,
}
2024-12-24 19:16:19 -05:00
pub type JSResult < T > = Result < T , JSError > ;
#[ derive(Debug, Error, Serialize) ]
pub enum JSError {
#[ error( " Failed to run `{runtime}` -- {error} " ) ]
JSRuntimeSpawn { runtime : String , error : String } ,
#[ error( " Got invalid UTF8 from `{runtime}` -- {error} " ) ]
JSRuntimeUtf8 { runtime : String , error : String } ,
#[ error( " `{runtime}` process exited with status {code} " ) ]
JSRuntimeExit { runtime : String , code : i32 } ,
#[ error( " {0} " ) ]
IO ( String ) ,
#[ error( " Could not parse this package's version as semver -- {0} " ) ]
Semver ( String ) ,
#[ error( " Failed to serialze grammar JSON -- {0} " ) ]
Serialzation ( String ) ,
}
impl From < std ::io ::Error > for JSError {
fn from ( value : std ::io ::Error ) -> Self {
Self ::IO ( value . to_string ( ) )
}
}
impl From < serde_json ::Error > for JSError {
fn from ( value : serde_json ::Error ) -> Self {
Self ::Serialzation ( value . to_string ( ) )
}
}
impl From < semver ::Error > for JSError {
fn from ( value : semver ::Error ) -> Self {
Self ::Semver ( value . to_string ( ) )
}
}
2019-01-11 13:30:45 -08:00
pub fn generate_parser_in_directory (
2024-02-04 01:30:33 -05:00
repo_path : & Path ,
2023-09-09 19:25:14 +02:00
out_path : Option < & str > ,
2019-01-14 14:07:42 -08:00
grammar_path : Option < & str > ,
2025-01-26 12:31:52 -05:00
mut abi_version : usize ,
2019-05-20 13:25:01 -07:00
report_symbol_name : Option < & str > ,
2023-07-18 13:24:52 +02:00
js_runtime : Option < & str > ,
2024-12-24 19:16:19 -05:00
) -> GenerateResult < ( ) > {
2024-02-21 11:47:59 -05:00
let mut repo_path = repo_path . to_owned ( ) ;
let mut grammar_path = grammar_path ;
// Populate a new empty grammar directory.
if let Some ( path ) = grammar_path {
let path = PathBuf ::from ( path ) ;
if ! path
. try_exists ( )
2024-12-24 19:16:19 -05:00
. map_err ( | e | GenerateError ::GrammarPath ( e . to_string ( ) ) ) ?
2024-02-21 11:47:59 -05:00
{
fs ::create_dir_all ( & path ) ? ;
grammar_path = None ;
repo_path = path ;
}
}
2024-10-06 17:55:00 -04:00
let grammar_path = grammar_path . map_or_else ( | | repo_path . join ( " grammar.js " ) , PathBuf ::from ) ;
2024-09-24 13:55:16 -04:00
// Read the grammar file.
let grammar_json = load_grammar_file ( & grammar_path , js_runtime ) ? ;
2023-07-30 20:43:52 +03:00
2024-10-06 17:55:00 -04:00
let src_path = out_path . map_or_else ( | | repo_path . join ( " src " ) , PathBuf ::from ) ;
2024-02-21 11:47:59 -05:00
let header_path = src_path . join ( " tree_sitter " ) ;
2023-07-30 20:43:52 +03:00
// Ensure that the output directories exist.
fs ::create_dir_all ( & src_path ) ? ;
fs ::create_dir_all ( & header_path ) ? ;
2024-09-24 13:55:16 -04:00
if grammar_path . file_name ( ) . unwrap ( ) ! = " grammar.json " {
2024-12-24 19:16:19 -05:00
fs ::write ( src_path . join ( " grammar.json " ) , & grammar_json ) . map_err ( | e | {
GenerateError ::IO ( format! (
2025-03-03 01:37:48 -05:00
" Failed to write grammar.json to {} -- {e} " ,
src_path . display ( )
2024-12-24 19:16:19 -05:00
) )
} ) ? ;
2019-02-08 15:15:47 -08:00
}
2019-07-19 13:11:08 -07:00
// Parse and preprocess the grammar.
let input_grammar = parse_grammar ( & grammar_json ) ? ;
2025-01-21 01:59:24 -05:00
let semantic_version = read_grammar_version ( & repo_path ) ? ;
2025-01-26 12:31:52 -05:00
if semantic_version . is_none ( ) & & abi_version > ABI_VERSION_MIN {
println! ( " Warning: No `tree-sitter.json` file found in your grammar, this file is required to generate with ABI {abi_version} . Using ABI version {ABI_VERSION_MIN} instead. " ) ;
println! ( " This file can be set up with `tree-sitter init`. For more information, see https://tree-sitter.github.io/tree-sitter/cli/init. " ) ;
abi_version = ABI_VERSION_MIN ;
}
2019-07-19 13:11:08 -07:00
// Generate the parser and related files.
2019-10-17 11:00:31 -07:00
let GeneratedParser {
c_code ,
node_types_json ,
2025-01-21 01:59:24 -05:00
} = generate_parser_for_grammar_with_opts (
& input_grammar ,
abi_version ,
semantic_version . map ( | v | ( v . major as u8 , v . minor as u8 , v . patch as u8 ) ) ,
report_symbol_name ,
) ? ;
2019-08-28 17:14:04 -07:00
2019-10-17 11:00:31 -07:00
write_file ( & src_path . join ( " parser.c " ) , c_code ) ? ;
write_file ( & src_path . join ( " node-types.json " ) , node_types_json ) ? ;
2024-03-10 16:38:53 -04:00
write_file ( & header_path . join ( " alloc.h " ) , ALLOC_HEADER ) ? ;
2024-09-27 15:12:03 -04:00
write_file ( & header_path . join ( " array.h " ) , ARRAY_HEADER ) ? ;
2022-01-23 10:29:52 -08:00
write_file ( & header_path . join ( " parser.h " ) , tree_sitter ::PARSER_HEADER ) ? ;
2019-07-19 13:11:08 -07:00
2019-01-09 14:43:49 -08:00
Ok ( ( ) )
2019-01-07 10:23:01 -08:00
}
2025-01-21 01:59:24 -05:00
pub fn generate_parser_for_grammar (
grammar_json : & str ,
semantic_version : Option < ( u8 , u8 , u8 ) > ,
) -> GenerateResult < ( String , String ) > {
2019-01-11 13:30:45 -08:00
let grammar_json = JSON_COMMENT_REGEX . replace_all ( grammar_json , " \n " ) ;
2019-07-19 13:11:08 -07:00
let input_grammar = parse_grammar ( & grammar_json ) ? ;
2025-01-21 01:59:24 -05:00
let parser = generate_parser_for_grammar_with_opts (
& input_grammar ,
tree_sitter ::LANGUAGE_VERSION ,
semantic_version ,
None ,
) ? ;
2024-09-07 20:13:58 -04:00
Ok ( ( input_grammar . name , parser . c_code ) )
2019-01-11 13:30:45 -08:00
}
fn generate_parser_for_grammar_with_opts (
2024-04-09 17:53:37 -07:00
input_grammar : & InputGrammar ,
2022-01-17 14:45:07 -08:00
abi_version : usize ,
2025-01-21 01:59:24 -05:00
semantic_version : Option < ( u8 , u8 , u8 ) > ,
2019-05-20 13:25:01 -07:00
report_symbol_name : Option < & str > ,
2024-12-24 19:16:19 -05:00
) -> GenerateResult < GeneratedParser > {
2024-04-09 17:53:37 -07:00
let ( syntax_grammar , lexical_grammar , inlines , simple_aliases ) =
prepare_grammar ( input_grammar ) ? ;
2019-11-13 10:38:47 -08:00
let variable_info =
node_types ::get_variable_info ( & syntax_grammar , & lexical_grammar , & simple_aliases ) ? ;
2019-03-27 16:17:02 -07:00
let node_types_json = node_types ::generate_node_types_json (
& syntax_grammar ,
& lexical_grammar ,
& simple_aliases ,
& variable_info ,
2025-07-13 13:11:16 -04:00
) ? ;
2024-11-12 11:43:00 -08:00
let supertype_symbol_map =
node_types ::get_supertype_symbol_map ( & syntax_grammar , & simple_aliases , & variable_info ) ;
2024-04-09 17:53:37 -07:00
let tables = build_tables (
2019-01-11 13:30:45 -08:00
& syntax_grammar ,
& lexical_grammar ,
& simple_aliases ,
2019-03-27 16:17:02 -07:00
& variable_info ,
2024-04-09 17:53:37 -07:00
& inlines ,
2019-05-20 13:25:01 -07:00
report_symbol_name ,
2019-01-11 13:30:45 -08:00
) ? ;
2019-01-16 13:53:01 -08:00
let c_code = render_c_code (
2024-04-09 17:53:37 -07:00
& input_grammar . name ,
tables ,
2019-01-11 13:30:45 -08:00
syntax_grammar ,
lexical_grammar ,
simple_aliases ,
2022-01-17 14:45:07 -08:00
abi_version ,
2025-01-21 01:59:24 -05:00
semantic_version ,
2024-11-12 11:43:00 -08:00
supertype_symbol_map ,
2019-01-16 13:53:01 -08:00
) ;
2019-02-12 11:06:18 -08:00
Ok ( GeneratedParser {
c_code ,
2019-03-26 14:42:32 -07:00
node_types_json : serde_json ::to_string_pretty ( & node_types_json ) . unwrap ( ) ,
2019-02-12 11:06:18 -08:00
} )
2019-01-11 13:30:45 -08:00
}
2025-01-21 01:59:24 -05:00
/// This will read the `tree-sitter.json` config file and attempt to extract the version.
///
/// If the file is not found in the current directory or any of its parent directories, this will
/// return `None` to maintain backwards compatibility. If the file is found but the version cannot
/// be parsed as semver, this will return an error.
fn read_grammar_version ( repo_path : & Path ) -> Result < Option < Version > , ParseVersionError > {
#[ derive(Deserialize) ]
struct TreeSitterJson {
metadata : Metadata ,
}
#[ derive(Deserialize) ]
struct Metadata {
version : String ,
}
let filename = " tree-sitter.json " ;
let mut path = repo_path . join ( filename ) ;
loop {
let json = path
. exists ( )
. then ( | | {
let contents = fs ::read_to_string ( path . as_path ( ) ) . map_err ( | e | {
ParseVersionError ::IO ( format! ( " Failed to read ` {} ` -- {e} " , path . display ( ) ) )
} ) ? ;
serde_json ::from_str ::< TreeSitterJson > ( & contents ) . map_err ( | e | {
ParseVersionError ::JSON ( format! ( " Failed to parse ` {} ` -- {e} " , path . display ( ) ) )
} )
} )
. transpose ( ) ? ;
if let Some ( json ) = json {
return Version ::parse ( & json . metadata . version )
. map_err ( | e | {
ParseVersionError ::Version ( format! (
" Failed to parse `{}` version as semver -- {e} " ,
path . display ( )
) )
} )
. map ( Some ) ;
}
path . pop ( ) ; // filename
if ! path . pop ( ) {
return Ok ( None ) ;
}
path . push ( filename ) ;
}
}
2024-12-24 19:16:19 -05:00
pub fn load_grammar_file (
grammar_path : & Path ,
js_runtime : Option < & str > ,
) -> LoadGrammarFileResult < String > {
2023-07-30 20:43:52 +03:00
if grammar_path . is_dir ( ) {
2024-12-24 19:16:19 -05:00
Err ( LoadGrammarError ::InvalidPath ) ? ;
2023-07-30 20:43:52 +03:00
}
2019-01-14 14:07:42 -08:00
match grammar_path . extension ( ) . and_then ( | e | e . to_str ( ) ) {
2024-12-24 19:16:19 -05:00
Some ( " js " ) = > Ok ( load_js_grammar_file ( grammar_path , js_runtime ) ? ) ,
Some ( " json " ) = > Ok ( fs ::read_to_string ( grammar_path ) ? ) ,
_ = > Err ( LoadGrammarError ::FileExtension ( grammar_path . to_owned ( ) ) ) ? ,
2019-01-14 14:07:42 -08:00
}
}
2024-12-24 19:16:19 -05:00
fn load_js_grammar_file ( grammar_path : & Path , js_runtime : Option < & str > ) -> JSResult < String > {
2021-08-05 03:50:10 +03:00
let grammar_path = fs ::canonicalize ( grammar_path ) ? ;
2023-07-18 13:24:52 +02:00
2024-05-24 23:53:33 +03:00
#[ cfg(windows) ]
let grammar_path = url ::Url ::from_file_path ( grammar_path )
. expect ( " Failed to convert path to URL " )
. to_string ( ) ;
2023-07-18 13:24:52 +02:00
let js_runtime = js_runtime . unwrap_or ( " node " ) ;
2024-05-24 23:53:33 +03:00
let mut js_command = Command ::new ( js_runtime ) ;
match js_runtime {
" node " = > {
js_command . args ( [ " --input-type=module " , " - " ] ) ;
}
" bun " = > {
js_command . arg ( " - " ) ;
}
" deno " = > {
js_command . args ( [ " run " , " --allow-all " , " - " ] ) ;
}
_ = > { }
}
let mut js_process = js_command
2019-01-17 10:09:03 -08:00
. env ( " TREE_SITTER_GRAMMAR_PATH " , grammar_path )
2019-01-07 10:23:01 -08:00
. stdin ( Stdio ::piped ( ) )
. stdout ( Stdio ::piped ( ) )
. spawn ( )
2024-12-24 19:16:19 -05:00
. map_err ( | e | JSError ::JSRuntimeSpawn {
runtime : js_runtime . to_string ( ) ,
error : e . to_string ( ) ,
} ) ? ;
2019-01-07 10:23:01 -08:00
2024-05-24 23:53:33 +03:00
let mut js_stdin = js_process
2019-01-07 10:23:01 -08:00
. stdin
. take ( )
2024-12-24 19:16:19 -05:00
. ok_or_else ( | | JSError ::IO ( format! ( " Failed to open stdin for ` {js_runtime} ` " ) ) ) ? ;
let cli_version = Version ::parse ( env! ( " CARGO_PKG_VERSION " ) ) ? ;
Expand regex support to include emojis and binary ops
The `Emoji` property alias is already present, but the actual property
is not available since it lives in a new file. This adds that file to
the `generate-unicode-categories-json`.
The `emoji-data` file follows the same format as the ones we already
consume in `generate-unicode-categories-json`, so adding emoji support
is fairly easy. his, grammars would need to hard-code a set of
unicode ranges in their own regex. The Javascript library `emoji-regex`
cannot be used because of #451.
For unclear reasons, the characters #, *, and 0-9 are marked as
`Emoji=Yes` by `emoji-data.txt`. Because of this, a grammar that wishes
to use emojis is likely to want to exclude those characters. For that
reason, this change also adds support for binary operations in regexes,
e.g. `[\p{Emoji}&&[^#*0-9]]`.
Lastly (and perhaps controversially), this change introduces new
variables available at grammar compile time, for the major, minor, and
patch versions of the tree-sitter CLI used to compile the grammar. This
will allow grammars to conditionally adopt these new regex features
while remaining backward compatible with older versions of the CLI.
Without this part of the change, grammar authors who do not precompile
and check-in their `grammar.json` would need to wait for downstream
systems to adopt a newer tree-sitter CLI version before they could begin
to use these features.
2022-02-14 21:46:12 -08:00
write! (
2024-05-24 23:53:33 +03:00
js_stdin ,
" globalThis.TREE_SITTER_CLI_VERSION_MAJOR = {};
2024-05-25 00:48:13 -04:00
globalThis . TREE_SITTER_CLI_VERSION_MINOR = { } ;
globalThis . TREE_SITTER_CLI_VERSION_PATCH = { } ; " ,
Expand regex support to include emojis and binary ops
The `Emoji` property alias is already present, but the actual property
is not available since it lives in a new file. This adds that file to
the `generate-unicode-categories-json`.
The `emoji-data` file follows the same format as the ones we already
consume in `generate-unicode-categories-json`, so adding emoji support
is fairly easy. his, grammars would need to hard-code a set of
unicode ranges in their own regex. The Javascript library `emoji-regex`
cannot be used because of #451.
For unclear reasons, the characters #, *, and 0-9 are marked as
`Emoji=Yes` by `emoji-data.txt`. Because of this, a grammar that wishes
to use emojis is likely to want to exclude those characters. For that
reason, this change also adds support for binary operations in regexes,
e.g. `[\p{Emoji}&&[^#*0-9]]`.
Lastly (and perhaps controversially), this change introduces new
variables available at grammar compile time, for the major, minor, and
patch versions of the tree-sitter CLI used to compile the grammar. This
will allow grammars to conditionally adopt these new regex features
while remaining backward compatible with older versions of the CLI.
Without this part of the change, grammar authors who do not precompile
and check-in their `grammar.json` would need to wait for downstream
systems to adopt a newer tree-sitter CLI version before they could begin
to use these features.
2022-02-14 21:46:12 -08:00
cli_version . major , cli_version . minor , cli_version . patch ,
)
2024-12-24 19:16:19 -05:00
. map_err ( | e | {
JSError ::IO ( format! (
" Failed to write tree-sitter version to `{js_runtime}`'s stdin -- {e} "
) )
} ) ? ;
js_stdin . write ( include_bytes! ( " ./dsl.js " ) ) . map_err ( | e | {
JSError ::IO ( format! (
" Failed to write grammar dsl to `{js_runtime}`'s stdin -- {e} "
) )
} ) ? ;
2024-05-24 23:53:33 +03:00
drop ( js_stdin ) ;
let output = js_process
2019-01-07 10:23:01 -08:00
. wait_with_output ( )
2024-12-24 19:16:19 -05:00
. map_err ( | e | JSError ::IO ( format! ( " Failed to read output from ` {js_runtime} ` -- {e} " ) ) ) ? ;
2019-01-07 10:23:01 -08:00
match output . status . code ( ) {
2024-12-24 19:16:19 -05:00
None = > panic! ( " ` {js_runtime} ` process was killed " ) ,
2024-02-05 00:42:05 -05:00
Some ( 0 ) = > {
2024-12-24 19:16:19 -05:00
let stdout = String ::from_utf8 ( output . stdout ) . map_err ( | e | JSError ::JSRuntimeUtf8 {
runtime : js_runtime . to_string ( ) ,
error : e . to_string ( ) ,
} ) ? ;
2024-02-05 00:42:05 -05:00
let mut grammar_json = & stdout [ .. ] ;
if let Some ( pos ) = stdout . rfind ( '\n' ) {
// If there's a newline, split the last line from the rest of the output
2024-02-07 07:13:03 -05:00
let node_output = & stdout [ .. pos ] ;
2024-02-05 00:42:05 -05:00
grammar_json = & stdout [ pos + 1 .. ] ;
let mut stdout = std ::io ::stdout ( ) . lock ( ) ;
stdout . write_all ( node_output . as_bytes ( ) ) ? ;
stdout . write_all ( b " \n " ) ? ;
stdout . flush ( ) ? ;
}
2024-12-24 19:16:19 -05:00
Ok ( serde_json ::to_string_pretty ( & serde_json ::from_str ::<
serde_json ::Value ,
> ( grammar_json ) ? ) ? )
2024-02-05 00:42:05 -05:00
}
2024-12-24 19:16:19 -05:00
Some ( code ) = > Err ( JSError ::JSRuntimeExit {
runtime : js_runtime . to_string ( ) ,
code ,
} ) ,
2019-01-07 10:23:01 -08:00
}
}
2019-01-17 12:50:30 -08:00
2024-12-24 19:16:19 -05:00
pub fn write_file ( path : & Path , body : impl AsRef < [ u8 ] > ) -> GenerateResult < ( ) > {
2021-06-09 12:32:22 -04:00
fs ::write ( path , body )
2024-12-24 19:16:19 -05:00
. map_err ( | e | GenerateError ::IO ( format! ( " Failed to write {:?} -- {e} " , path . file_name ( ) ) ) )
2019-05-30 16:52:30 -07:00
}