diff --git a/forc/Cargo.toml b/forc/Cargo.toml index d12c2a5af91..54f756ddfdd 100644 --- a/forc/Cargo.toml +++ b/forc/Cargo.toml @@ -14,7 +14,6 @@ ansi_term = "0.12" anyhow = "1.0.41" clap = { version = "3.1.2", features = ["env", "derive"] } dirs = "3.0.2" -flate2 = "1.0.20" fuel-asm = "0.1" fuel-gql-client = { version = "0.3", default-features = false } fuel-tx = "0.5" diff --git a/forc/src/ops/forc_dep_check.rs b/forc/src/ops/forc_dep_check.rs deleted file mode 100644 index ef49c257c11..00000000000 --- a/forc/src/ops/forc_dep_check.rs +++ /dev/null @@ -1,133 +0,0 @@ -use crate::utils::{ - dependency, - helpers::{read_manifest, user_forc_directory}, -}; -use anyhow::{anyhow, Result}; -use semver::Version; -use std::{ - path::{Path, PathBuf}, - str, -}; -use sway_utils::find_manifest_dir; - -/// Forc check will check if there are updates to Github-based dependencies. -/// If a target dependency `-d` is passed, it will check only this one dependency. -/// Otherwise, it will check for all dependencies in the manifest. -/// Note that this won't automatically update the dependencies, it will only -/// point out newer versions of the dependencies. -/// If a dependency was specified in the manifest _without_ a tag/version, -/// `forc update` can automatically update to the latest version. -/// If a dependency has a tag, `forc dep_check` will let you know if there's a newer tag -/// and then you can decide whether to update it in the manifest or not. -pub async fn check(path: Option, target_dependency: Option) -> Result<()> { - let this_dir = if let Some(path) = path { - PathBuf::from(path) - } else { - std::env::current_dir()? - }; - - let manifest_dir = match find_manifest_dir(&this_dir) { - Some(dir) => dir, - None => { - return Err(anyhow!( - "No manifest file found in this directory or any parent directories of it: {:?}", - this_dir - )) - } - }; - - let mut manifest = read_manifest(&manifest_dir).unwrap(); - - let dependencies = dependency::get_detailed_dependencies(&mut manifest); - - match target_dependency { - // Target dependency (`-d`) specified - Some(target_dep) => match dependencies.get(&target_dep) { - Some(dep) => Ok(check_dependency(&target_dep, dep).await?), - None => return Err(anyhow!("dependency {} not found", target_dep)), - }, - // No target dependency specified, try and update all dependencies - None => { - for (dependency_name, dep) in dependencies { - check_dependency(&dependency_name, dep).await?; - } - Ok(()) - } - } -} - -async fn check_dependency( - dependency_name: &str, - dep: &dependency::DependencyDetails, -) -> Result<()> { - let user_forc_dir = user_forc_directory(); - let dep_dir = user_forc_dir.join(dependency_name); - let target_directory = match &dep.branch { - Some(branch) => dep_dir.join(branch), - None => dep_dir.join("default"), - }; - - // Currently we only handle checks on github-based dependencies - if let Some(git) = &dep.git { - match &dep.version { - Some(version) => check_tagged_dependency(dependency_name, version, git).await?, - None => check_untagged_dependency(git, &target_directory, dependency_name, dep).await?, - } - } - Ok(()) -} - -async fn check_tagged_dependency( - dependency_name: &str, - current_version: &str, - git_repo: &str, -) -> Result<()> { - let releases = dependency::get_github_repo_releases(git_repo).await?; - - let current_release = Version::parse(current_version)?; - - let mut latest = current_release.clone(); - - for release in &releases { - let release_version = Version::parse(release)?; - - if release_version.gt(¤t_release) { - latest = release_version; - } - } - - if current_release.ne(&latest) { - println!( - "[{}] not up-to-date. Current version: {}, latest: {}", - dependency_name, current_release, latest - ); - } else { - println!( - "[{}] up-to-date. Current version: {}", - dependency_name, current_release, - ); - } - - Ok(()) -} - -async fn check_untagged_dependency( - git_repo: &str, - target_directory: &Path, - dependency_name: &str, - dep: &dependency::DependencyDetails, -) -> Result<()> { - let current = dependency::get_current_dependency_version(target_directory)?; - - let latest_hash = dependency::get_latest_commit_sha(git_repo, &dep.branch).await?; - - if current.hash == latest_hash { - println!("{} is up-to-date", dependency_name); - } else { - println!( - "[{}] not up-to-date. Current version: {}, latest: {}", - dependency_name, current.hash, latest_hash - ); - } - Ok(()) -} diff --git a/forc/src/ops/forc_update.rs b/forc/src/ops/forc_update.rs index f74b250be84..f18380b66a4 100644 --- a/forc/src/ops/forc_update.rs +++ b/forc/src/ops/forc_update.rs @@ -1,31 +1,31 @@ -use crate::{ - cli::UpdateCommand, - ops::forc_dep_check, - utils::{ - dependency, - helpers::{read_manifest, user_forc_directory}, - }, -}; +use crate::{cli::UpdateCommand, utils::helpers::read_manifest}; use anyhow::{anyhow, Result}; -use std::{path::PathBuf, str}; +use std::path::PathBuf; use sway_utils::find_manifest_dir; -/// Forc update will update the contents inside the Forc dependencies directory. -/// If a dependency `d` is passed as parameter, it will only try and update that specific dependency. -/// Otherwise, it will try and update all GitHub-based dependencies in a project's `Forc.toml`. -/// It won't automatically update dependencies that have a version specified, if you have -/// specified a version for a dependency and want to update it you should, instead, -/// run `forc update --check` to check for updates for all GitHub-based dependencies, and if -/// a new version is detected and return, manually update your `Forc.toml` with this new version. +/// Running `forc update` will check for updates for the entire dependency graph and commit new +/// semver-compatible versions to the `Forc.lock` file. For git dependencies, the commit is updated +/// to the HEAD of the specified branch, or remains unchanged in the case a tag is specified. Path +/// dependencies remain unchanged as they are always sourced directly. +/// +/// Run `forc update --check` to perform a dry-run and produce a list of updates that will be +/// performed across all dependencies without actually committing them to the lock file. +/// +/// Use the `--package ` flag to update only a specific package throughout the +/// dependency graph. pub async fn update(command: UpdateCommand) -> Result<()> { if command.check { - return forc_dep_check::check(command.path, command.target_dependency).await; + // TODO + unimplemented!( + "When set, output whether target dep may be updated but don't commit to lock file" + ); } let UpdateCommand { path, - target_dependency, check: _, + // TODO: Use `package` here rather than `target_dependency` + .. } = command; let this_dir = if let Some(path) = path { @@ -44,58 +44,10 @@ pub async fn update(command: UpdateCommand) -> Result<()> { } }; - let mut manifest = read_manifest(&manifest_dir).unwrap(); + let _manifest = read_manifest(&manifest_dir).unwrap(); - let dependencies = dependency::get_detailed_dependencies(&mut manifest); - - match target_dependency { - // Target dependency (`-d`) specified - Some(target_dep) => match dependencies.get(&target_dep) { - Some(dep) => Ok(update_dependency(&target_dep, dep).await?), - None => return Err(anyhow!("dependency {} not found", target_dep)), - }, - // No target dependency specified, try and update all dependencies - None => { - for (dependency_name, dep) in dependencies { - update_dependency(&dependency_name, dep).await?; - } - Ok(()) - } - } -} - -async fn update_dependency( - dependency_name: &str, - dep: &dependency::DependencyDetails, -) -> Result<()> { - // Currently we only handle updates on github-based dependencies - if let Some(git) = &dep.git { - match &dep.version { - // Automatically updating a dependency that has a tag/version specified in `Forc.toml` - // would mean to update the `Forc.toml` file, which I believe isn't a very - // nice behavior. Instead, if a tag/version is specified, the user should - // lookup for a desired version and manually specify it in `Forc.toml`. - Some(version) => println!("Ignoring update for {} at version {}: Forc update not implemented for dependencies with specified tag. To update to another tag, change the tag in `Forc.toml` and run the build command.", dependency_name, version), - None => { - let forc_dir = user_forc_directory(); - let dep_dir = forc_dir.join(dependency_name); - let target_directory = match &dep.branch { - Some(branch) => dep_dir.join(branch), - None => dep_dir.join("default"), - }; - - let current = dependency::get_current_dependency_version(&target_directory)?; - - let latest_hash = dependency::get_latest_commit_sha(git, &dep.branch).await?; - - if current.hash == latest_hash { - println!("{} is up-to-date", dependency_name); - } else { - dependency::replace_dep_version(&target_directory, git, dep)?; - println!("{}: {} -> {}", dependency_name, current.hash, latest_hash); - } - } - } - } - Ok(()) + // TODO + unimplemented!( + "Check the graph for git and registry changes and update the `Forc.lock` file accordingly" + ) } diff --git a/forc/src/ops/mod.rs b/forc/src/ops/mod.rs index 15e11339b71..c6df7a4120c 100644 --- a/forc/src/ops/mod.rs +++ b/forc/src/ops/mod.rs @@ -1,7 +1,6 @@ pub mod forc_abi_json; pub mod forc_build; pub mod forc_clean; -pub mod forc_dep_check; pub mod forc_deploy; pub mod forc_explorer; pub mod forc_fmt; diff --git a/forc/src/utils/dependency.rs b/forc/src/utils/dependency.rs index ae9c6407918..c2d495f9cec 100644 --- a/forc/src/utils/dependency.rs +++ b/forc/src/utils/dependency.rs @@ -1,17 +1,6 @@ -use crate::utils::{helpers::user_forc_directory, manifest::Manifest}; -use anyhow::{anyhow, bail, Context, Result}; -use flate2::read::GzDecoder; +use crate::utils::manifest::Manifest; use serde::{Deserialize, Serialize}; -use std::collections::hash_map::DefaultHasher; -use std::hash::{Hash, Hasher}; -use std::io::Read; -use std::{ - collections::HashMap, - fs, - io::Cursor, - path::{Path, PathBuf}, -}; -use tar::Archive; +use std::collections::HashMap; // A collection of remote dependency related functions @@ -50,298 +39,6 @@ impl From for OfflineMode { } } -pub type GitHubAPICommitsResponse = Vec; - -#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct GithubCommit { - pub sha: String, -} -/// VersionedDependencyDirectory holds the path to the directory where a given -/// GitHub-based dependency is installed and its respective git hash. -#[derive(Debug)] -pub struct VersionedDependencyDirectory { - pub hash: String, - pub path: PathBuf, -} - -pub type GitHubRepoReleases = Vec; - -#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct TaggedRelease { - #[serde(rename = "tag_name")] - pub tag_name: String, - #[serde(rename = "target_commitish")] - pub target_commitish: String, - pub name: String, - pub draft: bool, - pub prerelease: bool, - #[serde(rename = "created_at")] - pub created_at: String, - #[serde(rename = "published_at")] - pub published_at: String, -} - -/// 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. -pub fn download_github_dep( - dep_name: &str, - repo_base_url: &str, - branch: &Option, - version: &Option, - offline_mode: OfflineMode, -) -> Result { - // hash the dep name into a number to avoid bad characters - let mut s = DefaultHasher::new(); - dep_name.hash(&mut s); - let hashed_dep_name = s.finish().to_string(); - - // Version tag takes precedence over branch reference. - let forc_dir = user_forc_directory(); - let dep_dir = forc_dir.join(hashed_dep_name); - let out_dir = match &version { - Some(v) => dep_dir.join(v), - // If no version specified, check if a branch was specified - None => match &branch { - Some(b) => dep_dir.join(b), - // If no version and no branch, use default - None => dep_dir.join("default"), - }, - }; - - // Check if dependency is already installed, if so, return its path. - if 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 let OfflineMode::Yes = 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_repo_api_url(repo_base_url, branch, version); - - let _ = crate::utils::helpers::println_green(&format!( - " Downloading {:?} ({:?})", - 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. -pub fn build_github_repo_api_url( - dependency_url: &str, - branch: &Option, - version: &Option, -) -> String { - let dependency_url = dependency_url.trim_end_matches('/'); - let mut pieces = dependency_url.rsplit('/'); - - let project_name: &str = match pieces.next() { - Some(p) => p, - None => dependency_url, - }; - - let owner_name: &str = match pieces.next() { - Some(p) => p, - None => dependency_url, - }; - - // 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 - ) - } - }, - } -} - -pub fn download_tarball(url: &str, out_dir: &Path) -> Result { - let mut data = Vec::new(); - - // Download the tarball. - let handle = ureq::builder().user_agent("forc-builder").build(); - let resp = handle.get(url).call()?; - resp.into_reader().read_to_end(&mut data)?; - - // Unpack the tarball. - Archive::new(GzDecoder::new(Cursor::new(data))) - .unpack(out_dir) - .with_context(|| { - format!( - "failed to unpack tarball in directory: {}", - out_dir.display() - ) - })?; - - 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.display(), - )) -} - -pub fn replace_dep_version( - target_directory: &Path, - git: &str, - dep: &DependencyDetails, -) -> Result<()> { - let current = get_current_dependency_version(target_directory)?; - - let api_url = build_github_repo_api_url(git, &dep.branch, &dep.version); - download_tarball(&api_url, target_directory)?; - - // Delete old one - match fs::remove_dir_all(current.path) { - Ok(_) => Ok(()), - Err(e) => { - return Err(anyhow!( - "failed to remove old version of the dependency ({}): {}", - git, - e - )) - } - } -} - -pub fn get_current_dependency_version(dep_dir: &Path) -> Result { - let mut entries = - fs::read_dir(dep_dir).context(format!("couldn't read directory {}", dep_dir.display()))?; - let entry = match entries.next() { - Some(entry) => entry, - None => bail!("Dependency directory is empty. Run `forc build` to install dependencies."), - }; - - let path = entry?.path(); - if !path.is_dir() { - bail!("{} isn't a directory.", dep_dir.display()) - } - - let file_name = path.file_name().unwrap(); - // Dependencies directories are named as "$repo_owner-$repo-$concatenated_hash" - let hash = file_name - .to_str() - .with_context(|| format!("Invalid utf8 in dependency name: {}", path.display()))? - .split('-') - .last() - .with_context(|| format!("Unexpected dependency naming scheme: {}", path.display()))? - .into(); - Ok(VersionedDependencyDirectory { hash, path }) -} - -// Returns the _truncated_ (e.g `e6940e4`) latest commit hash of a -// GitHub repository given a branch. If branch is None, the default branch is used. -pub async fn get_latest_commit_sha( - dependency_url: &str, - branch: &Option, -) -> Result { - // Quick protection against `git` dependency URL ending with `/`. - let dependency_url = dependency_url.trim_end_matches('/'); - - let mut pieces = dependency_url.rsplit('/'); - - let project_name: &str = match pieces.next() { - Some(p) => p, - None => dependency_url, - }; - - let owner_name: &str = match pieces.next() { - Some(p) => p, - None => dependency_url, - }; - - let api_endpoint = match branch { - Some(b) => { - format!( - "https://api.github.com/repos/{}/{}/commits?sha={}&per_page=1", - owner_name, project_name, b - ) - } - None => { - format!( - "https://api.github.com/repos/{}/{}/commits?per_page=1", - owner_name, project_name - ) - } - }; - - let client = reqwest::Client::builder() - .user_agent("forc-builder") - .build()?; - - let resp = client.get(&api_endpoint).send().await?; - - let hash_vec = resp.json::().await?; - - // `take(7)` because the truncated SHA1 used by GitHub is 7 chars long. - let truncated_hash: String = hash_vec[0].sha.chars().take(7).collect(); - - if truncated_hash.is_empty() { - bail!( - "failed to extract hash from GitHub commit history API, response: {:?}", - hash_vec - ) - } - - Ok(truncated_hash) -} - // Helper to get only detailed dependencies (`Dependency::Detailed`). pub fn get_detailed_dependencies(manifest: &mut Manifest) -> HashMap { let mut dependencies: HashMap = HashMap::new(); @@ -359,37 +56,3 @@ pub fn get_detailed_dependencies(manifest: &mut Manifest) -> HashMap Result> { - // Quick protection against `git` dependency URL ending with `/`. - let dependency_url = dependency_url.trim_end_matches('/'); - - let mut pieces = dependency_url.rsplit('/'); - - let project_name: &str = match pieces.next() { - Some(p) => p, - None => dependency_url, - }; - - let owner_name: &str = match pieces.next() { - Some(p) => p, - None => dependency_url, - }; - - let api_endpoint = format!( - "https://api.github.com/repos/{}/{}/releases", - owner_name, project_name - ); - - let client = reqwest::Client::builder() - .user_agent("forc-builder") - .build()?; - - let resp = client.get(&api_endpoint).send().await?; - - let releases_vec = resp.json::().await?; - - let semver_releases: Vec = releases_vec.iter().map(|r| r.tag_name.to_owned()).collect(); - - Ok(semver_releases) -}