Skip to content

Commit b0e274f

Browse files
committed
Implement a new --extract-all feature
When this is passed, `ubi` will extract the entire archive instead of looking for just an executable.
1 parent 38a0a98 commit b0e274f

File tree

12 files changed

+590
-117
lines changed

12 files changed

+590
-117
lines changed

Changes.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
## 0.4.0
22

3+
- The `ubi` CLI tool now takes an optional `--extract-all` argument. If this is passed, it will only
4+
look for archive files and it will extract the entire contents of an archive it finds. There is
5+
also a new corresponding `UbiBuilder::extract_all` method. Requested by @Entze (Lukas Grassauer).
6+
GH #68.
37
- The `UbiBuilder::install_dir` method now takes `AsRef<Path>` instead of `PathBuf`, which should
48
make it more convenient to use.
59
- Previously, `ubi` would create the install directory very early in its process, well before it had

README.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,17 @@ Options:
9292
-e, --exe <exe> The name of this project's executable. By default this is the
9393
same as the project name, so for houseabsolute/precious we look
9494
for precious or precious.exe. When running on Windows the
95-
".exe" suffix will be added as needed.
95+
".exe" suffix will be added as needed. You cannot pass
96+
--extract-all when this is set
97+
--extract-all Pass this to tell `ubi` to extract all files from the archive.
98+
By default `ubi` will only extract an executable from an
99+
archive file. But if this is true, it will simply unpack the
100+
archive file in the specified directory. If all of the contents
101+
of the archive file share a top-level directory, that directory
102+
will be removed during unpacking. In other words, if an archive
103+
contains `./project/some-file` and `./project/docs.md`, it will
104+
extract them as `some-file` and `docs.md`. You cannot pass
105+
--exe when this is set.
96106
-m, --matching <matching> A string that will be matched against the release filename when
97107
there are multiple matching files for your OS/arch. For
98108
example, there may be multiple releases for an OS/arch that

