1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
use std::path::{Path, PathBuf};
use std::{env, fs};

use anyhow::Result;
use nanoserde::DeJson;
use xshell::Shell;

use crate::commands::cargo_cmd;
use crate::utils::project_root;
use crate::Config;

#[derive(Debug, DeJson)]
struct Metadata {
    packages: Vec<Package>,
}

#[derive(Debug, DeJson)]
struct Package {
    #[allow(dead_code)]
    name: String,
    targets: Vec<Target>,
    version: String,
}

#[derive(Debug, DeJson)]
struct Target {
    kind: Vec<String>,
    name: String,
}

pub fn dist(config: &Config) -> Result<()> {
    env::set_current_dir(project_root())?;

    if dist_dir().exists() {
        fs::remove_dir_all(dist_dir())?;
    }
    let binaries = project_binaries(config)?;

    for (binary, version) in &binaries {
        let dest_dir = dist_dir().join(format!("{binary}-{version}"));
        fs::create_dir_all(&dest_dir)?;

        build_binary(config, binary, &dest_dir)?;
        copy_docs(&dest_dir)?;
        // TODO: generate_assets(config, binary, &dest_dir)?;
    }
    Ok(())
}

fn build_binary(config: &Config, binary: &str, dest_dir: &Path) -> Result<()> {
    let sh = Shell::new()?;

    let cmd_option = cargo_cmd(config, &sh);
    if let Some(cmd) = cmd_option {
        let args = vec!["build", "--release", "--bin", binary];
        cmd.args(args).run()?;
    }

    let binary_filename = if cfg!(target_os = "windows") {
        format!("{binary}.exe")
    } else {
        binary.to_string()
    };
    let src = release_dir().join(&binary_filename);
    let dest = dest_dir.join(&binary_filename);

    fs::copy(&src, &dest)?;
    eprintln!("Copied {} to {}", src.display(), dest.display());

    Ok(())
}

fn copy_docs(dest_dir: &Path) -> Result<()> {
    for file in [
        "CHANGELOG.md",
        "LICENSE",
        "LICENSE-APACHE",
        "LICENSE-MIT",
        "README.md",
    ] {
        let src = PathBuf::from(file);
        if src.exists() {
            let dest = dest_dir.join(file);

            fs::copy(&src, &dest)?;
            eprintln!("Copied {} to {}", src.display(), dest.display());
        }
    }
    Ok(())
}

fn project_binaries(config: &Config) -> Result<Vec<(String, String)>> {
    let sh = Shell::new()?;
    let mut binaries = Vec::new();

    let cmd_option = cargo_cmd(config, &sh);
    if let Some(cmd) = cmd_option {
        let args = vec!["metadata", "--no-deps", "--format-version=1"];
        let output = cmd.args(args).output()?;

        let metadata_json = String::from_utf8(output.stdout)?;
        let metadata: Metadata = DeJson::deserialize_json(&metadata_json)?;

        for package in metadata.packages {
            for target in &package.targets {
                if target.name != "xtask" && target.kind.contains(&"bin".to_string()) {
                    binaries.push((target.name.clone(), package.version.clone()));
                }
            }
        }
    }
    Ok(binaries)
}

fn dist_dir() -> PathBuf {
    target_dir().join("dist")
}

fn release_dir() -> PathBuf {
    target_dir().join("release")
}

fn target_dir() -> PathBuf {
    let relative_path = env::var("CARGO_TARGET_DIR").unwrap_or_else(|_| "target".to_string());
    PathBuf::from(relative_path)
}