Skip to content

Commit

Permalink
Forc dependencies update and check (FuelLabs#122)
Browse files Browse the repository at this point in the history
Enable GitHub-based dependencies update and version checks
  • Loading branch information
digorithm authored Jul 14, 2021
1 parent eed0ee3 commit 6d61441
Show file tree
Hide file tree
Showing 12 changed files with 951 additions and 213 deletions.
255 changes: 250 additions & 5 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions forc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ hex = "0.4.3"
line-col = "0.2"
pest = {git = "https://github.com/sezna/pest.git", rev = "8aa58791f759daf4caee26e8560e862df5a6afb7"}
prettydiff = "0.4.0"
reqwest = { version = "0.11.4", features = ["json"] }
semver = "1.0.3"
serde = {version = "1.0", features = ["derive"]}
source-span = "2.4"
structopt = "0.3"
Expand Down
1 change: 1 addition & 0 deletions forc/src/cli/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ pub mod parse_bytecode;
pub mod publish;
pub mod serve;
pub mod test;
pub mod update;
27 changes: 27 additions & 0 deletions forc/src/cli/commands/update.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
use structopt::{self, StructOpt};

use crate::ops::forc_update;
#[derive(Debug, StructOpt)]
pub struct Command {
/// Path to the project, if not specified, current working directory will be used
#[structopt(short = "p")]
pub path: Option<String>,

/// Dependency to be updated.
/// If `d` isn't specified, all dependencies will be updated.
#[structopt(short = "d")]
pub target_dependency: Option<String>,

/// Checks if the dependencies have newer versions.
/// Won't actually perform the update, will output which
/// ones are up-to-date and outdated.
#[structopt(short = "c", long = "check")]
pub check: bool,
}

pub(crate) async fn exec(command: Command) -> Result<(), String> {
match forc_update::update(command).await {
Ok(_) => Ok(()),
Err(e) => Err(format!("couldn't update dependencies: {}", e)),
}
}
5 changes: 4 additions & 1 deletion forc/src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use structopt::StructOpt;
mod commands;
use self::commands::{
analysis, benchmark, build, coverage, deploy, format, init, mvprun, parse_bytecode, publish,
serve, test,
serve, test, update,
};

use analysis::Command as AnalysisCommand;
Expand All @@ -18,6 +18,7 @@ use parse_bytecode::Command as ParseBytecodeCommand;
use publish::Command as PublishCommand;
use serve::Command as ServeCommand;
use test::Command as TestCommand;
pub use update::Command as UpdateCommand;

#[derive(Debug, StructOpt)]
#[structopt(name = "forc", about = "Fuel HLL Orchestrator")]
Expand All @@ -43,6 +44,7 @@ enum Forc {
Serve(ServeCommand),
Test(TestCommand),
ParseBytecode(ParseBytecodeCommand),
Update(UpdateCommand),
}

pub(crate) async fn run_cli() -> Result<(), String> {
Expand All @@ -60,6 +62,7 @@ pub(crate) async fn run_cli() -> Result<(), String> {
Forc::Serve(command) => serve::exec(command),
Forc::Test(command) => test::exec(command),
Forc::ParseBytecode(command) => parse_bytecode::exec(command),
Forc::Update(command) => update::exec(command).await,
}?;
/*
let content = fs::read_to_string(opt.input.clone())?;
Expand Down
192 changes: 6 additions & 186 deletions forc/src/ops/forc_build.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use crate::utils::dependency::{Dependency, DependencyDetails};
use crate::{
cli::BuildCommand,
utils::dependency,
utils::helpers::{
find_manifest_dir, get_main_file, print_green_err, print_red_err, print_yellow_err,
read_manifest,
Expand All @@ -13,18 +15,12 @@ use source_span::{
use std::fs::File;
use std::io::Write;

use crate::utils::constants;
use crate::utils::manifest::{Dependency, DependencyDetails};
use anyhow::{anyhow, Context, Result};
use anyhow::Result;
use core_lang::{
BuildConfig, BytecodeCompilationResult, CompilationResult, FinalizedAsm, LibraryExports,
Namespace,
};
use curl::easy::Easy;
use dirs::home_dir;
use flate2::read::GzDecoder;
use std::{fs, io::Cursor, path::Path, path::PathBuf, str};
use tar::Archive;
use std::path::PathBuf;

pub fn build(command: BuildCommand) -> Result<Vec<u8>, String> {
let BuildCommand {
Expand Down Expand Up @@ -67,12 +63,12 @@ pub fn build(command: BuildCommand) -> Result<Vec<u8>, String> {

// Download a non-local dependency if the `git` property is set in this dependency.
if let Some(git) = &dep.git {
let downloaded_dep_path = match download_github_dep(
let downloaded_dep_path = match dependency::download_github_dep(
dependency_name,
git,
&dep.branch,
&dep.version,
offline_mode,
offline_mode.into(),
) {
Ok(path) => path,
Err(e) => {
Expand Down Expand Up @@ -119,182 +115,6 @@ pub fn build(command: BuildCommand) -> Result<Vec<u8>, String> {
Ok(main)
}

/// Downloads a non-local dependency that's hosted on GitHub.
/// By default, it stores the dependency in `~/.forc/`.
/// A given dependency `dep` is stored under `~/.forc/dep/default/$owner-$repo-$hash`.
/// If no hash (nor any other type of reference) is provided, Forc
/// will download the default branch at the latest commit.
/// If a branch is specified, it will go in `~/.forc/dep/$branch/$owner-$repo-$hash.
/// If a version is specified, it will go in `~/.forc/dep/$version/$owner-$repo-$hash.
/// Version takes precedence over branch reference.
fn download_github_dep(
dep_name: &String,
repo_base_url: &str,
branch: &Option<String>,
version: &Option<String>,
offline_mode: bool,
) -> Result<String> {
let home_dir = match home_dir() {
None => return Err(anyhow!("Couldn't find home directory (`~/`)")),
Some(p) => p.to_str().unwrap().to_owned(),
};

// Version tag takes precedence over branch reference.
let out_dir = match &version {
Some(v) => format!(
"{}/{}/{}/{}",
home_dir,
constants::FORC_DEPENDENCIES_DIRECTORY,
dep_name,
v
),
// If no version specified, check if a branch was specified
None => match &branch {
Some(b) => format!(
"{}/{}/{}/{}",
home_dir,
constants::FORC_DEPENDENCIES_DIRECTORY,
dep_name,
b
),
// If no version and no branch, use default
None => format!(
"{}/{}/{}/default",
home_dir,
constants::FORC_DEPENDENCIES_DIRECTORY,
dep_name
),
},
};

// Check if dependency is already installed, if so, return its path.
if Path::new(&out_dir).exists() {
for entry in fs::read_dir(&out_dir)? {
let path = entry?.path();
// If the path to that dependency at that branch/version already
// exists and there's a directory inside of it,
// this directory should be the installation path.

if path.is_dir() {
return Ok(path.to_str().unwrap().to_string());
}
}
}

// If offline mode is enabled, don't proceed as it will
// make use of the network to download the dependency from
// GitHub.
// If it's offline mode and the dependency already exists
// locally, then it would've been returned in the block above.
if offline_mode {
return Err(anyhow!(
"Can't build dependency: dependency {} doesn't exist locally and offline mode is enabled",
dep_name
));
}

let github_api_url = build_github_api_url(repo_base_url, &branch, &version);

println!("Downloading {:?} into {:?}", dep_name, out_dir);

match download_tarball(&github_api_url, &out_dir) {
Ok(downloaded_dir) => Ok(downloaded_dir),
Err(e) => Err(anyhow!("couldn't download from {}: {}", &github_api_url, e)),
}
}

/// Builds a proper URL that's used to call GitHub's API.
/// The dependency is specified as `https://github.com/:owner/:project`
/// And the API URL must be like `https://api.github.com/repos/:owner/:project/tarball`
/// Adding a `:ref` at the end makes it download a branch/tag based repo.
/// Omitting it makes it download the default branch at latest commit.
fn build_github_api_url(
dependency_url: &str,
branch: &Option<String>,
version: &Option<String>,
) -> String {
let mut pieces = dependency_url.rsplit("/");

let project_name: &str = match pieces.next() {
Some(p) => p.into(),
None => dependency_url.into(),
};

let owner_name: &str = match pieces.next() {
Some(p) => p.into(),
None => dependency_url.into(),
};

// Version tag takes precedence over branch reference.
match version {
Some(v) => {
format!(
"https://api.github.com/repos/{}/{}/tarball/{}",
owner_name, project_name, v
)
}
// If no version specified, check if a branch was specified
None => match branch {
Some(b) => {
format!(
"https://api.github.com/repos/{}/{}/tarball/{}",
owner_name, project_name, b
)
}
// If no version and no branch, download default branch at latest commit
None => {
format!(
"https://api.github.com/repos/{}/{}/tarball",
owner_name, project_name
)
}
},
}
}

fn download_tarball(url: &str, out_dir: &str) -> Result<String> {
let mut data = Vec::new();
let mut handle = Easy::new();

// Download the tarball.
handle.url(url).context("failed to configure tarball URL")?;
handle
.follow_location(true)
.context("failed to configure follow location")?;

handle
.useragent("forc-builder")
.context("failed to configure User-Agent")?;
{
let mut transfer = handle.transfer();
transfer
.write_function(|new_data| {
data.extend_from_slice(new_data);
Ok(new_data.len())
})
.context("failed to write download data")?;
transfer.perform().context("failed to download tarball")?;
}

// Unpack the tarball.
Archive::new(GzDecoder::new(Cursor::new(data)))
.unpack(out_dir)
.with_context(|| format!("failed to unpack tarball in directory: {}", out_dir))?;

for entry in fs::read_dir(out_dir)? {
let path = entry?.path();
match path.is_dir() {
true => return Ok(path.to_str().unwrap().to_string()),
false => (),
}
}

Err(anyhow!(
"couldn't find downloaded dependency in directory: {}",
out_dir
))
}

/// Takes a dependency and returns a namespace of exported things from that dependency
/// trait implementations are included as well
fn compile_dependency_lib<'source, 'manifest>(
Expand Down
Loading

0 comments on commit 6d61441

Please sign in to comment.