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

Prevent failure while creating BuildPlan from lock file in the case of a dependency removal #1974

Closed
wants to merge 25 commits into from
Closed
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
f985bf0
BuildPlan generation from lock file does not break when there is a de…
kayagokalp Jun 14, 2022
b4219a7
While removing the deleted nodes handle orphaned childs
kayagokalp Jun 16, 2022
9495057
removed debug println
kayagokalp Jun 16, 2022
e97c684
removed_deps removed from BuildPlan
kayagokalp Jun 17, 2022
38af42e
While checking from parent's manifest check for package name as well
kayagokalp Jun 17, 2022
e469399
removed unnecessary reference
kayagokalp Jun 17, 2022
4be5880
split validate into generate_diff and validate
kayagokalp Jun 20, 2022
aa90853
apply_pkg_diff visits nodes to delete in reverse BFS order
kayagokalp Jun 20, 2022
316a871
docs added for generate_diff
kayagokalp Jun 20, 2022
4548afb
Merge branch 'master' into kayagokalp/1778
eureka-cpu Jun 20, 2022
9bda581
merge master
kayagokalp Jun 23, 2022
38cc164
removed unnecessary logic in apply_diff_after_lock
kayagokalp Jun 23, 2022
344ceaa
doc updates
kayagokalp Jun 23, 2022
cb255e2
clippy && extra dereferencing removed
kayagokalp Jun 23, 2022
cb6a2e0
Apply suggestions from code review effor -> effort
kayagokalp Jun 23, 2022
bb40017
duplicate code removed
kayagokalp Jun 27, 2022
1905046
change visibility of apply_pkg_diff and from_lock to private
kayagokalp Jun 27, 2022
5d466ec
doc updated
kayagokalp Jun 27, 2022
6de10da
Merge master
kayagokalp Jun 27, 2022
4ac21d4
patch checks are added
kayagokalp Jun 27, 2022
057afeb
check for patches corrected
kayagokalp Jun 27, 2022
f11442b
lsp uses load_from_manifest, from_lock_file is private
kayagokalp Jun 29, 2022
476efa7
Merge branch 'master' into kayagokalp/1778
kayagokalp Jun 29, 2022
2bc0d94
add use::collections::HashSet
kayagokalp Jun 29, 2022
b590f03
Merge branch 'master' into kayagokalp/1778
mitchmindtree Jul 4, 2022
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
7 changes: 7 additions & 0 deletions forc-pkg/src/manifest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,13 @@ impl Manifest {
.and_then(|deps| deps.get(dep_name))
}

/// Retrieve the listed patches for the given name.
pub fn patch(&self, patch_name: &str) -> Option<&BTreeMap<std::string::String, Dependency>> {
self.patch
.as_ref()
.and_then(|patches| patches.get(patch_name))
}

