Refactor: Extract new crate binstalk-bins (#1294)

To reduce `binstalk` codegen and enable reuse of it.

Signed-off-by: Jiahao XU <Jiahao_XU@outlook.com>
This commit is contained in:
Jiahao XU 2023-08-16 07:48:42 +10:00 committed by GitHub
parent 76c72469eb
commit 0c5a65fb35
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 114 additions and 87 deletions

View file

@ -10,7 +10,7 @@ edition = "2021"
license = "GPL-3.0-only"
[dependencies]
atomic-file-install = { version = "0.0.0", path = "../atomic-file-install" }
binstalk-bins = { version = "0.0.0", path = "../binstalk-bins" }
binstalk-downloader = { version = "0.7.0", path = "../binstalk-downloader", default-features = false, features = ["gh-api-client"] }
binstalk-fetchers = { version = "0.0.0", path = "../binstalk-fetchers" }
binstalk-registry = { version = "0.0.0", path = "../binstalk-registry" }
@ -20,13 +20,11 @@ command-group = { version = "2.1.0", features = ["with-tokio"] }
compact_str = { version = "0.7.0", features = ["serde"] }
detect-targets = { version = "0.1.10", path = "../detect-targets" }
either = "1.8.1"
home = "0.5.5"
itertools = "0.11.0"
jobslot = { version = "0.2.11", features = ["tokio"] }
leon = { version = "2.0.1", path = "../leon" }
maybe-owned = "0.3.4"
miette = "5.9.0"
normalize-path = { version = "0.2.1", path = "../normalize-path" }
semver = { version = "1.0.17", features = ["serde"] }
strum = "0.25.0"
target-lexicon = { version = "0.12.11", features = ["std"] }

View file

@ -1,337 +0,0 @@
use std::{
borrow::Cow,
fmt,
path::{self, Component, Path, PathBuf},
};
use compact_str::{format_compact, CompactString};
use leon::Template;
use normalize_path::NormalizePath;
use tracing::debug;
use crate::{
errors::BinstallError,
fs::{
atomic_install, atomic_install_noclobber, atomic_symlink_file,
atomic_symlink_file_noclobber,
},
helpers::download::ExtractedFiles,
manifests::cargo_toml_binstall::{PkgFmt, PkgMeta},
};
/// Return true if the path does not look outside of current dir
///
/// * `path` - must be normalized before passing to this function
fn is_valid_path(path: &Path) -> bool {
!matches!(
path.components().next(),
// normalized path cannot have curdir or parentdir,
// so checking prefix/rootdir is enough.
Some(Component::Prefix(..) | Component::RootDir)
)
}
/// Must be called after the archive is downloaded and extracted.
/// This function might uses blocking I/O.
pub fn infer_bin_dir_template(data: &Data, extracted_files: &ExtractedFiles) -> Cow<'static, str> {
let name = data.name;
let target = data.target;
let version = data.version;
// Make sure to update
// fetchers::gh_crate_meta::hosting::{FULL_FILENAMES,
// NOVERSION_FILENAMES} if you update this array.
let gen_possible_dirs: [for<'r> fn(&'r str, &'r str, &'r str) -> String; 8] = [
|name, target, version| format!("{name}-{target}-v{version}"),
|name, target, version| format!("{name}-{target}-{version}"),
|name, target, version| format!("{name}-{version}-{target}"),
|name, target, version| format!("{name}-v{version}-{target}"),
|name, target, _version| format!("{name}-{target}"),
// Ignore the following when updating hosting::{FULL_FILENAMES, NOVERSION_FILENAMES}
|name, _target, version| format!("{name}-{version}"),
|name, _target, version| format!("{name}-v{version}"),
|name, _target, _version| name.to_string(),
];
let default_bin_dir_template = Cow::Borrowed("{ bin }{ binary-ext }");
gen_possible_dirs
.into_iter()
.map(|gen_possible_dir| gen_possible_dir(name, target, version))
.find(|dirname| extracted_files.get_dir(Path::new(&dirname)).is_some())
.map(|mut dir| {
dir.reserve_exact(1 + default_bin_dir_template.len());
dir += "/";
dir += &default_bin_dir_template;
Cow::Owned(dir)
})
// Fallback to no dir
.unwrap_or(default_bin_dir_template)
}
pub struct BinFile {
pub base_name: CompactString,
pub source: PathBuf,
pub archive_source_path: PathBuf,
pub dest: PathBuf,
pub link: Option<PathBuf>,
}
impl BinFile {
/// * `tt` - must have a template with name "bin_dir"
pub fn new(
data: &Data<'_>,
base_name: &str,
tt: &Template<'_>,
no_symlinks: bool,
) -> Result<Self, BinstallError> {
let binary_ext = if data.target.contains("windows") {
".exe"
} else {
""
};
let ctx = Context {
name: data.name,
repo: data.repo,
target: data.target,
version: data.version,
bin: base_name,
binary_ext,
target_related_info: data.target_related_info,
};
let (source, archive_source_path) = if data.meta.pkg_fmt == Some(PkgFmt::Bin) {
(
data.bin_path.to_path_buf(),
data.bin_path.file_name().unwrap().into(),
)
} else {
// Generate install paths
// Source path is the download dir + the generated binary path
let path = tt.render(&ctx)?;
let path_normalized = Path::new(&path).normalize();
if path_normalized.components().next().is_none() {
return Err(BinstallError::EmptySourceFilePath);
}
if !is_valid_path(&path_normalized) {
return Err(BinstallError::InvalidSourceFilePath {
path: path_normalized,
});
}
(data.bin_path.join(&path_normalized), path_normalized)
};
// Destination at install dir + base-name{.extension}
let mut dest = data.install_path.join(ctx.bin);
if !binary_ext.is_empty() {
let binary_ext = binary_ext.strip_prefix('.').unwrap();
// PathBuf::set_extension returns false if Path::file_name
// is None, but we know that the file name must be Some,
// thus we assert! the return value here.
assert!(dest.set_extension(binary_ext));
}
let (dest, link) = if no_symlinks {
(dest, None)
} else {
// Destination path is the install dir + base-name-version{.extension}
let dest_file_path_with_ver = format!("{}-v{}{}", ctx.bin, ctx.version, ctx.binary_ext);
let dest_with_ver = data.install_path.join(dest_file_path_with_ver);
(dest_with_ver, Some(dest))
};
Ok(Self {
base_name: format_compact!("{base_name}{binary_ext}"),
source,
archive_source_path,
dest,
link,
})
}
pub fn preview_bin(&self) -> impl fmt::Display + '_ {
LazyFormat {
base_name: &self.base_name,
source: Path::new(self.source.file_name().unwrap()).display(),
dest: self.dest.display(),
}
}
pub fn preview_link(&self) -> impl fmt::Display + '_ {
OptionalLazyFormat(self.link.as_ref().map(|link| LazyFormat {
base_name: &self.base_name,
source: link.display(),
dest: self.link_dest().display(),
}))
}
/// Return `Ok` if the source exists, otherwise `Err`.
pub fn check_source_exists(
&self,
extracted_files: &ExtractedFiles,
) -> Result<(), BinstallError> {
if extracted_files.has_file(&self.archive_source_path) {
Ok(())
} else {
Err(BinstallError::BinFileNotFound(self.source.clone()))
}
}
fn pre_install_bin(&self) -> Result<(), BinstallError> {
if !self.source.try_exists()? {
return Err(BinstallError::BinFileNotFound(self.source.clone()));
}
#[cfg(unix)]
std::fs::set_permissions(
&self.source,
std::os::unix::fs::PermissionsExt::from_mode(0o755),
)?;
Ok(())
}
pub fn install_bin(&self) -> Result<(), BinstallError> {
self.pre_install_bin()?;
debug!(
"Atomically install file from '{}' to '{}'",
self.source.display(),
self.dest.display()
);
atomic_install(&self.source, &self.dest)?;
Ok(())
}
pub fn install_bin_noclobber(&self) -> Result<(), BinstallError> {
self.pre_install_bin()?;
debug!(
"Installing file from '{}' to '{}' only if dst not exists",
self.source.display(),
self.dest.display()
);
atomic_install_noclobber(&self.source, &self.dest)?;
Ok(())
}
pub fn install_link(&self) -> Result<(), BinstallError> {
if let Some(link) = &self.link {
let dest = self.link_dest();
debug!(
"Create link '{}' pointing to '{}'",
link.display(),
dest.display()
);
atomic_symlink_file(dest, link)?;
}
Ok(())
}
pub fn install_link_noclobber(&self) -> Result<(), BinstallError> {
if let Some(link) = &self.link {
let dest = self.link_dest();
debug!(
"Create link '{}' pointing to '{}' only if dst not exists",
link.display(),
dest.display()
);
atomic_symlink_file_noclobber(dest, link)?;
}
Ok(())
}
fn link_dest(&self) -> &Path {
if cfg!(target_family = "unix") {
Path::new(self.dest.file_name().unwrap())
} else {
&self.dest
}
}
}
/// Data required to get bin paths
pub struct Data<'a> {
pub name: &'a str,
pub target: &'a str,
pub version: &'a str,
pub repo: Option<&'a str>,
pub meta: PkgMeta,
pub bin_path: &'a Path,
pub install_path: &'a Path,
/// More target related info, it's recommend to provide the following keys:
/// - target_family,
/// - target_arch
/// - target_libc
/// - target_vendor
pub target_related_info: &'a dyn leon::Values,
}
#[derive(Clone)]
struct Context<'c> {
name: &'c str,
repo: Option<&'c str>,
target: &'c str,
version: &'c str,
bin: &'c str,
/// Filename extension on the binary, i.e. .exe on Windows, nothing otherwise
binary_ext: &'c str,
target_related_info: &'c dyn leon::Values,
}
impl leon::Values for Context<'_> {
fn get_value<'s>(&'s self, key: &str) -> Option<Cow<'s, str>> {
match key {
"name" => Some(Cow::Borrowed(self.name)),
"repo" => self.repo.map(Cow::Borrowed),
"target" => Some(Cow::Borrowed(self.target)),
"version" => Some(Cow::Borrowed(self.version)),
"bin" => Some(Cow::Borrowed(self.bin)),
"binary-ext" => Some(Cow::Borrowed(self.binary_ext)),
// Soft-deprecated alias for binary-ext
"format" => Some(Cow::Borrowed(self.binary_ext)),
key => self.target_related_info.get_value(key),
}
}
}
struct LazyFormat<'a> {
base_name: &'a str,
source: path::Display<'a>,
dest: path::Display<'a>,
}
impl fmt::Display for LazyFormat<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} ({} -> {})", self.base_name, self.source, self.dest)
}
}
struct OptionalLazyFormat<'a>(Option<LazyFormat<'a>>);
impl fmt::Display for OptionalLazyFormat<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(lazy_format) = self.0.as_ref() {
fmt::Display::fmt(lazy_format, f)
} else {
Ok(())
}
}
}

