1#![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
138struct 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#[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); 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, "e!(#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 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}