/// Finds and returns the name of the dependency associated with a package of the specified
/// name if there is one.
///
Expand Down
139 changes: 114 additions & 25 deletions forc-pkg/src/pkg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use crate::{
use anyhow::{anyhow, bail, Context, Error, Result};
use forc_util::{
find_file_name, git_checkouts_directory, kebab_to_snake_case, print_on_failure,
print_on_success, print_on_success_library, println_yellow_err,
print_on_success, print_on_success_library, println_red, println_yellow_err,
};
use fuel_tx::StorageSlot;
use fuels_types::JsonABI;
Expand Down Expand Up @@ -242,8 +242,13 @@ impl BuildPlan {
sway_git_tag: &str,
) -> Result<Self> {
let lock_path = forc_util::lock_path(manifest.dir());
let plan_result = BuildPlan::from_lock_file(&lock_path, sway_git_tag);
let from_lock = BuildPlan::from_lock_file(&lock_path, sway_git_tag);
let removed_deps = from_lock
.as_ref()
.map(|from_lock| from_lock.1.clone())
.unwrap_or_default();

let plan_result = from_lock.map(|from_lock| from_lock.0);
// Retrieve the old lock file state so we can produce a diff.
let old_lock = plan_result
.as_ref()
Expand All @@ -267,10 +272,10 @@ impl BuildPlan {
// If there are no issues with the BuildPlan generated from the lock file
// Check and apply the diff.
if new_lock_cause.is_none() {
let diff = plan.validate(manifest, sway_git_tag)?;
if !diff.added.is_empty() || !diff.removed.is_empty() {
let diff = plan.generate_diff(manifest)?;
if !diff.added.is_empty() || !diff.removed.is_empty() || !removed_deps.is_empty() {
new_lock_cause = Some(anyhow!("lock file did not match manifest `diff`"));
plan = plan.apply_pkg_diff(diff, sway_git_tag, offline)?;
plan = plan.apply_pkg_diff(&diff, sway_git_tag, offline)?;
}
}

Expand All @@ -293,12 +298,13 @@ impl BuildPlan {
}

/// Create a new build plan from an existing one. Needs the difference with the existing plan with the lock.
pub fn apply_pkg_diff(
fn apply_pkg_diff(
&self,
pkg_diff: PkgDiff,
pkg_diff: &PkgDiff,
sway_git_tag: &str,
offline_mode: bool,
) -> Result<Self> {
self.validate(sway_git_tag)?;
let mut graph = self.graph.clone();
let mut path_map = self.path_map.clone();

Expand All @@ -307,7 +313,7 @@ impl BuildPlan {
.last()
.ok_or_else(|| anyhow!("Invalid Graph"))?;
let PkgDiff { added, removed } = pkg_diff;
remove_deps(&mut graph, &path_map, proj_node, &removed);
remove_deps(&mut graph, &path_map, proj_node, removed);

let mut visited_map: HashMap<Pinned, NodeIx> = graph
.node_references()
Expand All @@ -319,12 +325,13 @@ impl BuildPlan {
&mut graph,
&mut path_map,
&self.compilation_order,
&added,
added,
sway_git_tag,
offline_mode,
&mut visited_map,
)?;
let compilation_order = compilation_order(&graph)?;
self.validate(sway_git_tag)?;
Ok(Self {
graph,
path_map,
Expand All @@ -333,26 +340,58 @@ impl BuildPlan {
}

/// Attempt to load the build plan from the `Lock`.
pub fn from_lock(proj_path: &Path, lock: &Lock, sway_git_tag: &str) -> Result<Self> {
let graph = lock.to_graph()?;
/// Returns the best effort BuildPlan and the packages removed
/// from project's manifest file after the lock file is generated.
///
/// The returned removed dependencies are already removed from the dependency graph.
fn from_lock(
proj_path: &Path,
lock: &Lock,
sway_git_tag: &str,
) -> Result<(Self, Vec<DependencyName>)> {
let mut graph = lock.to_graph()?;
let tmp_compilation_order = compilation_order(&graph)?;
let (path_map, removed_deps) =
graph_to_path_map(proj_path, &graph, &tmp_compilation_order, sway_git_tag)?;
let removed_deps_vec: Vec<DependencyName> = removed_deps
.clone()
.into_iter()
.map(|dep| graph[dep].name.clone())
.collect();

// Remove the unresolveable path dependencies from the graph
apply_diff_after_lock(removed_deps, &mut graph)?;

// Since apply_diff_after_lock removes removed dependencies from the graph compilation order may change
let compilation_order = compilation_order(&graph)?;
let path_map = graph_to_path_map(proj_path, &graph, &compilation_order, sway_git_tag)?;
Ok(Self {
graph,
path_map,
compilation_order,
})
Ok((
Self {
graph,
path_map,
compilation_order,
},
removed_deps_vec,
))
}

/// Attempt to load the build plan from the `Forc.lock` file.
pub fn from_lock_file(lock_path: &Path, sway_git_tag: &str) -> Result<Self> {
/// Returns the best effort BuildPlan and the packages removed
/// from project's manifest file after the lock file is generated.
///
/// The returned removed dependencies are already removed from the dependency graph.
pub fn from_lock_file(
lock_path: &Path,
sway_git_tag: &str,
) -> Result<(Self, Vec<DependencyName>)> {
let proj_path = lock_path.parent().unwrap();
let lock = Lock::from_path(lock_path)?;
Self::from_lock(proj_path, &lock, sway_git_tag)
}

/// Ensure that the build plan is valid for the given manifest.
pub fn validate(&self, manifest: &Manifest, sway_git_tag: &str) -> Result<PkgDiff> {
/// Attempt to find the difference between the graph constructed from the lock file
/// and the graph that is described by the manifest file. The difference is used to
/// update BuildPlan.
pub fn generate_diff(&self, manifest: &Manifest) -> Result<PkgDiff> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can make this private too

let mut added = vec![];
let mut removed = vec![];
// Retrieve project's graph node.
Expand Down Expand Up @@ -383,7 +422,7 @@ impl BuildPlan {
if det.version.is_some() {
println_yellow_err(&format!(
" WARNING! Dependency \"{}\" specifies the unused `version` field: \
consider using `branch` or `tag` instead",
consider using `branch` or `tag` instead",
dep_name
));
}
Expand Down Expand Up @@ -411,6 +450,11 @@ impl BuildPlan {
.collect();
}

Ok(PkgDiff { added, removed })
}

/// Ensure that the build plan is valid for the given manifest.
fn validate(&self, sway_git_tag: &str) -> Result<()> {
// Ensure the pkg names of all nodes match their associated manifests.
for node in self.graph.node_indices() {
let pkg = &self.graph[node];
Expand All @@ -425,7 +469,7 @@ impl BuildPlan {
);
}
}
Ok(PkgDiff { added, removed })
Ok(())
}

/// View the build plan's compilation graph.
Expand Down Expand Up @@ -809,20 +853,58 @@ pub fn compilation_order(graph: &Graph) -> Result<Vec<NodeIx>> {
})
}

/// Applies the differences between the graph that is described by the lock file and the graph that
/// is actually possible to build.
///
/// `deps_to_remove` : Indices of path dependencies that are removed from their parent's manifest file after the lock file is generated.
///
/// There might be some differences between the graph that is described by the lock file and
/// the graph that can actually possible to build. This is happening in the case of a dependency
/// (described by path) removal after the lock file is generated. Since dependency is removed
/// graph_to_path_map cannot find the required path for the dependency entry in the lock file.
/// This requires us to remove those removed nodes from the graph so that we can have a BuildPlan
/// generated from lock file. After the BuildPlan is generated it can be used to apply diffs by
/// apply_pkg_diff().
fn apply_diff_after_lock(deps_to_remove: HashSet<NodeIx>, graph: &mut Graph) -> Result<()> {
for dep in deps_to_remove {
let dep_name = &graph[dep].name.clone();
println_red(&format!(" Removing {}", dep_name));
// Remove the deleted node
graph.remove_node(dep);
}
Ok(())
}
kayagokalp marked this conversation as resolved.
Show resolved Hide resolved
/// Search dependencies and patches for the details for a pinned dependency
fn check_manifest_for_pinned(manifest: &ManifestFile, pinned_dep: &Pinned) -> bool {
manifest.dep(&pinned_dep.name).is_none()
&& !manifest
.deps()
.filter_map(|dep| dep.1.package())
.any(|package| package == pinned_dep.name)
&& manifest.patch(&pinned_dep.name).is_none()
&& manifest
.patches()
.find_map(|patches| patches.1.get(&pinned_dep.name))
.is_none()
}

/// Given graph of pinned dependencies and the directory for the root node, produce a path map
/// containing the path to the local source for every node in the graph.
kayagokalp marked this conversation as resolved.
Show resolved Hide resolved
///
/// Returns the generated path map and the nodes that should be removed from the graph (the nodes
/// that are removed from their parent's manifest file).
pub fn graph_to_path_map(
proj_manifest_dir: &Path,
graph: &Graph,
compilation_order: &[NodeIx],
sway_git_tag: &str,
) -> Result<PathMap> {
) -> Result<(PathMap, HashSet<NodeIx>)> {
let mut path_map = PathMap::new();
let mut removed_deps = HashSet::new();

// We resolve all paths in reverse compilation order.
// That is, we follow paths starting from the project root.
let mut path_resolve_order = compilation_order.iter().cloned().rev();

// Add the project's package to the map.
let proj_node = path_resolve_order
.next()
Expand Down Expand Up @@ -869,6 +951,13 @@ pub fn graph_to_path_map(
// Construct the path relative to the parent's path.
let parent_path = &path_map[&parent.id()];
let parent_manifest = ManifestFile::from_dir(parent_path, sway_git_tag)?;
// Check if parent's manifest file includes this dependency.
// If not this is a removed dependency and we should continue with the following dep
// Note that we need to check from package name as well since dependency name and package name is not the same thing
if check_manifest_for_pinned(&parent_manifest, dep) {
removed_deps.insert(dep_node);
continue;
}
let detailed = parent_manifest
.dependencies
.as_ref()
Expand Down Expand Up @@ -916,7 +1005,7 @@ pub fn graph_to_path_map(
path_map.insert(dep.id(), dep_path.canonicalize()?);
}

Ok(path_map)
Ok((path_map, removed_deps))
}

/// Given a `graph`, the node index of a path dependency within that `graph`, and the supposed
Expand Down
2 changes: 1 addition & 1 deletion sway-lsp/src/core/document.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ impl TextDocument {
pkg::ManifestFile::from_dir(&manifest_dir, forc::utils::SWAY_GIT_TAG).unwrap();
let lock_path = forc_util::lock_path(manifest.dir());
let plan = pkg::BuildPlan::from_lock_file(&lock_path, forc::utils::SWAY_GIT_TAG).unwrap();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh I wonder if this BuildPlan should be constructed via load_from_manifest rather than from_lock_file - otherwise LSP won't pick up changes to the manifest until the user manually triggers a build and updates the lock file again. @JoshuaBatty does this sound right?

If we can change this over to load_from_manifest, we should be able to make from_lock_file private.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Depending on the input from @JoshuaBatty maybe we can do something like

let plan =
  pkg::BuildPlan::load_from_manifest(&manifest, false, false, forc::utils::SWAY_GIT_TAG)
      .unwrap_or_else(|_| {
          pkg::BuildPlan::from_lock_file(&lock_path, forc::utils::SWAY_GIT_TAG)
              .map(|plan| plan.0)
              .unwrap()
      });

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think rather than falling back to from_lock_file, we might be better off producing an error from LSP to the user so that they're aware of whatever the failure is. It might be a bit unexpected if LSP keeps using an old BuildPlan after they've updated the manifest.

Copy link
Member

@JoshuaBatty JoshuaBatty Jun 29, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I add a crate to the Cargo.toml file and save it in a rust project then I am able to start using types from that crate and even jump to the definition of items in that crate without needing to build the project again. Not sure if it's rust or rust-analyzer that is downloading the added crate once the manifest file has been saved. It would be nice if we could replicate this behavior somehow.

Copy link
Contributor

@mitchmindtree mitchmindtree Jun 29, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In that case it sounds like we do want to change the use of from_lock_file to load_from_manifest here - @kayagokalp are you happy to do so in this PR so we can make from_lock_file private too?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be good to go but didn't add an explicit error throwing for the failure as we didn't have one for the current version too:

let plan = pkg::BuildPlan::from_lock_file(&lock_path, forc::utils::SWAY_GIT_TAG).unwrap();

Should we change the current behavior and explicitly return an error? or keep the current logic there (which is simply unwrapping)

let res = pkg::check(&plan, silent_mode, forc::utils::SWAY_GIT_TAG).unwrap();
let res = pkg::check(&plan.0, silent_mode, forc::utils::SWAY_GIT_TAG).unwrap();

match res {
CompileAstResult::Failure { .. } => None,
Expand Down