Skip to main content

xtask/
dist.rs

1// TODO: Remove once nanoserde release > v0.1.35
2#![allow(clippy::ignored_unit_patterns)]
3
4use std::collections::HashMap;
5use std::path::{Path, PathBuf};
6use std::{env, fs};
7
8use anyhow::Result;
9use nanoserde::DeJson;
10use rustc_version::version_meta;
11use xshell::{Shell, cmd};
12
13use crate::Config;
14use crate::commands::{cargo_cmd, cross_cmd};
15use crate::utils::project_root;
16
17#[derive(Debug, DeJson)]
18struct Metadata {
19    packages: Vec<Package>,
20}
21
22#[derive(Debug, DeJson)]
23struct Package {
24    #[allow(dead_code)]
25    name: String,
26    targets: Vec<Target>,
27    version: String,
28}
29
30#[derive(Debug, DeJson)]
31struct Target {
32    kind: Vec<String>,
33    name: String,
34}
35
36pub fn dist(config: &Config) -> Result<()> {
37    env::set_current_dir(project_root())?;
38    if let Some(target) = &config.target {
39        // Safe: xtask is single-threaded and this is set before spawning cargo.
40        unsafe {
41            env::set_var("CARGO_BUILD_TARGET", target);
42        }
43    }
44
45    if dist_dir().exists() {
46        fs::remove_dir_all(dist_dir())?;
47    }
48    let binaries = project_binaries(config)?;
49
50    for (binary, version) in &binaries {
51        let dest_dir = dist_dir().join(format!("{binary}-{version}"));
52        fs::create_dir_all(&dest_dir)?;
53
54        build_binary(config, binary, &dest_dir)?;
55        copy_docs(&dest_dir)?;
56        generate_assets(config, binary, &dest_dir)?;
57        create_archive(binary, version)?;
58    }
59    Ok(())
60}
61
62fn build_binary(config: &Config, binary: &str, dest_dir: &Path) -> Result<()> {
63    let sh = Shell::new()?;
64
65    let cmd_option = if config.cross {
66        cross_cmd(config, &sh)
67    } else {
68        cargo_cmd(config, &sh)
69    };
70    if let Some(cmd) = cmd_option {
71        let mut args = vec!["build", "--release", "--bin", binary];
72        if let Some(target) = &config.target {
73            args.push("--target");
74            args.push(target);
75        }
76        cmd.args(args).run()?;
77    }
78
79    let binary_filename = if cfg!(target_os = "windows") {
80        format!("{binary}.exe")
81    } else {
82        binary.to_string()
83    };
84    let src = release_dir().join(&binary_filename);
85    let dest = dest_dir.join(&binary_filename);
86
87    fs::copy(&src, &dest)?;
88    eprintln!("Copied {} to {}", src.display(), dest.display());
89
90    Ok(())
91}
92
93fn copy_docs(dest_dir: &Path) -> Result<()> {
94    for file in [
95        "CHANGELOG.md",
96        "COPYING",
97        "LICENSE",
98        "LICENSE-APACHE",
99        "LICENSE-MIT",
100        "NOTICE",
101        "README.md",
102        "UNLICENSE",
103    ] {
104        let src = PathBuf::from(file);
105        if src.exists() {
106            let dest = dest_dir.join(file);
107
108            fs::copy(&src, &dest)?;
109            eprintln!("Copied {} to {}", src.display(), dest.display());
110        }
111    }
112    Ok(())
113}
114
115fn generate_assets(config: &Config, binary: &str, dest_dir: &Path) -> Result<()> {
116    let assets: HashMap<&str, (String, String)> = [
117        ("man-page", ("man".to_string(), format!("{binary}.1"))),
118        (
119            "bash",
120            ("completions".to_string(), format!("{binary}.bash")),
121        ),
122        (
123            "elvish",
124            ("completions".to_string(), format!("{binary}.elv")),
125        ),
126        (
127            "fish",
128            ("completions".to_string(), format!("{binary}.fish")),
129        ),
130        (
131            "powershell",
132            ("completions".to_string(), format!("_{binary}.ps1")),
133        ),
134        ("zsh", ("completions".to_string(), format!("_{binary}"))),
135    ]
136    .iter()
137    .cloned()
138    .collect();
139
140    let sh = Shell::new()?;
141    let host_triple = version_meta()?.host;
142
143    for (asset, (directory, filename)) in &assets {
144        let cmd_option = cargo_cmd(config, &sh);
145        if let Some(cmd) = cmd_option {
146            let args = vec![
147                "run",
148                "--target",
149                &host_triple,
150                "--bin",
151                binary,
152                "--",
153                "--generate",
154                asset,
155            ];
156            let output = cmd.args(args).output()?;
157
158            fs::create_dir_all(dest_dir.join(directory))?;
159            fs::write(dest_dir.join(directory).join(filename), output.stdout)?;
160
161            eprintln!("Generated {}/{directory}/{filename}", dest_dir.display());
162        }
163    }
164    Ok(())
165}
166
167fn create_archive(binary: &str, version: &str) -> Result<()> {
168    let sh = Shell::new()?;
169    let temp_dir = sh.create_temp_dir()?;
170
171    let input_dir = dist_dir();
172    let output_filename = match env::consts::OS {
173        "linux" | "macos" => format!("{binary}-{version}{}.tar.gz", target_triple()),
174        "windows" => format!("{binary}-{version}{}.zip", target_triple()),
175        _ => anyhow::bail!("Unsupported OS"),
176    };
177    let temp_output = temp_dir.path().join(output_filename.clone());
178
179    match env::consts::OS {
180        "linux" | "macos" => {
181            // On Unix, create a tarball
182            cmd!(sh, "tar -czf {temp_output} -C {input_dir} .").run()?;
183        }
184        "windows" => {
185            // On Windows, create a zip file
186            #[rustfmt::skip]
187            cmd!(
188                sh,
189                "powershell.exe Compress-Archive -Path {input_dir}/* -DestinationPath {temp_output}"
190            )
191            .run()?;
192        }
193        _ => anyhow::bail!("Unsupported OS"),
194    }
195
196    let final_output = dist_dir().join(output_filename);
197    eprintln!("$ mv {} {}", temp_output.display(), final_output.display());
198    fs::copy(&temp_output, &final_output)?;
199    fs::remove_file(temp_output)?;
200
201    Ok(())
202}
203
204fn target_triple() -> String {
205    env::var_os("CARGO_BUILD_TARGET").map_or_else(String::new, |target| {
206        format!("-{}", target.to_string_lossy())
207    })
208}
209
210fn project_binaries(config: &Config) -> Result<Vec<(String, String)>> {
211    let sh = Shell::new()?;
212    let mut binaries = Vec::new();
213
214    let cmd_option = cargo_cmd(config, &sh);
215    if let Some(cmd) = cmd_option {
216        let args = vec!["metadata", "--no-deps", "--format-version=1"];
217        let output = cmd.args(args).output()?;
218
219        let metadata_json = String::from_utf8(output.stdout)?;
220        let metadata: Metadata = DeJson::deserialize_json(&metadata_json)?;
221
222        for package in metadata.packages {
223            for target in &package.targets {
224                if target.name != "xtask" && target.kind.contains(&"bin".to_string()) {
225                    binaries.push((target.name.clone(), package.version.clone()));
226                }
227            }
228        }
229    }
230    Ok(binaries)
231}
232
233fn dist_dir() -> PathBuf {
234    target_dir().join("dist")
235}
236
237fn release_dir() -> PathBuf {
238    env::var_os("CARGO_BUILD_TARGET").map_or_else(
239        || target_dir().join("release"),
240        |build_target| target_dir().join(build_target).join("release"),
241    )
242}
243
244fn target_dir() -> PathBuf {
245    let relative_path = env::var("CARGO_TARGET_DIR").unwrap_or_else(|_| "target".to_string());
246    PathBuf::from(relative_path)
247}