View file

@ -16,6 +16,7 @@ use tokio::task;
use tracing::{error, warn};
use crate::{
bins,
helpers::{
cargo_toml::Error as CargoTomlError, cargo_toml_workspace::Error as LoadManifestFromWSError,
},
@ -105,20 +106,6 @@ pub enum BinstallError {
#[label(transparent)]
FetchError(Box<FetchError>),
/// Failed to render template.
///
/// - Code: `binstall::template`
/// - Exit: 69
#[error("Failed to render template: {0}")]
#[diagnostic(severity(error), code(binstall::template))]
#[source_code(transparent)]
#[label(transparent)]
TemplateRenderError(
#[from]
#[diagnostic_source]
leon::RenderError,
),
/// Failed to download or failed to decode the body.
///
/// - Code: `binstall::download`
@ -257,13 +244,17 @@ pub enum BinstallError {
)]
NoViableTargets,
/// Bin file is not found.
/// Failed to find or install binaries.
///
/// - Code: `binstall::binfile`
/// - Code: `binstall::bins`
/// - Exit: 88
#[error("bin file {0} not found")]
#[diagnostic(severity(error), code(binstall::binfile))]
BinFileNotFound(PathBuf),
#[error("failed to find or install binaries: {0}")]
#[diagnostic(
severity(error),
code(binstall::targets::none_host),
help("Try to specify --target")
)]
BinFile(#[from] bins::Error),
/// `Cargo.toml` of the crate does not have section "Package".
///
@ -281,25 +272,6 @@ pub enum BinstallError {
#[diagnostic(severity(error), code(binstall::SourceFilePath))]
DuplicateSourceFilePath { path: PathBuf },
/// bin-dir configuration provided generates source path outside
/// of the temporary dir.
///
/// - Code: `binstall::cargo_manifest`
/// - Exit: 91
#[error(
"bin-dir configuration provided generates source path outside of the temporary dir: {path}"
)]
#[diagnostic(severity(error), code(binstall::SourceFilePath))]
InvalidSourceFilePath { path: PathBuf },
/// bin-dir configuration provided generates empty source path.
///
/// - Code: `binstall::cargo_manifest`
/// - Exit: 92
#[error("bin-dir configuration provided generates empty source path")]
#[diagnostic(severity(error), code(binstall::SourceFilePath))]
EmptySourceFilePath,
/// Fallback to `cargo-install` is disabled.
///
/// - Code: `binstall::no_fallback_to_cargo_install`
@ -364,7 +336,6 @@ impl BinstallError {
UrlParse(_) => 65,
TemplateParseError(..) => 67,
FetchError(..) => 68,
TemplateRenderError(..) => 69,
Download(_) => 68,
SubProcess { .. } => 70,
Io(_) => 74,
@ -377,11 +348,9 @@ impl BinstallError {
SuperfluousVersionOption => 84,
UnspecifiedBinaries => 86,
NoViableTargets => 87,
BinFileNotFound(_) => 88,
BinFile(_) => 88,
CargoTomlMissingPackage(_) => 89,
DuplicateSourceFilePath { .. } => 90,
InvalidSourceFilePath { .. } => 91,
EmptySourceFilePath => 92,
NoFallbackToCargoInstall => 94,
InvalidPkgFmt(..) => 95,
GhApiErr(..) => 96,

View file

@ -1,13 +1,11 @@
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
mod bins;
pub mod errors;
pub mod helpers;
pub mod ops;
use atomic_file_install as fs;
use binstalk_bins as bins;
pub use binstalk_fetchers as fetchers;
pub use binstalk_registry as registry;
pub use binstalk_types as manifests;
pub use detect_targets::{get_desired_targets, DesiredTargets, TARGET};
pub use home;

View file

@ -261,7 +261,7 @@ async fn download_extract_and_verify(
.iter()
.zip(bin_files)
.filter_map(|(bin, bin_file)| {
match bin_file.check_source_exists(&extracted_files) {
match bin_file.check_source_exists(&mut |p| extracted_files.has_file(p)) {
Ok(()) => Some(Ok(bin_file)),
// This binary is optional
@ -284,7 +284,8 @@ async fn download_extract_and_verify(
}
}
})
.collect::<Result<Vec<bins::BinFile>, BinstallError>>()
.collect::<Result<Vec<bins::BinFile>, bins::Error>>()
.map_err(BinstallError::from)
}
fn collect_bin_files(
@ -314,7 +315,9 @@ fn collect_bin_files(
.bin_dir
.as_deref()
.map(Cow::Borrowed)
.unwrap_or_else(|| bins::infer_bin_dir_template(&bin_data, extracted_files));
.unwrap_or_else(|| {
bins::infer_bin_dir_template(&bin_data, &mut |p| extracted_files.get_dir(p).is_some())
});
let template = Template::parse(&bin_dir)?;
@ -323,7 +326,7 @@ fn collect_bin_files(
.binaries
.iter()
.map(|bin| bins::BinFile::new(&bin_data, bin.name.as_str(), &template, no_symlinks))
.collect::<Result<Vec<_>, BinstallError>>()?;
.collect::<Result<Vec<_>, bins::Error>>()?;
let mut source_set = BTreeSet::new();

View file

@ -51,7 +51,7 @@ impl Resolution {
impl ResolutionFetch {
pub fn install(self, opts: &Options) -> Result<CrateInfo, BinstallError> {
type InstallFp = fn(&bins::BinFile) -> Result<(), BinstallError>;
type InstallFp = fn(&bins::BinFile) -> Result<(), bins::Error>;
let (install_bin, install_link): (InstallFp, InstallFp) = match (opts.no_track, opts.force)
{