1#![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 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 cmd!(sh, "tar -czf {temp_output} -C {input_dir} .").run()?;
183 }
184 "windows" => {
185 #[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}