ubi-cli/src/main.rs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ async fn main() {
5757

5858
const MAX_TERM_WIDTH: usize = 100;
5959

60+
#[allow(clippy::too_many_lines)]
6061
fn cmd() -> Command {
6162
Command::new("ubi")
6263
.version(env!("CARGO_PKG_VERSION"))
@@ -98,8 +99,22 @@ fn cmd() -> Command {
9899
"The name of this project's executable. By default this is the same as the",
99100
" project name, so for houseabsolute/precious we look for precious or",
100101
r#" precious.exe. When running on Windows the ".exe" suffix will be added"#,
101-
" as needed.",
102+
" as needed. You cannot pass --extract-all when this is set.",
102103
)))
104+
.arg(
105+
Arg::new("extract-all")
106+
.long("extract-all")
107+
.action(ArgAction::SetTrue)
108+
.help(concat!(
109+
// Pass this to tell `ubi` to extract all files from the archive. By default
110+
// `ubi` will only extract an executable from an archive file. But if this is
111+
// true, it will simply unpack the archive file. If all of the contents of
112+
// the archive file share a top-level directory, that directory will be removed
113+
// during unpacking. In other words, if an archive contains
114+
// `./project/some-file` and `./project/docs.md`, it will extract them as
115+
// `some-file` and `docs.md`. You cannot pass --exe when this is set.
116+
)),
117+
)
103118
.arg(
104119
Arg::new("matching")
105120
.long("matching")
@@ -196,6 +211,9 @@ fn make_ubi<'a>(
196211
if let Some(e) = matches.get_one::<String>("exe") {
197212
builder = builder.exe(e);
198213
}
214+
if matches.get_flag("extract-all") {
215+
builder = builder.extract_all();
216+
}
199217
if let Some(ft) = matches.get_one::<String>("forge") {
200218
builder = builder.forge(ForgeType::from_str(ft)?);
201219
}

ubi/src/builder.rs

Lines changed: 54 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use crate::{
33
forge::{Forge, ForgeType},
44
github::GitHub,
55
gitlab::GitLab,
6-
installer::Installer,
6+
installer::{ArchiveInstaller, ExeInstaller, Installer},
77
picker::AssetPicker,
88
ubi::Ubi,
99
};
@@ -32,6 +32,7 @@ pub struct UbiBuilder<'a> {
3232
install_dir: Option<PathBuf>,
3333
matching: Option<&'a str>,
3434
exe: Option<&'a str>,
35+
extract_all: bool,
3536
github_token: Option<&'a str>,
3637
gitlab_token: Option<&'a str>,
3738
platform: Option<&'a Platform>,
@@ -100,12 +101,25 @@ impl<'a> UbiBuilder<'a> {
100101
/// Set the name of the executable to look for in archive files. By default this is the same as
101102
/// the project name, so for `houseabsolute/precious` we look for `precious` or
102103
/// `precious.exe`. When running on Windows the ".exe" suffix will be added as needed.
104+
///
105+
/// You cannot call `extract_all` if you set this.
103106
#[must_use]
104107
pub fn exe(mut self, exe: &'a str) -> Self {
105108
self.exe = Some(exe);
106109
self
107110
}
108111

112+
/// Call this to tell `ubi` to extract all files from the archive. By default `ubi` will look
113+
/// for an executable in an archive file. But if this is true, it will simply unpack the archive
114+
/// file in the specified directory.
115+
///
116+
/// You cannot set `exe` when this is true.
117+
#[must_use]
118+
pub fn extract_all(mut self) -> Self {
119+
self.extract_all = true;
120+
self
121+
}
122+
109123
/// Set a GitHub token to use for API requests. If this is not set then this will be taken from
110124
/// the `GITHUB_TOKEN` env var if it is set.
111125
#[must_use]
@@ -175,6 +189,9 @@ impl<'a> UbiBuilder<'a> {
175189
if self.url.is_some() && (self.project.is_some() || self.tag.is_some()) {
176190
return Err(anyhow!("You cannot set a url with a project or tag"));
177191
}
192+
if self.exe.is_some() && self.extract_all {
193+
return Err(anyhow!("You cannot set exe and extract_all"));
194+
}
178195

179196
let platform = self.determine_platform()?;
180197

@@ -183,20 +200,37 @@ impl<'a> UbiBuilder<'a> {
183200
let asset_url = self.url.map(Url::parse).transpose()?;
184201
let (project_name, forge_type) =
185202
parse_project_name(self.project, asset_url.as_ref(), self.forge.clone())?;
186-
let exe = exe_name(self.exe, &project_name, &platform);
203+
let installer = self.new_installer(&project_name, &platform)?;
187204
let forge = self.new_forge(project_name, &forge_type)?;
188-
let install_path = install_path(self.install_dir, &exe)?;
189205
let is_musl = self.is_musl.unwrap_or_else(|| platform_is_musl(&platform));
190206

191207
Ok(Ubi::new(
192208
forge,
193209
asset_url,
194-
AssetPicker::new(self.matching, platform, is_musl),
195-
Installer::new(install_path, exe),
210+
AssetPicker::new(self.matching, platform, is_musl, self.extract_all),
211+
installer,
196212
reqwest_client()?,
197213
))
198214
}
199215

216+
fn new_installer(&self, project_name: &str, platform: &Platform) -> Result<Box<dyn Installer>> {
217+
let (install_path, exe) = if self.extract_all {
218+
(install_path(self.install_dir.as_deref(), None)?, None)
219+
} else {
220+
let exe = exe_name(self.exe, project_name, platform);
221+
(
222+
install_path(self.install_dir.as_deref(), Some(&exe))?,
223+
Some(exe),
224+
)
225+
};
226+
227+
Ok(if self.extract_all {
228+
Box::new(ArchiveInstaller::new(install_path))
229+
} else {
230+
Box::new(ExeInstaller::new(install_path, exe.unwrap()))
231+
})
232+
}
233+
200234
fn new_forge(
201235
&self,
202236
project_name: String,
@@ -283,17 +317,19 @@ fn parse_project_name(
283317
))
284318
}
285319

286-
fn install_path(install_dir: Option<PathBuf>, exe: &str) -> Result<PathBuf> {
287-
let mut path = if let Some(i) = install_dir {
288-
i
320+
fn install_path(install_dir: Option<&Path>, exe: Option<&str>) -> Result<PathBuf> {
321+
let mut install_dir = if let Some(install_dir) = install_dir {
322+
install_dir.to_path_buf()
289323
} else {
290-
let mut i = env::current_dir()?;
291-
i.push("bin");
292-
i
324+
let mut install_dir = env::current_dir()?;
325+
install_dir.push("bin");
326+
install_dir
293327
};
294-
path.push(exe);
295-
debug!("install path = {}", path.to_string_lossy());
296-
Ok(path)
328+
if let Some(exe) = exe {
329+
install_dir.push(exe);
330+
}
331+
debug!("install path = {}", install_dir.to_string_lossy());
332+
Ok(install_dir)
297333
}
298334

299335
fn exe_name(exe: Option<&str>, project_name: &str, platform: &Platform) -> String {
@@ -303,12 +339,13 @@ fn exe_name(exe: Option<&str>, project_name: &str, platform: &Platform) -> Strin
303339
_ => e.to_string(),
304340
}
305341
} else {
306-
let parts = project_name.split('/').collect::<Vec<&str>>();
307-
let e = parts[parts.len() - 1].to_string();
342+
// We know that this contains a slash because it already went through `parse_project_name`
343+
// successfully.
344+
let e = project_name.split('/').last().unwrap();
308345
if matches!(platform.target_os, OS::Windows) {
309346
format!("{e}.exe")
310347
} else {
311-
e
348+
e.to_string()
312349
}
313350
};
314351
debug!("exe name = {name}");

ubi/src/extension.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,23 @@ impl Extension {
5353
}
5454
}
5555

56+
pub(crate) fn is_archive(&self) -> bool {
57+
match self {
58+
Extension::Bz | Extension::Bz2 | Extension::Exe | Extension::Gz | Extension::Xz => {
59+
false
60+
}
61+
Extension::Tar
62+
| Extension::TarBz
63+
| Extension::TarBz2
64+
| Extension::TarGz
65+
| Extension::TarXz
66+
| Extension::Tbz
67+
| Extension::Tgz
68+
| Extension::Txz
69+
| Extension::Zip => true,
70+
}
71+
}
72+
5673
pub(crate) fn from_path<S: AsRef<str>>(path: S) -> Result<Option<Extension>> {
5774
let path = path.as_ref();
5875
let Some(ext_str) = Path::new(path).extension() else {
@@ -146,6 +163,8 @@ mod test {
146163
#[test_case("i386-linux-ghcup-0.1.30.0-linux_amd64", Ok(None))]
147164
#[test_case("foo.bar", Err(ExtensionError::UnknownExtension { path: "foo.bar".to_string(), ext: "bar".to_string() }.into()))]
148165
fn from_path(path: &str, expect: Result<Option<Extension>>) {
166+
crate::test_case::init_logging();
167+
149168
let ext = Extension::from_path(path);
150169
if expect.is_ok() {
151170
assert!(ext.is_ok());

0 commit comments

Comments
 (0)