1use std::{
2 io,
3 path::{Path, PathBuf},
4};
5
6use clap::Args;
7use lux_lib::{
8 build::{Build, BuildBehaviour},
9 config::{Config, ConfigBuilder},
10 lockfile::LocalPackage,
11 lua_installation::LuaInstallation,
12 lua_rockspec::RemoteLuaRockspec,
13 lua_version::LuaVersion,
14 operations::{Install, InstallProject, PackageInstallSpec},
15 package::{PackageName, PackageReq},
16 progress::MultiProgress,
17 tree::{self, FlatDistTree, InstallTree},
18 workspace::Workspace,
19};
20use miette::{miette, IntoDiagnostic, Result, WrapErr};
21use path_slash::PathExt;
22use tempfile::{tempdir, TempDir};
23use tokio::fs::{self, File};
24use walkdir::WalkDir;
25use zip::{write::SimpleFileOptions, ZipWriter};
26
27use crate::{
28 args::{OutputFormat, PackageOrRockspec},
29 workspace::exists_matching_workspace_member,
30};
31
32#[derive(Args)]
33pub struct FlatArchive {
34 #[clap(value_parser)]
49 package_or_rockspec: Option<PackageOrRockspec>,
50
51 #[arg(short, long, visible_short_alias = 'd')]
53 destination: Option<PathBuf>,
54
55 #[clap(default_value_t=CompressionMethod::default())]
56 #[arg(short, long, value_enum, visible_short_alias = 'c')]
57 compression_method: CompressionMethod,
58
59 #[arg(long, default_value = "text", value_enum, ignore_case = true)]
60 output_format: OutputFormat,
61}
62
63#[derive(Clone, clap::ValueEnum, Default)]
64enum CompressionMethod {
65 #[default]
67 Stored,
68 Deflated,
70 Bzip2,
72 Xz,
74 Zstd,
76 Lzma,
78}
79
80pub async fn dist_archive(args: FlatArchive, config: Config) -> Result<()> {
81 let staging_dir = tempdir().into_diagnostic()?;
82 let config = ConfigBuilder::from(config)
83 .wrap_bin_scripts(Some(false))
85 .user_tree(Some(staging_dir.path().to_path_buf()))
86 .build()?;
87
88 let (pkg, install_root) = match &args.package_or_rockspec {
89 None => install_project(None, &staging_dir, &config).await,
90 Some(PackageOrRockspec::Package(package_req))
91 if exists_matching_workspace_member(package_req)? =>
92 {
93 install_project(Some(package_req.name()), &staging_dir, &config).await
94 }
95 Some(PackageOrRockspec::Package(package)) => {
96 install_package(package, &staging_dir, &config).await
97 }
98 Some(PackageOrRockspec::RockSpec(rockspec_path)) => {
99 install_rockspec(rockspec_path, &staging_dir, &config).await
100 }
101 }?;
102
103 let destination = args
104 .destination
105 .clone()
106 .map(|dest| {
107 if dest.is_dir() {
108 dest.join(format!("{}-{}.zip", pkg.name(), pkg.version()))
109 } else {
110 dest
111 }
112 })
113 .unwrap_or(PathBuf::from(format!(
114 "{}-{}.zip",
115 pkg.name(),
116 pkg.version()
117 )));
118
119 zip_dir(&install_root, &destination, &args.compression_method).await?;
120
121 match args.output_format {
122 OutputFormat::Json => {
123 println!("{}", serde_json::to_string(&destination).into_diagnostic()?)
124 }
125 OutputFormat::Text => println!("Wrote archive to {}", destination.display()),
126 }
127
128 Ok(())
129}
130
131async fn install_project(
132 package: Option<&PackageName>,
133 staging_dir: &TempDir,
134 config: &Config,
135) -> Result<(LocalPackage, PathBuf)> {
136 let workspace = Workspace::current_or_err()?;
137 let project = match package {
138 Some(package) => workspace.select_member(package)?,
139 None => workspace.single_member()?,
140 };
141 let lua_version = project.lua_version(config)?;
142 let tree = FlatDistTree::new(staging_dir.path().to_path_buf(), lua_version, config)?;
143 Ok((
144 InstallProject::new()
145 .project(project)
146 .config(config)
147 .tree(&tree)
148 .build()
149 .await?,
150 tree.root(),
151 ))
152}
153
154async fn install_package(
155 package: &PackageReq,
156 staging_dir: &TempDir,
157 config: &Config,
158) -> Result<(LocalPackage, PathBuf)> {
159 let lua_version = LuaVersion::from(config)?.clone();
160 let tree = FlatDistTree::new(staging_dir.path().to_path_buf(), lua_version, config)?;
161 let packages = Install::new(config)
162 .package(
163 PackageInstallSpec::new(package.clone(), tree::EntryType::Entrypoint)
164 .build_behaviour(BuildBehaviour::Force)
165 .build(),
166 )
167 .tree(tree.clone())
168 .install()
169 .await?;
170 let package = packages
171 .into_iter()
172 .find(|pkg| pkg.name() == package.name())
173 .ok_or_else(|| miette!("package was not installed"))?;
174 Ok((package, tree.root()))
175}
176
177async fn install_rockspec(
178 rockspec_path: &Path,
179 staging_dir: &TempDir,
180 config: &Config,
181) -> Result<(LocalPackage, PathBuf)> {
182 let content = tokio::fs::read_to_string(&rockspec_path)
183 .await
184 .into_diagnostic()?;
185 let lua_version = LuaVersion::from(config)?.clone();
186 let rockspec = match rockspec_path
187 .extension()
188 .map(|ext| ext.to_string_lossy().to_string())
189 .unwrap_or("".into())
190 .as_str()
191 {
192 "rockspec" => Ok(RemoteLuaRockspec::new(&content)?),
193 _ => Err(miette!(
194 "expected a path to a .rockspec or a package requirement."
195 )),
196 }?;
197 let progress = MultiProgress::new_arc(config);
198 let bar = progress.map(|p| p.new_bar());
199 let lua = LuaInstallation::new(
200 &lua_version,
201 config,
202 &progress.map(|progress| progress.new_bar()),
203 )
204 .await?;
205 let tree = FlatDistTree::new(staging_dir.path().to_path_buf(), lua_version, config)?;
206 let package = Build::new()
207 .rockspec(&rockspec)
208 .lua(&lua)
209 .tree(&tree)
210 .entry_type(tree::EntryType::Entrypoint)
211 .config(config)
212 .progress(&bar)
213 .build()
214 .await?;
215 Ok((package, tree.root()))
216}
217
218async fn zip_dir(src_dir: &Path, dest_file: &Path, method: &CompressionMethod) -> Result<()> {
219 if dest_file.exists() {
220 return Err(miette!("File {} already exists!", dest_file.display()));
221 }
222 let temp_archive = PathBuf::from(format!("{}.part", dest_file.display()));
223 let archive = File::create(&temp_archive)
224 .await
225 .into_diagnostic()?
226 .into_std()
227 .await;
228 let walkdir = WalkDir::new(src_dir);
229 let mut zip = ZipWriter::new(archive);
230
231 let compression_method = match method {
232 CompressionMethod::Stored => zip::CompressionMethod::Stored,
233 CompressionMethod::Deflated => zip::CompressionMethod::Deflated,
234 CompressionMethod::Bzip2 => zip::CompressionMethod::Bzip2,
235 CompressionMethod::Xz => zip::CompressionMethod::Xz,
236 CompressionMethod::Zstd => zip::CompressionMethod::Zstd,
237 CompressionMethod::Lzma => zip::CompressionMethod::Lzma,
238 };
239
240 #[cfg(target_family = "unix")]
241 let options = SimpleFileOptions::default()
242 .compression_method(compression_method)
243 .unix_permissions(0o755);
244
245 #[cfg(target_family = "windows")]
246 let options = SimpleFileOptions::default().compression_method(compression_method);
247
248 for entry_result in walkdir.into_iter() {
249 let entry = entry_result.map_err(|err| {
250 miette!(
251 "Error while traversing directory {}: {}.",
252 src_dir.display(),
253 err,
254 )
255 })?;
256 let path = entry.path();
257 let relative_path = path.strip_prefix(src_dir).into_diagnostic()?;
258 let relative_path_str = relative_path.to_slash_lossy().to_string();
259 if path.is_file() {
260 zip.start_file(relative_path_str, options)
261 .into_diagnostic()?;
262 let mut f = File::open(path).await.into_diagnostic()?.into_std().await;
263 io::copy(&mut f, &mut zip).into_diagnostic()?;
264 } else if !relative_path.as_os_str().is_empty() {
265 zip.add_directory(relative_path_str, options)
266 .into_diagnostic()?;
267 }
268 }
269 zip.finish().into_diagnostic()?;
270 fs::rename(&temp_archive, &dest_file)
271 .await
272 .into_diagnostic()
273 .wrap_err(format!(
274 "Error renaming {} to {}",
275 temp_archive.display(),
276 dest_file.display()
277 ))?;
278 Ok(())
279}