dockerbuild 0.1.4

Build linux binary under macOS using Docker
use std::fs;
use std::process::Command;
use rust_util::{XResult, util_cmd};
use crate::build_util;
use crate::build_util::Builder;

const DOCKER_CMD: &str = "docker";
const DCOLER_TEMP_CMD: &str = "temp_sub_cmd";
const DOCKER_WORK_DIR: &str = "/usr/src/app";

// $ docker run --rm --user "$(id -u)":"$(id -g)" \
// -v "$PWD":/usr/src/app -w /usr/src/app rust:1.47 \
// cargo build --release
#[derive(Default)]
pub struct DockerCmd {
    docker_name: String,
    docker_current_dir: Option<String>,
    docker_run_uid: Option<u32>,
    docker_run_gid: Option<u32>,
    volumns: Vec<(String, String)>,
    mirror: Option<String>,
    tty: bool,
    builder_args: Vec<String>,
}

#[allow(dead_code)]
impl DockerCmd {
    pub fn new(docker_name: &str) -> Self {
        success!("Docker image: {}", docker_name);
        Self {
            docker_name: docker_name.into(),
            tty: true,
            ..Default::default()
        }
    }

    pub fn current_dir(&mut self, current_dir: &str) -> &mut Self {
        self.docker_current_dir = Some(current_dir.into());
        self
    }

    pub fn uid(&mut self, uid: u32) -> &mut Self {
        self.docker_run_uid = Some(uid);
        self
    }

    pub fn gid(&mut self, gid: u32) -> &mut Self {
        self.docker_run_gid = Some(gid);
        self
    }

    pub fn mirror(&mut self, mirror: &Option<&str>) -> &mut Self {
        self.mirror = mirror.map(|s| s.into());
        self
    }

    pub fn tty(&mut self, tty: bool) -> &mut Self {
        self.tty = tty;
        self
    }

    pub fn add_volumn(&mut self, outer_vol: &str, docker_vol: &str) -> &mut Self {
        self.volumns.push((outer_vol.into(), docker_vol.into()));
        self
    }

    pub fn add_build_arg<T>(&mut self, arg: T) -> &mut Self where T: Into<String> {
        self.builder_args.push(arg.into());
        self
    }

    pub fn exec(self, cmds: &[String]) -> XResult<()> {
        let mut cmd = Command::new(DOCKER_CMD);
        cmd.arg("run");
        cmd.arg("--rm");
        if self.tty {
            cmd.arg("-t");
        }
        cmd.arg("--user");
        cmd.arg(&format!("{}:{}",
            self.docker_run_uid.unwrap_or_else(|| users::get_current_uid() as u32),
            self.docker_run_gid.unwrap_or_else(|| users::get_current_gid() as u32),
        ));
        cmd.arg("-v");
        cmd.arg(&format!("{}:{}", build_util::get_work_dir(&self.docker_current_dir)?, DOCKER_WORK_DIR));
        for (outer_vol, docker_vol) in self.volumns {
            cmd.arg("-v");
            cmd.arg(&format!("{}:{}", outer_vol, docker_vol));
        }
        cmd.args(vec!["-w", DOCKER_WORK_DIR]);
        cmd.arg(&self.docker_name);

        let builder = Builder::from(&self.docker_current_dir)?;
        let builder_name = match builder.get_name() {
            Some(n) => n, None => {
                warning!("Unknown builder, use default: cargo");
                "cargo".into() // default use cargo?
            },
        };

        let mut sub_cmd = String::with_capacity(1024);
        if let Some(mirror) = &self.mirror {
            sub_cmd.push_str(&make_cmd(mirror));
            sub_cmd.push_str("\n");
            success!("Build crates mirror: {}", mirror);
        }

        let mut builder_cmd = builder_name.clone();
        let mut sub_build_cmd = vec![];
        let mut cmds_iter = cmds.iter();
        if let Some(cmd0) = cmds_iter.next() {
            if cmd0.starts_with(":") {
                builder_cmd = cmd0.chars().skip(1).collect::<String>();
            } else {
                sub_build_cmd.push(cmd0.clone());
            }
        } else {
            warning!("Docker build param is empty!");
        }
        cmds_iter.for_each(|c| { sub_build_cmd.push(c.clone()); });

        let mut final_sub_cmd = String::with_capacity(512);
        final_sub_cmd.push_str(&builder_cmd);
        final_sub_cmd.push(' ');
        final_sub_cmd.push_str(&sub_build_cmd.iter().map(|arg| escape_arg(arg)).collect::<Vec<_>>().join(" "));
        if !self.builder_args.is_empty() {
            for build_arg in &self.builder_args {
                final_sub_cmd.push(' ');
                final_sub_cmd.push_str(build_arg);
            }
        }
        sub_cmd.push_str(&final_sub_cmd);

        fs::write(DCOLER_TEMP_CMD, &sub_cmd).ok();

        cmd.args(vec!["bash", DCOLER_TEMP_CMD]);

        success!("Docker cmd exec: {:?}", cmd);
        if let Some(mirror) = &self.mirror {
            success!("Build crates mirror: {}", mirror);
        }
        success!("Build command: {}", final_sub_cmd);
        debugging!("Docker temp sub cmd: {}", sub_cmd);
        let r = util_cmd::run_command_and_wait(&mut cmd).map_err(|e| e.into());
        fs::remove_file(DCOLER_TEMP_CMD).ok();
        r
    }
}

fn escape_arg(arg: &str) -> String {
    let mut r = String::with_capacity(arg.len() + 10);
    r.push('\'');
    for c in arg.chars() {
        if c == '\'' || c == '\\' {
            r.push('\\');
        }
        r.push(c);
    }
    r.push('\'');
    r
}

fn make_cmd(mirror: &str) -> String {
    let mut s = String::with_capacity(256);
    s.push_str(&format!(r#"echo '[source.crates-io]
    registry = "https://github.com/rust-lang/crates.io-index"
    replace-with = "mirror"
    [source.mirror]
    registry = "{}"
    ' > /usr/local/cargo/config"#, mirror));
    s
}