#![doc = include_str!("../readme.md")]
use ureq::{ Agent, Response };
use std::{
env::{ consts::{ ARCH, OS }, VarError, var },
fmt::{ Display, Formatter, Result as FmtResult },
fs::{ File, remove_file },
io::copy,
path::{ Path, PathBuf }
};
use zip::{ result::ZipError, ZipArchive };
static CRATE_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));
#[derive(Debug)]
pub enum Error<'a> {
NotProvidedPlatform,
NonExistsVersion(&'a str),
NonExistsPlatformVersion(&'a str),
GitHubApi((u16, String)),
VarError(VarError),
Io(std::io::Error),
Ureq(Box<ureq::Error>),
Zip(ZipError)
}
impl<'a> Display for Error<'a> {
fn fmt(&self, f: &mut Formatter) -> FmtResult {
match self {
Error::NotProvidedPlatform => {
write!(f, "Pre-built binaries for `{}-{}` platform don't provided", OS, ARCH)
},
Error::NonExistsVersion(version) => {
write!(f, "Pre-built binaries version `{}` not exists", version)
},
Error::NonExistsPlatformVersion(version) => {
write!(
f,
"Pre-built binaries version `{}` for `{}-{}` platform don't provided",
version, OS, ARCH
)
},
Error::GitHubApi((status, response)) => {
write!(f, "GitHub API response error: {} {}", status, response)
},
Error::VarError(err) => write!(f, "{}", err),
Error::Io(err) => write!(f, "{}", err),
Error::Ureq(err) => write!(f, "{}", err),
Error::Zip(err) => write!(f, "{}", err)
}
}
}
fn prepare_asset_version(version: &str) -> String {
if !version.contains("rc") {
return String::from(version)
}
let parts = version.split_once("rc").unwrap();
format!("{}rc-{}", parts.0, parts.1)
}
fn get_protoc_asset_name<'a>(
version: &str, os: &str, arch: &str
) -> Result<String, Error<'a>> {
let asset_os = match os {
"linux" => "linux",
"macos" => "osx",
"windows" => "win",
_ => return Err(Error::NotProvidedPlatform)
};
let asset_arch = match os {
"linux" => match arch {
"aarch64" => "aarch_64",
"powerpc64" => "ppcle_64",
"s390x" => "s390_64",
"x86" => "x86_32",
"x86_64" => "x86_64",
_ => return Err(Error::NotProvidedPlatform)
},
"macos" => match arch {
"aarch64" => "aarch_64",
"x86_64" => "x86_64",
_ => return Err(Error::NotProvidedPlatform)
},
"windows" => match arch {
"x86" => "32",
"x86_64" => "64",
_ => return Err(Error::NotProvidedPlatform)
},
_ => unreachable!()
};
let os_arch_delimiter = match os {
"windows" => "",
_ => "-"
};
Ok(format!(
"protoc-{}-{}{}{}", prepare_asset_version(version), asset_os, os_arch_delimiter, asset_arch
))
}
#[allow(clippy::result_large_err)]
fn get(url: &str) -> Result<Response, ureq::Error> {
Agent::new().get(url).set("User-Agent", CRATE_USER_AGENT).call()
}
fn install<'a>(
version: &'a str, out_dir: &Path, protoc_asset_name: &String, protoc_out_dir: &PathBuf
) -> Result<(), Error<'a>> {
match get(&format!(
"https://api.github.com/repos/protocolbuffers/protobuf/releases/tags/v{}", version
)) {
Ok(_) => {},
Err(ureq::Error::Status(code, response)) => {
match code {
404 => return Err(Error::NonExistsVersion(version)),
_ => {
let text = response.into_string().map_err(Error::Io)?;
return Err(Error::GitHubApi((code, text)))
}
}
},
Err(err) => return Err(Error::Ureq(Box::new(err)))
}
let protoc_asset_file_name = format!("{}.zip", protoc_asset_name);
let response = match get(&format!(
"https://github.com/protocolbuffers/protobuf/releases/download/v{}/{}",
version, protoc_asset_file_name
)) {
Ok(response) => response,
Err(ureq::Error::Status(code, response)) => {
match code {
404 => return Err(Error::NonExistsPlatformVersion(version)),
_ => {
let text = response.into_string().map_err(Error::Io)?;
return Err(Error::GitHubApi((code, text)))
}
}
},
Err(err) => return Err(Error::Ureq(Box::new(err)))
};
let protoc_asset_file_path = out_dir.join(&protoc_asset_file_name);
if protoc_asset_file_path.exists() {
remove_file(&protoc_asset_file_path).map_err(Error::Io)?;
}
let mut file = File::options()
.create(true).read(true).write(true)
.open(&protoc_asset_file_path)
.map_err(Error::Io)?;
let mut response_reader = response.into_reader();
copy(&mut response_reader, &mut file).map_err(Error::Io)?;
let mut archive = ZipArchive::new(file).map_err(Error::Zip)?;
archive.extract(protoc_out_dir).map_err(Error::Zip)?;
remove_file(&protoc_asset_file_path).map_err(Error::Io)?;
Ok(())
}
pub fn init(version: &str) -> Result<(PathBuf, PathBuf), Error> {
let out_dir = PathBuf::from(var("OUT_DIR").map_err(Error::VarError)?);
let protoc_asset_name = get_protoc_asset_name(version, OS, ARCH)?;
let protoc_out_dir = out_dir.join(&protoc_asset_name);
if !protoc_out_dir.exists() {
install(version, &out_dir, &protoc_asset_name, &protoc_out_dir)?;
}
let mut protoc_bin = protoc_out_dir.clone();
protoc_bin.push("bin");
protoc_bin.push(format!("protoc{}", match OS { "windows" => ".exe", _ => "" }));
let mut protoc_include = protoc_out_dir;
protoc_include.push("include");
Ok((protoc_bin, protoc_include))
}
#[cfg(test)]
mod test {
use std::env::temp_dir;
use crate::{
CRATE_USER_AGENT, Error, prepare_asset_version, get_protoc_asset_name, get, install
};
#[test]
fn prepare_assets_versions() {
assert_eq!(prepare_asset_version("22.0"), "22.0");
assert_eq!(prepare_asset_version("22.0-rc3"), "22.0-rc-3");
}
#[test]
fn get_protoc_assets_names() {
fn check_not_provided_platform_err(result: Result<String, Error>) {
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), Error::NotProvidedPlatform { .. }));
}
fn check_protoc_asset_name_result(result: Result<String, Error>, expect: &str) {
assert!(result.is_ok());
assert_eq!(result.unwrap(), expect);
}
check_not_provided_platform_err(get_protoc_asset_name("22.0", "freebsd", "x86_64"));
check_not_provided_platform_err(get_protoc_asset_name("22.0", "freebsd", "aarch64"));
check_not_provided_platform_err(get_protoc_asset_name("22.0", "windows", "aarch64"));
check_protoc_asset_name_result(
get_protoc_asset_name("22.0", "linux", "x86"),
"protoc-22.0-linux-x86_32"
);
check_protoc_asset_name_result(
get_protoc_asset_name("22.0-rc3", "macos", "aarch64"),
"protoc-22.0-rc-3-osx-aarch_64"
);
check_protoc_asset_name_result(
get_protoc_asset_name("21.12", "windows", "x86_64"),
"protoc-21.12-win64"
);
}
#[test]
fn get_fail() {
let result = get("https://bf2d04e1aea451f5b530e4c36666c0f0.com");
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), ureq::Error::Transport { .. }));
}
#[test]
fn get_user_agent() {
let result = get("https://httpbin.org/get");
assert!(result.is_ok());
let response = result.unwrap();
assert_eq!(response.status(), 200);
let text_result = response.into_string();
assert!(text_result.is_ok());
let text = text_result.unwrap();
assert!(text.contains(CRATE_USER_AGENT))
}
#[test]
fn install_fail_non_exists_version() {
let version = "0.1";
let out_dir = temp_dir().join("protoc-prebuilt-unit");
let protos_asset_name = get_protoc_asset_name(version, "windows", "x86_64").unwrap();
let protoc_out_dir = out_dir.join(&protos_asset_name);
let result = install(version, &out_dir, &protos_asset_name, &protoc_out_dir);
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), Error::NonExistsVersion { .. }));
}
#[test]
fn install_fail_non_exists_platform_version() {
let version = "3.19.4";
let out_dir = temp_dir().join("protoc-prebuilt-unit");
let protos_asset_name = get_protoc_asset_name(version, "macos", "aarch64").unwrap();
let protoc_out_dir = out_dir.join(&protos_asset_name);
let result = install(version, &out_dir, &protos_asset_name, &protoc_out_dir);
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), Error::NonExistsPlatformVersion { .. }));
}
}