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