Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

use the github asset url information #8

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 59 additions & 33 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use anyhow::{anyhow, Error};
use anyhow::{anyhow, Context, Error};
use ini::Ini;
use once_cell::sync::OnceCell;
use serde::{Deserialize, Serialize};
Expand All @@ -17,7 +17,6 @@ use std::os::unix::fs::PermissionsExt;
#[cfg(unix)]
use std::time::SystemTime;

const BASE_URL: &str = "https://github.com/facebook/buck2/releases/download";
const BUCK_RELEASE_URL: &str = "https://github.com/facebook/buck2/tags";

fn get_buckle_dir() -> Result<PathBuf, Error> {
Expand Down Expand Up @@ -81,6 +80,13 @@ fn get_buck2_project_root() -> Option<&'static Path> {
path.as_deref()
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct Asset {
pub name: String,
pub browser_download_url: Url,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct Release {
Expand All @@ -101,7 +107,7 @@ pub struct Release {
pub created_at: Option<String>,
pub published_at: Option<String>,
pub author: serde_json::Value,
pub assets: Vec<serde_json::Value>,
pub assets: Vec<Asset>,
}

fn get_releases(path: &Path) -> Result<Vec<Release>, Error> {
Expand Down Expand Up @@ -164,52 +170,72 @@ fn get_arch() -> Result<&'static str, Error> {

fn download_http(version: String, output_dir: &Path) -> Result<PathBuf, Error> {
let releases = get_releases(output_dir)?;
let mut buck2_path = output_dir.to_path_buf();
let mut dir_path = output_dir.to_path_buf();

let mut unpackable = None;
let mut verbatim = vec![];
let arch = get_arch()?;

let mut release_found = false;
for release in releases {
if release.tag_name == version {
buck2_path.push(release.target_commitish);
release_found = true;
if release.name.as_ref() == Some(&version) {
if release.tag_name == version {
dir_path.push(release.target_commitish);
}
for asset in release.assets {
let name = asset.name;
let url = asset.browser_download_url;
if name == format!("buck2-{}.zst", arch)
|| name == format!("buck2-{}.exe.zst", arch)
{
unpackable = Some((name, url));
} else if name == "prelude_hash" {
verbatim.push((name, url));
}
}
}
}
if !release_found {

let (name, url) = if let Some(unpackable) = unpackable {
unpackable
} else {
return Err(anyhow!("{version} was not available. Please check '{BUCK_RELEASE_URL}' for available releases."));
}
};

// Path to directory that caches buck
let dir_path = buck2_path.clone();
if dir_path.exists() {
// Already downloaded
let binary_path: PathBuf = [&dir_path, Path::new("buck2")].iter().collect();
if binary_path.exists() {
// unpacked binary already present, so do nothing
return Ok(dir_path);
}

buck2_path.push("buck2");
if let Some(prefix) = buck2_path.parent() {
fs::create_dir_all(prefix)?;
// Create the release directory if it doesn't exist
fs::create_dir_all(&dir_path).with_context(|| anyhow!("problem creating {:?}", dir_path))?;

for (name, url) in verbatim {
// Fetch the verbatim items hash and store, do this before the binary so we don't see a partial hash
// We do this as the complete executable is atomic via tmp_file rename
let verbatim_path: PathBuf = [&dir_path, Path::new(&name)].iter().collect();
let resp = reqwest::blocking::get(url)?;
let mut verbatim_file = File::create(&verbatim_path)
.with_context(|| anyhow!("problem creating {:?}", verbatim_path))?;
verbatim_file.write_all(&resp.bytes()?)?;
verbatim_file.flush()?;
}

// Fetch the buck2 archive, decode it, make it executable
let mut tmp_buck2_bin = NamedTempFile::new_in(dir_path.clone())?;
let arch = get_arch()?;
eprintln!("buckle: fetching buck2 {version}");
let resp = reqwest::blocking::get(format!("{BASE_URL}/{version}/buck2-{arch}.zst"))?;
zstd::stream::copy_decode(resp, &tmp_buck2_bin)?;
tmp_buck2_bin.flush()?;
let mut tmp_file = NamedTempFile::new_in(&dir_path)?;
eprintln!("buckle: fetching {name} {version}");
let resp = reqwest::blocking::get(url)?;
zstd::stream::copy_decode(resp, &tmp_file)?;
tmp_file.flush()?;
#[cfg(unix)]
{
let permissions = fs::Permissions::from_mode(0o755);
fs::set_permissions(&tmp_buck2_bin, permissions)?;
fs::set_permissions(&tmp_file, permissions)
.with_context(|| anyhow!("problem setting permissions on {:?}", tmp_file))?;
}
fs::rename(tmp_buck2_bin.path(), &buck2_path)?;

// Also fetch the prelude hash and store it
let mut prelude_path = dir_path.clone();
prelude_path.push("prelude_hash");
let resp = reqwest::blocking::get(format!("{BASE_URL}/{version}/prelude_hash"))?;
let mut prelude_hash = File::create(prelude_path)?;
prelude_hash.write_all(&resp.bytes()?)?;
prelude_hash.flush()?;
// only move to final binary_path once fully written and stable
fs::rename(tmp_file.path(), &binary_path)
.with_context(|| anyhow!("problem renaming {:?} to {:?}", tmp_file, binary_path))?;

Ok(dir_path)
}
Expand Down
56 changes: 55 additions & 1 deletion tests/integration_buck2.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
#[cfg(test)]
use assert_cmd::Command;
use std::fs::File;
use std::io::Read;

/// Integration test that buckle can download buck2 and run it with same arguments.
#[test]
fn test_buck2_latest() {
let tmpdir = tempfile::TempDir::new().unwrap();
let mut cmd = Command::cargo_bin("buckle").unwrap();
cmd.env("BUCKLE_CACHE", tmpdir.path().as_os_str());
cmd.arg("--version");
let assert = cmd.assert();
let stdout = String::from_utf8(assert.get_output().stdout.to_vec()).unwrap();
Expand All @@ -16,12 +20,62 @@ fn test_buck2_latest() {
/// version
#[test]
fn test_buck2_specific_version() {
let tmpdir = tempfile::TempDir::new().unwrap();
let mut cmd = Command::cargo_bin("buckle").unwrap();
cmd.env("BUCKLE_CACHE", tmpdir.path().as_os_str());
cmd.env("USE_BUCK2_VERSION", "2023-07-15");
cmd.arg("--version");
// TODO verify the right version is download after buck2 properly states it's version
let assert = cmd.assert();
let stdout = String::from_utf8(assert.get_output().stdout.to_vec()).unwrap();
assert!(stdout.starts_with("buck2 "), "found {}", stdout);
assert.success();

// Check we have the buck2 binary
let expected_binary_path = tmpdir
.path()
.join("buckle")
.join("6f73c2bc7b5b2024e4ecc451feeaded67714e060")
.join("buck2");
let binary_file = File::open(&expected_binary_path);
assert!(
binary_file.is_ok(),
"expected file {:?} to exist",
expected_binary_path
);

// Check we have the prelude hash
let expected_prelude_path = tmpdir
.path()
.join("buckle")
.join("6f73c2bc7b5b2024e4ecc451feeaded67714e060")
.join("prelude_hash");
let prelude_hash_file = File::open(&expected_prelude_path);
assert!(
prelude_hash_file.is_ok(),
"expected file {:?} to exist",
expected_prelude_path
);

// Check the prelude hash is as expected for the specified version
let mut prelude_hash = String::new();
prelude_hash_file
.unwrap()
.read_to_string(&mut prelude_hash)
.unwrap();
assert_eq!(
prelude_hash.trim_end(),
"be8b3ede73906d6f00055ac6d1caa77f399dcf8f"
);
}

#[test]
fn test_buck2_fail() {
let tmpdir = tempfile::TempDir::new().unwrap();
let mut cmd = Command::cargo_bin("buckle").unwrap();
cmd.env("BUCKLE_CACHE", tmpdir.path().as_os_str());
cmd.arg("--totally-unknown-argument");
let assert = cmd.assert();
let stderr = String::from_utf8(assert.get_output().stderr.to_vec()).unwrap();
assert!(stderr.contains("error: Found argument"), "found {}", stderr);
assert.failure();
}
Loading