1use anyhow::{anyhow, bail, Context as _};
2use camino::Utf8Path;
3use cargo_metadata as cm;
4use indoc::formatdoc;
5use itertools::Itertools as _;
6use proc_macro2::{TokenStream, TokenTree};
7use std::{
8 env,
9 ffi::OsStr,
10 fmt,
11 io::{self, Write},
12 path::{Path, PathBuf},
13};
14use structopt::{clap::AppSettings, StructOpt};
15use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor as _};
16
17#[derive(StructOpt)]
18#[structopt(
19 about,
20 author,
21 bin_name("cargo"),
22 global_settings(&[AppSettings::DeriveDisplayOrder, AppSettings::UnifiedHelpMessage])
23)]
24pub enum Opt {
25 #[structopt(
26 about,
27 author,
28 usage(
29 r#"cargo executable-payload [OPTIONS]
30 cargo executable-payload [OPTIONS] --src <PATH>
31 cargo executable-payload [OPTIONS] --bin <NAME>"#,
32 )
33 )]
34 ExecutablePayload {
35 #[structopt(long)]
37 use_cross: bool,
38
39 #[structopt(long, value_name("PATH"))]
41 strip_exe: Option<PathBuf>,
42
43 #[structopt(long)]
45 no_upx: bool,
46
47 #[structopt(short, long, value_name("PATH"))]
49 output: Option<PathBuf>,
50
51 #[structopt(long, value_name("PATH"), conflicts_with("bin"))]
53 src: Option<PathBuf>,
54
55 #[structopt(long, value_name("NAME"))]
57 bin: Option<String>,
58
59 #[structopt(long, value_name("TRIPLE"), default_value("x86_64-unknown-linux-musl"))]
61 target: String,
62
63 #[structopt(long, value_name("PATH"))]
65 manifest_path: Option<PathBuf>,
66 },
67}
68
69pub struct Shell {
70 stderr: StandardStream,
71}
72
73impl Shell {
74 pub fn new() -> Self {
75 Self {
76 stderr: StandardStream::stderr(if atty::is(atty::Stream::Stderr) {
77 ColorChoice::Auto
78 } else {
79 ColorChoice::Never
80 }),
81 }
82 }
83
84 pub fn err(&mut self) -> &mut dyn Write {
85 &mut self.stderr
86 }
87
88 pub(crate) fn status(
89 &mut self,
90 status: impl fmt::Display,
91 message: impl fmt::Display,
92 ) -> io::Result<()> {
93 self.print(status, message, Color::Green, true)
94 }
95
96 pub fn error(&mut self, message: impl fmt::Display) -> io::Result<()> {
97 self.print("error", message, Color::Red, false)
98 }
99
100 fn print(
101 &mut self,
102 status: impl fmt::Display,
103 message: impl fmt::Display,
104 color: Color,
105 justified: bool,
106 ) -> io::Result<()> {
107 self.stderr
108 .set_color(ColorSpec::new().set_bold(true).set_fg(Some(color)))?;
109 if justified {
110 write!(self.stderr, "{:>12}", status)?;
111 } else {
112 write!(self.stderr, "{}", status)?;
113 self.stderr.set_color(ColorSpec::new().set_bold(true))?;
114 write!(self.stderr, ":")?;
115 }
116 self.stderr.reset()?;
117 writeln!(self.stderr, " {}", message)
118 }
119}
120
121impl Default for Shell {
122 fn default() -> Self {
123 Self::new()
124 }
125}
126
127pub fn run(opt: Opt, shell: &mut Shell) -> anyhow::Result<()> {
128 let Opt::ExecutablePayload {
129 use_cross,
130 strip_exe,
131 no_upx,
132 output,
133 src,
134 bin,
135 target,
136 manifest_path,
137 } = opt;
138
139 let cwd = env::current_dir().with_context(|| "failed to get CWD")?;
140 let manifest_path = if let Some(manifest_path) = manifest_path {
141 cwd.join(manifest_path.strip_prefix(".").unwrap_or(&manifest_path))
142 } else {
143 locate_project(&cwd)?
144 };
145 let metadata = cargo_metadata(&manifest_path, &cwd)?;
146
147 let (bin, bin_package) = if let Some(bin) = bin {
148 bin_target_by_name(&metadata, &bin)
149 } else if let Some(src) = src {
150 bin_target_by_src_path(&metadata, &cwd.join(src))
151 } else {
152 exactly_one_bin_target(&metadata)
153 }?;
154
155 let source_code = std::fs::read_to_string(&bin.src_path)
156 .with_context(|| format!("could not read `{}`", bin.src_path))?;
157
158 let artifact_base64 = build(
159 shell,
160 &metadata.target_directory,
161 &bin_package.manifest_path.with_file_name(""),
162 &bin.name,
163 use_cross,
164 &target,
165 strip_exe.map(|p| cwd.join(p)).as_deref(),
166 no_upx,
167 )?;
168
169 let rs = format_with_template(&source_code, &artifact_base64);
170 if let Some(output) = output {
171 std::fs::write(output, rs)?;
172 } else {
173 let mut stdout = io::stdout();
174 stdout.write_all(rs.as_ref())?;
175 stdout.flush()?;
176 }
177 Ok(())
178}
179
180fn locate_project(cwd: &Path) -> anyhow::Result<PathBuf> {
181 cwd.ancestors()
182 .map(|p| p.join("Cargo.toml"))
183 .find(|p| p.exists())
184 .with_context(|| {
185 format!(
186 "could not find `Cargo.toml` in `{}` or any parent directory",
187 cwd.display(),
188 )
189 })
190}
191
192fn cargo_metadata(manifest_path: &Path, cwd: &Path) -> cm::Result<cm::Metadata> {
193 cm::MetadataCommand::new()
194 .manifest_path(manifest_path)
195 .current_dir(cwd)
196 .exec()
197}
198
199fn bin_target_by_name<'a>(
200 metadata: &'a cm::Metadata,
201 name: &str,
202) -> anyhow::Result<(&'a cm::Target, &'a cm::Package)> {
203 match *bin_targets(metadata)
204 .filter(|(t, _)| t.name == name)
205 .collect::<Vec<_>>()
206 {
207 [] => bail!("no bin target named `{}`", name),
208 [bin] => Ok(bin),
209 [..] => bail!("multiple bin targets named `{}` in this workspace", name),
210 }
211}
212
213fn bin_target_by_src_path<'a>(
214 metadata: &'a cm::Metadata,
215 src_path: &Path,
216) -> anyhow::Result<(&'a cm::Target, &'a cm::Package)> {
217 match *bin_targets(metadata)
218 .filter(|(t, _)| t.src_path == src_path)
219 .collect::<Vec<_>>()
220 {
221 [] => bail!(
222 "`{}` is not the main source file of any bin targets in this workspace ",
223 src_path.display(),
224 ),
225 [bin] => Ok(bin),
226 [..] => bail!(
227 "multiple bin targets which `src_path` is `{}`",
228 src_path.display(),
229 ),
230 }
231}
232
233fn exactly_one_bin_target(metadata: &cm::Metadata) -> anyhow::Result<(&cm::Target, &cm::Package)> {
234 match &*bin_targets(metadata).collect::<Vec<_>>() {
235 [] => bail!("no bin target in this workspace"),
236 [bin] => Ok(*bin),
237 [bins @ ..] => bail!(
238 "could not determine which binary to choose. Use the `--bin` option or `--src` option \
239 to specify a binary.\n\
240 available binaries: {}\n\
241 note: currently `cargo-executable-payload` does not support the `default-run` manifest \
242 key.",
243 bins.iter()
244 .map(|(cm::Target { name, .. }, _)| name)
245 .format(", "),
246 ),
247 }
248}
249
250fn bin_targets(metadata: &cm::Metadata) -> impl Iterator<Item = (&cm::Target, &cm::Package)> {
251 metadata
252 .packages
253 .iter()
254 .filter(move |cm::Package { id, .. }| metadata.workspace_members.contains(id))
255 .flat_map(|p| p.targets.iter().map(move |t| (t, p)))
256 .filter(|(cm::Target { kind, .. }, _)| *kind == ["bin".to_owned()])
257}
258
259#[allow(clippy::too_many_arguments)]
260fn build(
261 shell: &mut Shell,
262 target_dir: &Utf8Path,
263 manifest_dir: &Utf8Path,
264 bin_name: &str,
265 use_cross: bool,
266 target: &str,
267 strip_exe: Option<&Path>,
268 no_upx: bool,
269) -> anyhow::Result<String> {
270 fn run_command(
271 shell: &mut Shell,
272 cwd: &Utf8Path,
273 program: impl AsRef<OsStr>,
274 args: &[impl AsRef<OsStr>],
275 before_spawn: fn(&mut duct::Expression),
276 ) -> anyhow::Result<()> {
277 let program = program.as_ref();
278 let program = which::which_in(&program, env::var_os("PATH"), cwd)
279 .map_err(|_| anyhow!("`{}` does not seem to exist", program.to_string_lossy()))?;
280 let args = args.iter().map(AsRef::as_ref).collect::<Vec<_>>();
281
282 let format = format!(
283 "`{}{}`",
284 shell_escape::escape(program.to_string_lossy()),
285 args.iter().format_with("", |arg, f| f(&format_args!(
286 " {}",
287 shell_escape::escape(arg.to_string_lossy()),
288 ))),
289 );
290
291 shell.status("Running", &format)?;
292 let mut cmd = duct::cmd(program, args).dir(cwd);
293 before_spawn(&mut cmd);
294 cmd.run()
295 .with_context(|| format!("{} didn't exit successfully", format))?;
296 Ok(())
297 }
298
299 let tempdir = tempfile::Builder::new()
300 .prefix("cargo-executable-payload-")
301 .tempdir()?;
302
303 let program = if use_cross {
304 "cross".into()
305 } else {
306 env::var_os("CARGO").with_context(|| "`$CARGO` is not present")?
307 };
308 let args = vec![
309 OsStr::new("build"),
310 OsStr::new("--release"),
311 OsStr::new("--bin"),
312 OsStr::new(bin_name),
313 OsStr::new("--target"),
314 OsStr::new(target),
315 ];
316 run_command(shell, manifest_dir, program, &args, |_| ())?;
317
318 let mut artifact_path = target_dir.join(target).join("release").join(bin_name);
319 if target.contains("windows") {
320 artifact_path.set_extension("exe");
321 }
322
323 let artifact_file_name = artifact_path.file_name().unwrap_or("");
324
325 std::fs::copy(&artifact_path, tempdir.path().join(artifact_file_name))?;
326
327 let artifact_path = tempdir.path().join(artifact_file_name);
328
329 let program = strip_exe.unwrap_or_else(|| "strip".as_ref());
330 if let Ok(program) = which::which_in(program, env::var_os("PATH"), manifest_dir) {
331 let args = [OsStr::new("-s"), artifact_path.as_ref()];
332 run_command(shell, manifest_dir, program, &args, |_| ())?;
333 }
334
335 if !no_upx {
336 if let Ok(program) = which::which_in("upx", env::var_os("PATH"), manifest_dir) {
337 let args = [OsStr::new("--best"), artifact_path.as_ref()];
338 run_command(shell, manifest_dir, program, &args, |cmd| {
339 *cmd = cmd.stdout_to_stderr();
340 })?;
341 }
342 }
343
344 let artifact = std::fs::read(artifact_path)?;
345 let artifact = base64::encode(artifact);
346
347 tempdir.close()?;
348 Ok(artifact)
349}
350
351fn format_with_template(original_source_code: &str, payload: &str) -> String {
352 formatdoc! {r#"
353 //! This code is generated by [cargo-executable-payload](https://github.com/qryxip/cargo-executable-payload).
354
355 original_source_code! {{
356 {original_source_code}}}
357
358 fn main()->std::io::Result<()>{{use std::{{fs::{{File,Permissions}},io::Write as _,os::unix::{{fs::PermissionsExt as _,process::CommandExt as _}},process::Command}};let mut file=File::create(PATH)?;file.write_all(&decode())?;file.set_permissions(Permissions::from_mode(0o755))?;file.sync_all()?;drop(file);Err(Command::new(PATH).exec())}}fn decode()->Vec<u8>{{let mut table=[0;256];for(i,&c)in b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".iter().enumerate(){{table[usize::from(c)]=i as u8;}}let mut acc=vec![];for chunk in PAYLOAD.as_bytes().chunks(4){{let index0=table[usize::from(chunk[0])];let index1=table[usize::from(chunk[1])];let index2=table[usize::from(chunk[2])];let index3=table[usize::from(chunk[3])];acc.push((index0<<2)+(index1>>4));acc.push((index1<<4)+(index2>>2));acc.push((index2<<6)+index3)}}if PAYLOAD.ends_with("=="){{acc.pop();acc.pop();}}else if PAYLOAD.ends_with('='){{acc.pop();}}acc}}#[macro_export]macro_rules!original_source_code{{($($_:tt)*)=>()}}static PATH:&str="/tmp/a.out";static PAYLOAD:&str="{payload}";
359 "#,
360 original_source_code = indent_code(original_source_code),
361 payload = payload,
362 }
363}
364
365fn indent_code(code: &str) -> String {
366 return if code.parse::<TokenStream>().map_or(false, is_safe_to_indent) {
367 code.lines()
368 .map(|line| match line {
369 "" => "\n".to_owned(),
370 line => format!(" {}\n", line),
371 })
372 .join("")
373 } else {
374 code.to_owned()
375 };
376
377 fn is_safe_to_indent(tokens: TokenStream) -> bool {
378 tokens.into_iter().all(|tt| match tt {
379 TokenTree::Group(group) => is_safe_to_indent(group.stream()),
380 TokenTree::Literal(lit) => lit.span().start().line == lit.span().end().line,
381 _ => true,
382 })
383 }
384}
385
386#[cfg(test)]
387mod tests {
388 use indoc::indoc;
389 use test_case::test_case;
390
391 #[test_case(indoc!("") => indoc!(""); "empty")]
392 #[test_case(
393 indoc! {r#"
394 #![warn(rust_2018_idioms)]
395
396 fn main() {
397 println!("Hello, world!");
398 }
399 "#} => indoc! {r#"
400 | #![warn(rust_2018_idioms)]
401 |
402 | fn main() {
403 | println!("Hello, world!");
404 | }
405 "#};
406 "no_multi_line_literal"
407 )]
408 #[test_case(
409 indoc! {r#"
410 #![warn(rust_2018_idioms)]
411
412 fn main() {
413 println!(
414 "Hello, \
415 world!"
416 );
417 }
418 "#} => indoc! {r#"
419 |#![warn(rust_2018_idioms)]
420 |
421 |fn main() {
422 | println!(
423 | "Hello, \
424 | world!"
425 | );
426 |}
427 "#};
428 "multi_line_literal"
429 )]
430 fn indent_code_margined(code: &str) -> String {
431 crate::indent_code(code)
432 .lines()
433 .map(|s| format!("|{}\n", s))
434 .collect()
435 }
436}