embed_rust/
lib.rs

1//! A macro that allows to embed a Rust executable in another Rust program.
2
3#![deny(unused_crate_dependencies)]
4#![warn(unused_qualifications)]
5#![warn(trivial_numeric_casts)]
6#![warn(unreachable_pub)]
7#![warn(unused_results)]
8#![warn(macro_use_extern_crate)]
9#![warn(noop_method_call)]
10#![warn(single_use_lifetimes)]
11#![warn(unused_lifetimes)]
12#![warn(unused_macro_rules)]
13#![warn(variant_size_differences)]
14#![warn(clippy::cast_lossless)]
15#![warn(clippy::unused_async)]
16#![warn(clippy::ref_as_ptr)]
17#![warn(clippy::manual_c_str_literals)]
18#![warn(clippy::redundant_test_prefix)]
19#![warn(clippy::ignore_without_reason)]
20#![warn(clippy::doc_comment_double_space_linebreaks)]
21#![warn(clippy::unnecessary_debug_formatting)]
22#![warn(clippy::elidable_lifetime_names)]
23#![warn(clippy::single_option_map)]
24#![warn(clippy::manual_midpoint)]
25#![warn(clippy::unnecessary_semicolon)]
26#![warn(clippy::return_and_then)]
27#![warn(clippy::precedence_bits)]
28#![warn(clippy::as_pointer_underscore)]
29#![warn(clippy::literal_string_with_formatting_args)]
30#![warn(clippy::unnecessary_literal_bound)]
31#![warn(clippy::map_with_unused_argument_over_ranges)]
32#![warn(clippy::used_underscore_items)]
33#![warn(clippy::manual_is_power_of_two)]
34#![warn(clippy::non_zero_suggestions)]
35#![warn(clippy::unused_trait_names)]
36#![warn(missing_docs)]
37
38use std::{
39    collections::{HashMap, HashSet, hash_map::Entry},
40    env::{self, temp_dir},
41    fs::{self, File},
42    io::{Read as _, Write as _},
43    path::{Path, PathBuf},
44    process::Command,
45};
46
47use fs2::FileExt as _;
48use path_slash::PathBufExt as _;
49use proc_macro2::{Group, Span};
50use quote::quote;
51use serde::Deserialize;
52use syn::{
53    Error, Ident, LitStr, Token, braced, bracketed,
54    punctuated::Punctuated,
55    token::{Brace, Bracket, Comma},
56};
57
58const CARGO_DEPENDENCIES_SECTION: &str = "[dependencies]";
59const COMPILER_ARTIFACT_MESSAGE_TYPE: &str = "compiler-artifact";
60
61#[derive(Deserialize)]
62struct CompilerArtifactMessage {
63    reason: String,
64    filenames: Vec<String>,
65}
66
67macro_rules! parse_options {
68    ($input: expr, $key: ident, $args: ident, $argument_parser: block) => {
69        parse_options!($input, $key, $args, $argument_parser, {}, false);
70    };
71    ($input: expr, $key: ident, $args: ident, $argument_parser: block, $string_argument_parser: block) => {
72        parse_options!($input, $key, $args, $argument_parser, $string_argument_parser, true);
73    };
74    ($input: expr, $key: ident, $args: ident, $argument_parser: block, $string_argument_parser: block, $allow_string_arguments: expr) => {
75        let mut seen_arguments = HashSet::new();
76        let $args;
77        let _: Brace = braced!($args in $input);
78        while !$args.is_empty() {
79            let lookahead = $args.lookahead1();
80            if lookahead.peek(Ident) {
81                let $key: Ident = $args.parse()?;
82                let _colon: syn::token::Colon = $args.parse()?;
83                if !seen_arguments.insert($key.to_string()) {
84                    return Err(Error::new(
85                        $key.span(),
86                        format!("Duplicated parameter `{}`", $key),
87                    ));
88                }
89                $argument_parser;
90            } else if $allow_string_arguments && lookahead.peek(LitStr) {
91                #[allow(unused_variables)]
92                let $key: LitStr = $args.parse()?;
93                let _colon: syn::token::Colon = $args.parse()?;
94                $string_argument_parser;
95            } else {
96                return Err(lookahead.error());
97            }
98            let lookahead = $args.lookahead1();
99            if lookahead.peek(Comma) {
100                let _: Comma = $args.parse()?;
101            } else if !$args.is_empty() {
102                return Err(lookahead.error());
103            }
104        }
105    };
106}
107
108#[derive(Debug)]
109struct GitSource {
110    url: String,
111    branch: Option<String>,
112    path: Option<PathBuf>,
113}
114
115#[derive(Debug)]
116enum Source {
117    Inline(Group),
118    Git(GitSource),
119    Path(PathBuf),
120}
121
122enum CommandItem {
123    Raw(String),
124    InputPath,
125    OutputPath,
126}
127
128impl CommandItem {
129    fn to_string<'a>(&'a self, input: &'a String, output: &'a String) -> &'a String {
130        match self {
131            CommandItem::Raw(string) => string,
132            CommandItem::InputPath => input,
133            CommandItem::OutputPath => output,
134        }
135    }
136}
137
138/// Arguments for the [`embed_rust`] macro.
139struct MatchEmbedRustArgs {
140    sources: Vec<Source>,
141    extra_files: HashMap<PathBuf, String>,
142    dependencies: String,
143    post_build_commands: Vec<Vec<CommandItem>>,
144    binary_cache_path: Option<PathBuf>,
145}
146
147impl syn::parse::Parse for MatchEmbedRustArgs {
148    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
149        let mut sources = Vec::new();
150        let mut extra_files = HashMap::new();
151        let mut dependencies = String::new();
152        let mut post_build_commands = Vec::new();
153        let mut binary_cache_path = None;
154        parse_options!(
155            input,
156            key,
157            args,
158            {
159                match key.to_string().as_str() {
160                    "source" => sources.push(Source::Inline(args.parse()?)),
161                    "dependencies" => {
162                        let dependencies_string: LitStr = args.parse()?;
163                        dependencies = dependencies_string.value();
164                    }
165                    "git" => {
166                        let lookahead = args.lookahead1();
167                        sources.push(Source::Git(if lookahead.peek(LitStr) {
168                            let url: LitStr = args.parse()?;
169                            GitSource {
170                                url: url.value(),
171                                branch: None,
172                                path: None,
173                            }
174                        } else if lookahead.peek(Brace) {
175                            let mut url = None;
176                            let mut branch = None;
177                            let mut path = None;
178                            parse_options!(args, key, git_args, {
179                                match key.to_string().as_str() {
180                                    "url" => {
181                                        let url_literal: LitStr = git_args.parse()?;
182                                        url = Some(url_literal.value());
183                                    }
184                                    "branch" => {
185                                        let branch_literal: LitStr = git_args.parse()?;
186                                        branch = Some(branch_literal.value());
187                                    }
188                                    "path" => {
189                                        let path_literal: LitStr = git_args.parse()?;
190                                        path = Some(PathBuf::from_slash(path_literal.value()));
191                                    }
192                                    _ => {
193                                        return Err(Error::new(
194                                            key.span(),
195                                            format!("Invalid parameter `{key}`"),
196                                        ));
197                                    }
198                                }
199                            });
200                            let Some(url) = url else {
201                                return Err(Error::new(
202                                    key.span(),
203                                    format!("missing `url` key for `{key}` argument"),
204                                ));
205                            };
206                            GitSource { url, branch, path }
207                        } else {
208                            return Err(lookahead.error());
209                        }))
210                    }
211                    "path" => {
212                        let path_literal: LitStr = args.parse()?;
213                        sources.push(Source::Path(PathBuf::from_slash(path_literal.value())));
214                    }
215                    "post_build" => {
216                        if !post_build_commands.is_empty() {
217                            return Err(Error::new(
218                                key.span(),
219                                format!("Can only have one `{key}`"),
220                            ));
221                        }
222                        let commands;
223                        let _: Bracket = bracketed!(commands in args);
224                        post_build_commands = commands
225                            .parse_terminated(
226                                |command| {
227                                    let command_items;
228                                    let _: Bracket = bracketed!(command_items in command);
229                                    Ok(Punctuated::<_, Token![,]>::parse_separated_nonempty_with(
230                                        &command_items,
231                                        |command_item| {
232                                            let lookahead = command_item.lookahead1();
233                                            if lookahead.peek(LitStr) {
234                                                let item: LitStr = command_item.parse()?;
235                                                Ok(CommandItem::Raw(item.value()))
236                                            } else if lookahead.peek(Ident) {
237                                                let item: Ident = command_item.parse()?;
238                                                match item.to_string().as_str() {
239                                                    "input_path" => Ok(CommandItem::InputPath),
240                                                    "output_path" => Ok(CommandItem::OutputPath),
241                                                    _ => Err(Error::new(
242                                                        item.span(),
243                                                        format!(
244                                                            "Invalid command expansion variable `{item}`, only `input_path` and `output_path` are valid",
245                                                            item = item.to_string().as_str(),
246                                                        ),
247                                                    )),
248                                                }
249                                            } else {
250                                                Err(lookahead.error())
251                                            }
252                                        },
253                                    )?
254                                    .into_iter()
255                                    .collect())
256                                },
257                                Token![,],
258                            )?
259                            .into_iter()
260                            .collect();
261                    }
262                    "binary_cache_path" => {
263                        let path_literal: LitStr = args.parse()?;
264                        binary_cache_path = Some(PathBuf::from_slash(path_literal.value()));
265                    }
266                    _ => return Err(Error::new(key.span(), format!("Invalid parameter `{key}`"))),
267                }
268            },
269            {
270                let key_value = key.value();
271                let path = PathBuf::from_slash(key_value.clone());
272                let extra_file_slot = match extra_files.entry(path) {
273                    Entry::Vacant(entry) => entry,
274                    Entry::Occupied(_) => {
275                        return Err(Error::new(
276                            key.span(),
277                            format!("Duplicated file `{key_value}`"),
278                        ));
279                    }
280                };
281                match key_value.as_str() {
282                    "src/main.rs" => sources.push(Source::Inline(args.parse()?)),
283                    _ => {
284                        let content = if key_value.ends_with(".rs") {
285                            let source: Group = args.parse()?;
286                            let source = source.stream();
287                            quote!(#source).to_string()
288                        } else {
289                            let lookahead = args.lookahead1();
290                            if lookahead.peek(LitStr) {
291                                let content: LitStr = args.parse()?;
292                                content.value()
293                            } else if lookahead.peek(Brace) {
294                                let source: Group = args.parse()?;
295                                let source = source.stream();
296                                quote!(#source).to_string()
297                            } else {
298                                return Err(lookahead.error());
299                            }
300                        };
301                        let _content: &String = extra_file_slot.insert(content);
302                    }
303                }
304            }
305        );
306        if sources.is_empty() {
307            return Err(Error::new(Span::call_site(), "Missing `source` attribute"));
308        }
309        Ok(Self {
310            sources,
311            extra_files,
312            dependencies,
313            post_build_commands,
314            binary_cache_path,
315        })
316    }
317}
318
319/// Compile Rust code and return the bytes of the compiled binary.
320///
321/// # Arguments
322/// The macro accepts arguments using a `{key: value}` syntax.
323/// To following arguments are supported:
324///
325/// * `source` / `path` / `git` / `"src/main.rs"` (one of them is required):
326///   These define the source of the Rust code that should be compiled.
327///   There can be more than one source, in which case the first existing source will be selected.
328///   `source` and `"src/main.rs"` always exist, but `path` will be skipped if the path does not point
329///   to an existing directory and `git` will be skipped if cloning the repository fails.
330///
331///   - `source`:
332///     Inline source to compile.
333///     ```rust
334///     # use embed_rust::embed_rust;
335///     const BINARY: &[u8] = embed_rust!({
336///         source: {
337///             fn main() {
338///                 println!("Hello world!");
339///             }
340///         },
341///     });
342///     ```
343///   - `path`:
344///     Absolute or relative path to a directory that contains a Rust project to compile.
345///     Relative paths will be resolved from the parent directory of the current file.
346///     ```rust
347///     # use embed_rust::embed_rust;
348///     const BINARY: &[u8] = embed_rust!({path: "../tests/projects/relative-path"});
349///     ```
350///   - `git`:
351///     Configuration for a git repository to clone that contains the Rust project to compile.
352///     This parameter accepts one of two forms of values:
353///     + Key-value parameters:
354///       A key-value map of options.
355///       The `url` provides the url of the repository and is required.
356///       The `path` is optional and specifies a subpath in the repository where the Rust project to compile can be found.
357///       If omitted to root of the repository is used.
358///       The `branch` is optional and specifies the branch to check out, otherwise the default branch is used.
359///       ```rust
360///       # use embed_rust::embed_rust;
361///       const BINARY: &[u8] = embed_rust!({
362///           git: { url: "https://github.com/Abestanis/embed-rust.git", path: "tests/projects/git", branch: "main" }
363///       });
364///       ```
365///     + `"<url>"`:
366///       A string with the url of the git repository. This is the same as only specifying the `url` in the key-value parameter case.
367///       This assumes the Rust project is at the top level of the repository and uses the default branch.
368///       ```rust
369///       # use embed_rust::embed_rust;
370///       const BINARY: &[u8] = embed_rust!({git: "https://github.com/Abestanis/embed-rust.git"});
371///       ```
372///   - `"src/main.rs"`:
373///     A string literal with the Rust source to compile (see the argument `"<path>"` below).
374/// * `"<path>"` (optional):
375///   A path and string literal content for any file that should be present when compiling the source.
376///   `"<path>"` is a relative path from the root of the Rust project that is being compiled.
377///   This can be used for example to overwrite the cargo config file to compile for a specific target:
378///   ```rust,ignore
379///   # use embed_rust::embed_rust;
380///   const BINARY: &[u8] = embed_rust!({
381///       source: {
382///           // [...]
383///       },
384///       ".cargo/config.toml": r#"
385///           [build]
386///           target = "thumbv7em-none-eabihf"
387///       "#,
388///   });
389///   ```
390/// * `dependencies` (optional):
391///   Dependencies of the Rust code as if they had been written in a `Cargo.toml` file.
392///   ```rust
393///   # use embed_rust::embed_rust;
394///   const BINARY: &[u8] = embed_rust!({
395///       source: {
396///           use clap::command;
397///           fn main() {
398///               let _matches = command!().get_matches();
399///           }
400///       },
401///       dependencies: r#"
402///            clap = { version = "~4.5", features = ["cargo"] }
403///        "#
404///   });
405///   ```
406/// * `binary_cache_path` (optional):
407///   The absolute or relative path to which the compiled rust binary is written to.
408///   Relative paths will be resolved from the parent directory of the current file.
409///   If no path is provided a temporary path will be used.
410///   ```rust
411///   # use embed_rust::embed_rust;
412///   const BINARY: &[u8] = embed_rust!({
413///       source: {
414///           fn main() {
415///               println!("Hello world!");
416///           }
417///       },
418///       binary_cache_path: "../tests/binaries/relative-path.bin",
419///   });
420///   ```
421/// * `post_build` (optional):
422///   A list of commands that will be executed after the binary has been build.
423///   Each command is a list where the first element is the command and all other elements are arguments.
424///   The arguments must be string literals except or the special symbols `input_path` or `output_path`.
425///   `input_path` will be replaced with the path to the compiled binary and `output_path` will be a
426///   path to a temporary file which can be used as an output path for the command.
427///   If the `output_path` is present in the command the file at the path will be used as the compiled binary
428///   for subsequent commands and it's contents will be returned by the macro instead of the original compiled binary.
429///   ```rust
430///   # use embed_rust::embed_rust;
431///   const BINARY: &[u8] = embed_rust!({
432///       source: {
433///           fn main() {
434///               println!("Hello world!");
435///           }
436///       },
437///       post_build: [
438///           ["cp", input_path, output_path] // Useless example, just copy the generated binary to the output path.
439///       ]
440///   });
441///   ```
442#[proc_macro]
443pub fn embed_rust(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
444    let args = syn::parse_macro_input!(tokens as MatchEmbedRustArgs);
445    let path = match compile_rust(args) {
446        Ok(path) => path,
447        Err(error) => return error.into_compile_error().into(),
448    };
449    let Some(path) = path.to_str() else {
450        return Error::new(
451            Span::call_site(),
452            "Generated binary path contains invalid UTF-8",
453        )
454        .into_compile_error()
455        .into();
456    };
457    quote! {
458        include_bytes!(#path)
459    }
460    .into()
461}
462
463fn lock_and_clear_directory(generated_project_dir: &Path) -> syn::Result<File> {
464    let mut lock_file = generated_project_dir.to_path_buf();
465    let _did_have_name: bool = lock_file.set_extension(".lock");
466    let lock_file = File::options()
467        .read(true)
468        .write(true)
469        .create(true)
470        .truncate(true)
471        .open(lock_file)
472        .map_err(|error| {
473            Error::new(
474                Span::call_site(),
475                format!("Failed to open lock-file: {error:?}"),
476            )
477        })?;
478    if let Err(error) = lock_file.lock_exclusive() {
479        return Err(Error::new(
480            Span::call_site(),
481            format!("Failed to lock lock-file: {error:?}"),
482        ));
483    }
484    let _ = fs::remove_dir_all(generated_project_dir); // Ignore errors about non-existent directories.
485    if let Err(error) = fs::create_dir_all(generated_project_dir) {
486        return Err(Error::new(
487            Span::call_site(),
488            format!("Failed to create embedded project directory: {error:?}"),
489        ));
490    }
491    Ok(lock_file)
492}
493
494fn fill_project_template(
495    generated_project_dir: &Path,
496    extra_files: &HashMap<PathBuf, String>,
497    dependencies: &str,
498) -> syn::Result<()> {
499    for (path, content) in extra_files.iter() {
500        write_file(generated_project_dir.join(path), content)?;
501    }
502    if !dependencies.is_empty() {
503        let cargo_file = generated_project_dir.join("Cargo.toml");
504        let mut cargo_content = String::new();
505        let _bytes_read: usize = File::open(cargo_file.clone())
506            .map_err(|error| {
507                Error::new(
508                    Span::call_site(),
509                    format!(
510                        "Failed to open {cargo_file}: {error:?}",
511                        cargo_file = cargo_file.display()
512                    ),
513                )
514            })?
515            .read_to_string(&mut cargo_content)
516            .map_err(|error| {
517                Error::new(
518                    Span::call_site(),
519                    format!(
520                        "Failed to read {cargo_file}: {error:?}",
521                        cargo_file = cargo_file.display()
522                    ),
523                )
524            })?;
525        if !cargo_content.contains(CARGO_DEPENDENCIES_SECTION) {
526            return Err(Error::new(
527                Span::call_site(),
528                "Generated Cargo.toml has no dependencies section",
529            ));
530        }
531        cargo_content = cargo_content.replace(
532            CARGO_DEPENDENCIES_SECTION,
533            (CARGO_DEPENDENCIES_SECTION.to_owned() + "\n" + dependencies).as_str(),
534        );
535        write_file(cargo_file, &cargo_content)?;
536    }
537    Ok(())
538}
539
540fn prepare_source(
541    source: &Source,
542    generated_project_dir: &Path,
543    source_file_dir: &Path,
544    args: &MatchEmbedRustArgs,
545) -> syn::Result<(Option<File>, PathBuf)> {
546    Ok(match source {
547        Source::Inline(source) => {
548            let lock_file = lock_and_clear_directory(generated_project_dir)?;
549            let _output: Vec<u8> = run_command(
550                Command::new("cargo")
551                    .current_dir(generated_project_dir)
552                    .arg("init"),
553                "Failed to initialize embedded project crate",
554            )?;
555            let source_dir = generated_project_dir.join("src");
556            let main_source_file = source_dir.join("main.rs");
557            let main_source = source.stream();
558            write_file(main_source_file, &quote!(#main_source).to_string())?;
559            fill_project_template(generated_project_dir, &args.extra_files, &args.dependencies)?;
560            (Some(lock_file), generated_project_dir.to_path_buf())
561        }
562        Source::Git(git_source) => {
563            let lock_file = lock_and_clear_directory(generated_project_dir)?;
564            let mut clone_command = Command::new("git");
565            let mut clone_command = clone_command
566                .arg("clone")
567                .arg("--recurse-submodules")
568                .arg("--depth=1");
569            if let Some(ref branch) = git_source.branch {
570                clone_command = clone_command.arg("--branch").arg(branch);
571            }
572            let _output: Vec<u8> = run_command(
573                clone_command
574                    .arg(&git_source.url)
575                    .arg(generated_project_dir),
576                "Failed to clone embedded project",
577            )?;
578            let generated_project_dir = if let Some(ref path) = git_source.path {
579                generated_project_dir.join(path.clone())
580            } else {
581                generated_project_dir.to_path_buf()
582            };
583            fill_project_template(
584                &generated_project_dir,
585                &args.extra_files,
586                &args.dependencies,
587            )?;
588            (Some(lock_file), generated_project_dir)
589        }
590        Source::Path(path) => {
591            let generated_project_dir = if path.is_absolute() {
592                path.clone()
593            } else {
594                source_file_dir.join(path)
595            };
596            if !generated_project_dir.exists() {
597                return Err(Error::new(
598                    Span::call_site(),
599                    format!("Given path does not exist: {}", path.display()),
600                ));
601            }
602            (None, generated_project_dir)
603        }
604    })
605}
606
607fn compile_rust(args: MatchEmbedRustArgs) -> syn::Result<PathBuf> {
608    let call_site = Span::call_site().unwrap();
609    let Some(source_file) = call_site.local_file() else {
610        return Err(Error::new(
611            Span::call_site(),
612            "Unable to get path of source file",
613        ));
614    };
615    if !source_file.exists() {
616        return Err(Error::new(
617            Span::call_site(),
618            "Unable to get path of source file (file does not exist)",
619        ));
620    }
621    let crate_dir = PathBuf::from(
622        env::var("CARGO_MANIFEST_DIR")
623            .expect("'CARGO_MANIFEST_DIR' environment variable is missing"),
624    );
625
626    let source_file_id = sanitize_filename::sanitize(
627        source_file
628            .strip_prefix(&crate_dir)
629            .unwrap_or(&source_file)
630            .to_string_lossy(),
631    )
632    .replace('.', "_");
633
634    let line = call_site.line();
635    let column = call_site.column();
636    let id = format!("{source_file_id}_{line}_{column}");
637    let mut generated_project_dir = env::var("OUT_DIR")
638        .map_or_else(|_| temp_dir(), PathBuf::from)
639        .join(id);
640    let source_file_dir = source_file
641        .parent()
642        .expect("Should be able to resolve the parent directory of the source file");
643
644    let mut i = 0;
645    let lock_file = loop {
646        match prepare_source(
647            &args.sources[i],
648            &generated_project_dir,
649            source_file_dir,
650            &args,
651        ) {
652            Ok((lock_file, new_generated_project_dir)) => {
653                generated_project_dir = new_generated_project_dir;
654                break lock_file;
655            }
656            Err(error) => {
657                if i + 1 == args.sources.len() {
658                    if let Some(compiled_binary_path) = args.binary_cache_path {
659                        return Ok(compiled_binary_path);
660                    }
661                    return Err(error);
662                }
663            }
664        }
665        i += 1;
666    };
667
668    let mut build_command = Command::new("cargo");
669    let build_command = build_command
670        .current_dir(&generated_project_dir)
671        .arg("build")
672        .arg("--release");
673    let _output: Vec<u8> = run_command(
674        build_command,
675        format!(
676            "Failed to build embedded project crate at {}",
677            generated_project_dir.display()
678        )
679        .as_str(),
680    )?;
681
682    // Find binary.
683    let output = run_command(
684        build_command.arg("--message-format").arg("json"),
685        "Failed to build embedded project crate",
686    )?;
687    let Ok(output) = core::str::from_utf8(&output) else {
688        return Err(Error::new(
689            Span::call_site(),
690            "Unable to parse cargo output: Invalid UTF-8",
691        ));
692    };
693    let mut artifact_message = None;
694    for line in output.lines() {
695        if line.contains(COMPILER_ARTIFACT_MESSAGE_TYPE) {
696            artifact_message = Some(line);
697        }
698    }
699    let Some(artifact_message) = artifact_message else {
700        return Err(Error::new(
701            Span::call_site(),
702            "Did not found an artifact message in cargo build output",
703        ));
704    };
705    let artifact_message: CompilerArtifactMessage = serde_json::from_str(artifact_message)
706        .map_err(|error| {
707            Error::new(
708                Span::call_site(),
709                format!("Failed to parse artifact message from cargo: {error:?}"),
710            )
711        })?;
712    if artifact_message.reason != COMPILER_ARTIFACT_MESSAGE_TYPE {
713        return Err(Error::new(
714            Span::call_site(),
715            "Invalid cargo artifact message: Wrong reason",
716        ));
717    }
718    let Some(mut artifact_path) = artifact_message.filenames.first() else {
719        return Err(Error::new(
720            Span::call_site(),
721            "Invalid cargo artifact message: No artifact path",
722        ));
723    };
724    let output_artifact_path = &(artifact_path.to_owned() + ".tmp");
725    let mut used_output_path = false;
726    for command_items in args.post_build_commands {
727        let (mut shell, first_arg) = if cfg!(target_os = "windows") {
728            (Command::new("powershell"), "/C")
729        } else {
730            (Command::new("sh"), "-c")
731        };
732        let command = shell.arg(first_arg).arg(
733            command_items
734                .iter()
735                .map(|item| item.to_string(artifact_path, output_artifact_path))
736                .fold(String::new(), |left, right| left + " " + right),
737        );
738        let _output: Vec<u8> = run_command(command, "Failed to run post_build command")?;
739        used_output_path |= command_items
740            .iter()
741            .any(|item| matches!(item, CommandItem::OutputPath));
742    }
743    if used_output_path {
744        artifact_path = output_artifact_path;
745    }
746    let artifact_path = PathBuf::from(artifact_path);
747    let artifact_path = if let Some(binary_cache_path) = args.binary_cache_path {
748        let absolute_binary_cache_path = source_file_dir.join(&binary_cache_path);
749        if let Some(parent) = absolute_binary_cache_path.parent() {
750            fs::create_dir_all(parent).map_err(|error| {
751                Error::new(
752                    Span::call_site(),
753                    format!(
754                        "Failed to create directories for binary_cache_path at {parent}: {error:?}",
755                        parent = parent.display()
756                    ),
757                )
758            })?;
759        }
760        let _bytes_copied: u64 = fs::copy(artifact_path, &absolute_binary_cache_path).map_err(|error| {
761            Error::new(
762                Span::call_site(),
763                format!(
764                    "Failed to copy generated binary to binary_cache_path at {absolute_binary_cache_path}: {error:?}",
765                    absolute_binary_cache_path=absolute_binary_cache_path.display()
766                ),
767            )
768        })?;
769        binary_cache_path
770    } else {
771        artifact_path
772    };
773    drop(lock_file);
774
775    Ok(artifact_path)
776}
777
778fn run_command(command: &mut Command, error_message: &str) -> syn::Result<Vec<u8>> {
779    match command.output() {
780        Ok(output) => {
781            if !output.status.success() {
782                Err(Error::new(
783                    Span::call_site(),
784                    format!(
785                        "{error_message}: `{command:?}` failed with exit code {exit_code:?}\n# Stdout:\n{stdout}\n# Stderr:\n{stderr}",
786                        exit_code = output.status.code(),
787                        stdout = core::str::from_utf8(output.stdout.as_slice())
788                            .unwrap_or("<Invalid UTF-8>"),
789                        stderr = core::str::from_utf8(output.stderr.as_slice())
790                            .unwrap_or("<Invalid UTF-8>")
791                    ),
792                ))
793            } else {
794                Ok(output.stdout)
795            }
796        }
797        Err(error) => Err(Error::new(
798            Span::call_site(),
799            format!("{error_message}: `{command:?}` failed: {error:?}"),
800        )),
801    }
802}
803
804fn write_file(path: PathBuf, content: &String) -> syn::Result<()> {
805    if let Some(parent_dir) = path.parent() {
806        if let Err(error) = fs::create_dir_all(parent_dir) {
807            return Err(Error::new(
808                Span::call_site(),
809                format!(
810                    "Failed to create parent directory for {path}: {error:?}",
811                    path = path.display()
812                ),
813            ));
814        }
815    }
816    let mut file = File::create(path.clone()).map_err(|error| {
817        Error::new(
818            Span::call_site(),
819            format!("Failed to open {path}: {error:?}", path = path.display()),
820        )
821    })?;
822    file.write_all(content.as_bytes()).map_err(|error| {
823        Error::new(
824            Span::call_site(),
825            format!(
826                "Failed to write {path} to project: {error:?}",
827                path = path.display()
828            ),
829        )
830    })?;
831    Ok(())
832}