cargo_executable_payload/
lib.rs

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        /// Use `cross` instead of `$CARGO`
36        #[structopt(long)]
37        use_cross: bool,
38
39        /// Path to `strip(1)`
40        #[structopt(long, value_name("PATH"))]
41        strip_exe: Option<PathBuf>,
42
43        /// Do not apply `upx`
44        #[structopt(long)]
45        no_upx: bool,
46
47        /// Write output to the file instead of stdout
48        #[structopt(short, long, value_name("PATH"))]
49        output: Option<PathBuf>,
50
51        /// Path the main source file of the bin target
52        #[structopt(long, value_name("PATH"), conflicts_with("bin"))]
53        src: Option<PathBuf>,
54
55        /// Name of the bin target
56        #[structopt(long, value_name("NAME"))]
57        bin: Option<String>,
58
59        /// Build for the target triple
60        #[structopt(long, value_name("TRIPLE"), default_value("x86_64-unknown-linux-musl"))]
61        target: String,
62
63        /// Path to Cargo.toml
64        #[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}