-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
When building the appsignal-javascript mono repo from scratch the build would fail as it depends on packages in the project that are not yet compiled. This was because it tried to compile packages in the alphabetical order, rather than what packages depend on which other packages. This move and expansion of the dependency tree from the PackagePromoter adds a `packages` method that returns the packages in the order of least dependencies first. This should make sure package A is compiled before package B.
- Loading branch information
Showing
8 changed files
with
242 additions
and
51 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
bump: "patch" | ||
--- | ||
|
||
Implement dependency tree, this accounts for package dependencies in compilation order. When package B depends on package A it will first compile package A and then package B. This prevents compilation errors. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
# frozen_string_literal: true | ||
|
||
module Mono | ||
class DependencyTree | ||
def initialize(packages) | ||
@raw_packages = packages | ||
build | ||
end | ||
|
||
# Fetch package dependency tree entry with its dependencies and dependents | ||
# | ||
# @return [Hash<String, Hash<Symbol, Object>] | ||
def [](name) | ||
tree[name] | ||
end | ||
|
||
# Return list of packages in dependency order. The first item has the least | ||
# amount of dependencies on packages in the project (most likely none) and | ||
# the last one the most depdendencies. | ||
# | ||
# @raise [CircularDependencyError] if the project packages have a circular | ||
# depdendency on one another. | ||
# @return [Array] | ||
def packages | ||
return @packages if defined?(@packages) | ||
|
||
pkgs = [] | ||
unsorted_packages = tree.keys | ||
iterations = Hash.new { |hash, key| hash[key] = 0 } | ||
until unsorted_packages.empty? | ||
package_name = unsorted_packages.shift | ||
iterations[package_name] += 1 | ||
package = tree[package_name] | ||
if depdendencies_checked? pkgs, package[:dependencies] | ||
# Reset iterations for remaining packages so we don't claim it to be | ||
# a circular dependency when we're still making progress. | ||
unsorted_packages.each { |pkg| iterations[pkg] = 0 } | ||
|
||
# Dependencies are all ordered, so we can include it in the list next | ||
pkgs << package_name | ||
else | ||
# Dependency not yet ordered, moving to last position to try again | ||
# later when hopefully all dependencies have been ordered | ||
unsorted_packages << package_name | ||
end | ||
|
||
# A package has been looped more than 10 times without resolving any | ||
# packages, most likely a circular dependency was found. | ||
next unless iterations[package_name] > 10 | ||
|
||
raise CircularDependencyError, unsorted_packages | ||
end | ||
@packages = pkgs.map { |name| tree[name][:package] } | ||
end | ||
|
||
private | ||
|
||
attr_reader :raw_packages, :tree | ||
|
||
def depdendencies_checked?(pkgs, dependencies) | ||
dep_checks = dependencies.map { |name, _| pkgs.include? name } | ||
dep_checks.all?(true) | ||
end | ||
|
||
# Build a dependency tree. Track which packages depent on other packages in | ||
# this project. It creates a tree with dependencies going both ways: | ||
# dependencies and dependents. | ||
def build # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity | ||
@tree = | ||
Hash.new do |hash, key| | ||
hash[key] = { | ||
:package => nil, | ||
:dependents => [], # List of packages that depend on this package | ||
:dependencies => [] # List of packages that this package depends on | ||
} | ||
end | ||
|
||
package_names = raw_packages.map(&:name) | ||
# Build a package tree based on dependencies | ||
raw_packages.each do |package| | ||
# Only track packages in this project, not other ecosystem dependencies | ||
next unless package_names.include? package.name | ||
|
||
@tree[package.name][:package] = package | ||
deps = package.dependencies | ||
next unless deps | ||
|
||
package_deps = {} | ||
deps.each do |key, value| | ||
package_deps[key] = value if package_names.include?(key) | ||
end | ||
@tree[package.name][:dependencies] = package_deps | ||
end | ||
# Loop through it again to figure out depenents from the dependencies | ||
raw_packages.each do |package| # rubocop:disable Style/CombinableLoops | ||
@tree[package.name][:dependencies].each do |dep, _version_lock| | ||
# Only track packages in this project, not other ecosystem | ||
# dependencies | ||
next unless package_names.include? dep | ||
|
||
# Keep track of dependent packages | ||
@tree[dep][:dependents] << package.name | ||
end | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
# frozen_string_literal: true | ||
|
||
RSpec.describe Mono::DependencyTree do | ||
describe "#packages" do | ||
let(:config) { Mono::Config.new({}) } # Mock empty config | ||
|
||
it "returns packages in dependency order" do | ||
prepare_new_project do | ||
create_package "types" do | ||
create_package_json :version => "1.0.0" | ||
end | ||
create_package "nodejs-ext" do | ||
create_package_json :version => "1.2.3" | ||
end | ||
create_package "nodejs" do | ||
create_package_json :version => "2.0.0", | ||
:dependencies => { | ||
"nodejs-ext" => "=1.2.3", | ||
"types" => "=1.0.0" | ||
} | ||
end | ||
create_package "apollo" do | ||
create_package_json :version => "2.0.0", | ||
:dependencies => { | ||
"nodejs" => "=2.0.0", | ||
"types" => "=1.0.0" | ||
} | ||
end | ||
create_package "apollo-addon" do | ||
create_package_json :version => "2.0.0", | ||
:dependencies => { | ||
"apollo" => "=2.0.0" | ||
} | ||
end | ||
end | ||
|
||
package_types = nodejs_package("types") | ||
package_ext = nodejs_package("nodejs-ext") | ||
package_node = nodejs_package("nodejs") | ||
package_apollo = nodejs_package("apollo") | ||
package_apollo_addon = nodejs_package("apollo-addon") | ||
promoter = described_class.new([ | ||
package_apollo_addon, | ||
package_apollo, | ||
package_node, | ||
package_ext, | ||
package_types | ||
]) | ||
expect(promoter.packages).to eql([ | ||
package_ext, | ||
package_types, | ||
package_node, | ||
package_apollo, | ||
package_apollo_addon | ||
]) | ||
end | ||
|
||
context "with infinite loop of dependencies" do | ||
it "raises an error if there's a circular dependency loop" do | ||
prepare_new_project do | ||
create_package "a" do | ||
create_package_json :version => "1.0.1", :dependencies => { "c" => "1.0.3" } | ||
end | ||
create_package "b" do | ||
create_package_json :version => "1.0.2", :dependencies => { "a" => "1.0.1" } | ||
end | ||
create_package "c" do | ||
create_package_json :version => "1.0.3", :dependencies => { "b" => "1.0.2" } | ||
end | ||
end | ||
|
||
package_a = nodejs_package("a") | ||
package_b = nodejs_package("b") | ||
package_c = nodejs_package("c") | ||
promoter = described_class.new([package_a, package_b, package_c]) | ||
expect { promoter.packages }.to raise_error(Mono::CircularDependencyError) | ||
end | ||
end | ||
end | ||
|
||
def nodejs_package(path) | ||
Mono::Languages::Nodejs::Package.new(nil, package_path(path), config) | ||
end | ||
end |
Oops, something went wrong.