1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132
//! Download official protobuf compiler (protoc) releases with a single command, pegged to the
//! version of your choice.
use anyhow::bail;
use reqwest::StatusCode;
use std::io::Cursor;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::{env, fs};
/// Downloads an official [release] of the protobuf compiler (protoc) and returns the path to it.
///
/// The release archive matching the given `version` will be downloaded, and the protoc binary will
/// be extracted into a subdirectory of `out_dir`. You can choose a `version` from the
/// [release] page, for example "21.2". Don't prefix it with a "v".
///
/// `out_dir` can be anywhere you want, but if calling this function from a build script, you should
/// probably use the `OUT_DIR` env var (which is set by Cargo automatically for build scripts).
///
/// A previously downloaded protoc binary of the correct version will be reused if already present
/// in `out_dir`.
///
/// # Examples:
///
/// ```no_run
/// # use std::env;
/// # use std::path::Path;
/// // From within build.rs...
/// let out_dir = env::var("OUT_DIR").unwrap();
/// let protoc_path = protoc_fetcher::protoc("21.2", Path::new(&out_dir));
/// ```
///
/// If you are using [tonic-build] (or [prost-build]), you can instruct it to use the fetched
/// `protoc` binary by setting the `PROTOC` env var.
///
/// ```no_run
/// # use std::env;
/// # use std::path::Path;
/// # let out_dir = env::var("OUT_DIR").unwrap();
/// # let path_to_my_protos = Path::new("a/b/c");
/// # let protoc_path = protoc_fetcher::protoc("21.2", Path::new(&out_dir)).unwrap();
/// env::set_var("PROTOC", &protoc_path);
/// tonic_build::compile_protos(path_to_my_protos);
/// ```
///
/// [release]: https://github.com/protocolbuffers/protobuf/releases
/// [tonic-build]: https://crates.io/crates/tonic-build
/// [prost-build]: https://crates.io/crates/prost-build
pub fn protoc(version: &str, out_dir: &Path) -> anyhow::Result<PathBuf> {
let protoc_path = ensure_protoc_installed(version, out_dir)?;
Ok(protoc_path)
}
/// Checks for an existing protoc of the given version; if not found, then the official protoc
/// release is downloaded and "installed", i.e., the binary is copied from the release archive
/// into the `generated` directory.
fn ensure_protoc_installed(version: &str, install_dir: &Path) -> anyhow::Result<PathBuf> {
let release_name = get_protoc_release_name(version);
let protoc_dir = install_dir.join(format!("protoc-fetcher/{release_name}"));
let protoc_path = protoc_dir.join("bin/protoc");
if protoc_path.exists() {
println!("protoc with correct version is already installed.");
} else {
println!("protoc v{version} not found, downloading...");
download_protoc(&protoc_dir, &release_name, version)?;
}
println!(
"`protoc --version`: {}",
get_protoc_version(&protoc_path).unwrap()
);
Ok(protoc_path)
}
fn download_protoc(protoc_dir: &Path, release_name: &str, version: &str) -> anyhow::Result<()> {
let archive_url = protoc_release_archive_url(release_name, version);
let response = reqwest::blocking::get(archive_url)?;
if response.status() != StatusCode::OK {
bail!(
"Error downloading release archive: {} {}",
response.status(),
response.text().unwrap_or_default()
);
}
println!("Download successful.");
fs::create_dir_all(protoc_dir)?;
let cursor = Cursor::new(response.bytes()?);
zip_extract::extract(cursor, protoc_dir, false)?;
println!("Extracted archive.");
let protoc_path = protoc_dir.join("bin/protoc");
if !protoc_path.exists() {
bail!("Extracted protoc archive, but could not find bin/protoc!");
}
println!("protoc installed successfully: {:?}", &protoc_path);
Ok(())
}
fn protoc_release_archive_url(release_name: &str, version: &str) -> String {
let archive_url =
format!("https://github.com/protocolbuffers/protobuf/releases/download/v{version}/{release_name}.zip");
println!("Release URL: {archive_url}");
archive_url
}
fn get_protoc_release_name(version: &str) -> String {
let mut platform = env::consts::OS;
let mut arch = env::consts::ARCH;
println!("Detected: {}, {}", platform, arch);
// Adjust values to match the protoc release names. Examples:
// - linux 64-bit: protoc-21.2-linux-x86_64.zip
// - macos ARM: protoc-21.2-osx-aarch_64.zip
if platform == "macos" {
platform = "osx"; // protoc is stuck in the past XD
}
if arch == "aarch64" {
arch = "aarch_64";
}
format!("protoc-{version}-{platform}-{arch}")
}
fn get_protoc_version(protoc_path: &Path) -> anyhow::Result<String> {
let version = String::from_utf8(Command::new(&protoc_path).arg("--version").output()?.stdout)?;
Ok(version)
}