boxwerk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +8 -0
- data/LICENSE.txt +19 -0
- data/README.md +376 -0
- data/Rakefile +8 -0
- data/example/Gemfile +6 -0
- data/example/Gemfile.lock +66 -0
- data/example/README.md +130 -0
- data/example/app.rb +101 -0
- data/example/package.yml +6 -0
- data/example/packages/finance/lib/invoice.rb +51 -0
- data/example/packages/finance/lib/tax_calculator.rb +26 -0
- data/example/packages/finance/package.yml +10 -0
- data/example/packages/util/lib/calculator.rb +21 -0
- data/example/packages/util/lib/geometry.rb +26 -0
- data/example/packages/util/package.yml +5 -0
- data/exe/boxwerk +28 -0
- data/lib/boxwerk/cli.rb +130 -0
- data/lib/boxwerk/graph.rb +111 -0
- data/lib/boxwerk/loader.rb +277 -0
- data/lib/boxwerk/package.rb +51 -0
- data/lib/boxwerk/registry.rb +37 -0
- data/lib/boxwerk/setup.rb +71 -0
- data/lib/boxwerk/version.rb +5 -0
- data/lib/boxwerk.rb +14 -0
- data/sig/boxwerk.rbs +4 -0
- metadata +87 -0
data/example/app.rb
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Example Boxwerk Application
|
|
4
|
+
# Run with: RUBY_BOX=1 boxwerk app.rb
|
|
5
|
+
|
|
6
|
+
puts '=' * 70
|
|
7
|
+
puts 'Boxwerk Application'
|
|
8
|
+
puts '=' * 70
|
|
9
|
+
puts ''
|
|
10
|
+
|
|
11
|
+
puts 'Creating invoice...'
|
|
12
|
+
invoice = Finance::Invoice.new(tax_rate: 0.15)
|
|
13
|
+
invoice.add_item('Web Development', 200_000) # $2000.00 in cents
|
|
14
|
+
invoice.add_item('Design Work', 150_000) # $1500.00 in cents
|
|
15
|
+
invoice.add_item('Consulting', 80_000) # $800.00 in cents
|
|
16
|
+
|
|
17
|
+
data = invoice.to_h
|
|
18
|
+
|
|
19
|
+
puts "\nInvoice Details:"
|
|
20
|
+
data[:items].each_with_index do |item, i|
|
|
21
|
+
amount_dollars = item[:amount] / 100.0
|
|
22
|
+
puts " #{i + 1}. #{item[:description]}: $#{format('%.2f', amount_dollars)}"
|
|
23
|
+
end
|
|
24
|
+
puts ''
|
|
25
|
+
puts " Subtotal: $#{format('%.2f', data[:subtotal] / 100.0)}"
|
|
26
|
+
puts " Tax (15%): $#{format('%.2f', data[:tax] / 100.0)}"
|
|
27
|
+
puts " Total: $#{format('%.2f', data[:total] / 100.0)}"
|
|
28
|
+
puts ''
|
|
29
|
+
|
|
30
|
+
puts 'Testing isolation...'
|
|
31
|
+
# Finance::Invoice should be available
|
|
32
|
+
begin
|
|
33
|
+
test_invoice = Finance::Invoice.new
|
|
34
|
+
puts ' ✓ Finance::Invoice accessible'
|
|
35
|
+
rescue NameError => e
|
|
36
|
+
puts " ✗ Finance::Invoice not accessible: #{e.message}"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Finance::TaxCalculator should be available
|
|
40
|
+
begin
|
|
41
|
+
Finance::TaxCalculator
|
|
42
|
+
puts ' ✓ Finance::TaxCalculator accessible'
|
|
43
|
+
rescue NameError => e
|
|
44
|
+
puts " ✗ Finance::TaxCalculator not accessible: #{e.message}"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# UtilCalculator should NOT be available (transitive dependency)
|
|
48
|
+
begin
|
|
49
|
+
UtilCalculator.add(1, 2)
|
|
50
|
+
puts ' ✗ ERROR: UtilCalculator leaked from transitive dependency!'
|
|
51
|
+
rescue NameError
|
|
52
|
+
puts ' ✓ UtilCalculator not accessible (correct isolation)'
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Invoice should NOT be at top level (only in Finance namespace)
|
|
56
|
+
begin
|
|
57
|
+
Invoice.new
|
|
58
|
+
puts ' ✗ ERROR: Invoice available at top level!'
|
|
59
|
+
rescue NameError
|
|
60
|
+
puts ' ✓ Invoice only accessible via Finance namespace'
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Calculator should NOT be available (transitive dependency from util package)
|
|
64
|
+
begin
|
|
65
|
+
Calculator.add(1, 2)
|
|
66
|
+
puts ' ✗ ERROR: Calculator leaked from transitive dependency!'
|
|
67
|
+
rescue NameError
|
|
68
|
+
puts ' ✓ Calculator not accessible (correct isolation)'
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Geometry should NOT be available (transitive dependency from util package)
|
|
72
|
+
begin
|
|
73
|
+
Geometry.circle_area(5)
|
|
74
|
+
puts ' ✗ ERROR: Geometry leaked from transitive dependency!'
|
|
75
|
+
rescue NameError
|
|
76
|
+
puts ' ✓ Geometry not accessible (correct isolation)'
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Money gem SHOULD be accessible (gems are global, not isolated)
|
|
80
|
+
begin
|
|
81
|
+
test_money = Money.new(100, 'USD')
|
|
82
|
+
puts ' ✓ Money gem accessible (gems are global)'
|
|
83
|
+
rescue NameError => e
|
|
84
|
+
puts " ✗ ERROR: Money gem not accessible: #{e.message}"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
puts ''
|
|
88
|
+
puts '=' * 70
|
|
89
|
+
puts '✓ Application completed successfully'
|
|
90
|
+
puts '=' * 70
|
|
91
|
+
puts ''
|
|
92
|
+
puts 'Boxwerk CLI setup process:'
|
|
93
|
+
puts ' 1. `boxwerk run app.rb` found root package.yml'
|
|
94
|
+
puts ' 2. Built dependency graph (util → finance → root)'
|
|
95
|
+
puts ' 3. Validated no circular dependencies'
|
|
96
|
+
puts ' 4. Booted packages in topological order (all in isolated boxes)'
|
|
97
|
+
puts ' 5. Executed app.rb in root package box with Finance imported'
|
|
98
|
+
puts ''
|
|
99
|
+
puts 'Key difference: ALL packages (including root) run in isolated boxes.'
|
|
100
|
+
puts 'The main Ruby process only contains gems and the Boxwerk runtime.'
|
|
101
|
+
puts ''
|
data/example/package.yml
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Invoice represents a financial invoice with line items and tax calculation
|
|
4
|
+
# Uses the Money gem for precise currency handling
|
|
5
|
+
class Invoice
|
|
6
|
+
attr_reader :items, :tax_rate
|
|
7
|
+
|
|
8
|
+
def initialize(tax_rate: TaxCalculator::STANDARD_RATE)
|
|
9
|
+
@items = []
|
|
10
|
+
@tax_rate = tax_rate
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def add_item(description, amount_cents)
|
|
14
|
+
@items << {
|
|
15
|
+
description: description,
|
|
16
|
+
amount: Money.new(amount_cents, 'USD'),
|
|
17
|
+
}
|
|
18
|
+
self
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def subtotal
|
|
22
|
+
@items.reduce(Money.new(0, 'USD')) { |sum, item| sum + item[:amount] }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def tax
|
|
26
|
+
(subtotal * tax_rate).round
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def total
|
|
30
|
+
subtotal + tax
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def to_h
|
|
34
|
+
{
|
|
35
|
+
items:
|
|
36
|
+
@items.map do |item|
|
|
37
|
+
{ description: item[:description], amount: item[:amount].cents }
|
|
38
|
+
end,
|
|
39
|
+
subtotal: subtotal.cents,
|
|
40
|
+
tax_rate: tax_rate,
|
|
41
|
+
tax: tax.cents,
|
|
42
|
+
total: total.cents,
|
|
43
|
+
}
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def self.quick_invoice(amount_cents, tax_rate: TaxCalculator::STANDARD_RATE)
|
|
47
|
+
invoice = new(tax_rate: tax_rate)
|
|
48
|
+
invoice.add_item('Service', amount_cents)
|
|
49
|
+
invoice
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# TaxCalculator provides tax calculation utilities
|
|
4
|
+
# Uses the imported Util package for calculations
|
|
5
|
+
class TaxCalculator
|
|
6
|
+
STANDARD_RATE = 0.10 # 10%
|
|
7
|
+
LUXURY_RATE = 0.20 # 20%
|
|
8
|
+
|
|
9
|
+
def self.calculate_tax(amount, rate = STANDARD_RATE)
|
|
10
|
+
# Use UtilCalculator from selective import
|
|
11
|
+
UtilCalculator.multiply(amount, rate)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.calculate_total(amount, rate = STANDARD_RATE)
|
|
15
|
+
tax = calculate_tax(amount, rate)
|
|
16
|
+
# Use UtilCalculator.add from selective import
|
|
17
|
+
UtilCalculator.add(amount, tax)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.reverse_calculate(total_with_tax, rate = STANDARD_RATE)
|
|
21
|
+
# Calculate original amount from total including tax
|
|
22
|
+
# Formula: original = total / (1 + rate)
|
|
23
|
+
divisor = UtilCalculator.add(1, rate)
|
|
24
|
+
UtilCalculator.divide(total_with_tax, divisor)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Calculator provides basic arithmetic operations
|
|
4
|
+
class Calculator
|
|
5
|
+
def self.add(a, b)
|
|
6
|
+
a + b
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def self.subtract(a, b)
|
|
10
|
+
a - b
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.multiply(a, b)
|
|
14
|
+
a * b
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.divide(a, b)
|
|
18
|
+
raise ArgumentError, 'Cannot divide by zero' if b.zero?
|
|
19
|
+
a.to_f / b
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Geometry provides geometric calculations
|
|
4
|
+
class Geometry
|
|
5
|
+
PI = 3.14159265359
|
|
6
|
+
|
|
7
|
+
def self.circle_area(radius)
|
|
8
|
+
PI * radius * radius
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.circle_circumference(radius)
|
|
12
|
+
2 * PI * radius
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.rectangle_area(width, height)
|
|
16
|
+
width * height
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.rectangle_perimeter(width, height)
|
|
20
|
+
2 * (width + height)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.triangle_area(base, height)
|
|
24
|
+
(base * height) / 2.0
|
|
25
|
+
end
|
|
26
|
+
end
|
data/exe/boxwerk
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Boxwerk CLI
|
|
5
|
+
# Usage: boxwerk <command> [args...]
|
|
6
|
+
|
|
7
|
+
# Check if Ruby::Box is available and enabled
|
|
8
|
+
unless ENV['RUBY_BOX'] == '1'
|
|
9
|
+
puts "Error: Boxwerk requires Ruby::Box to be enabled"
|
|
10
|
+
puts "Please set RUBY_BOX=1 environment variable"
|
|
11
|
+
exit 1
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Verify Ruby::Box is actually available in this Ruby version
|
|
15
|
+
unless defined?(Ruby::Box)
|
|
16
|
+
puts "Error: Ruby::Box is not available in this Ruby version"
|
|
17
|
+
puts "Boxwerk requires Ruby 4.0 or later with Box support"
|
|
18
|
+
exit 1
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Setup Bundler, require any gems and run Boxwerk - all in the root box
|
|
22
|
+
Ruby::Box.root.eval(<<~RUBY)
|
|
23
|
+
require 'bundler/setup'
|
|
24
|
+
# Application gems need to be required in the root box to be accessible in package boxes
|
|
25
|
+
Bundler.require
|
|
26
|
+
# Run the CLI to handle the invocation
|
|
27
|
+
Boxwerk::CLI.run(ARGV)
|
|
28
|
+
RUBY
|
data/lib/boxwerk/cli.rb
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Boxwerk
|
|
4
|
+
# CLI handles command-line execution of Boxwerk applications
|
|
5
|
+
module CLI
|
|
6
|
+
class << self
|
|
7
|
+
# Main entry point for CLI
|
|
8
|
+
# @param argv [Array<String>] Command line arguments
|
|
9
|
+
def run(argv)
|
|
10
|
+
if argv.empty?
|
|
11
|
+
print_usage
|
|
12
|
+
exit 1
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
command = argv[0]
|
|
16
|
+
|
|
17
|
+
case command
|
|
18
|
+
when 'run'
|
|
19
|
+
run_command(argv[1..-1])
|
|
20
|
+
when 'console'
|
|
21
|
+
console_command(argv[1..-1])
|
|
22
|
+
when 'help', '--help', '-h'
|
|
23
|
+
print_usage
|
|
24
|
+
exit 0
|
|
25
|
+
else
|
|
26
|
+
puts "Error: Unknown command '#{command}'"
|
|
27
|
+
puts ''
|
|
28
|
+
print_usage
|
|
29
|
+
exit 1
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def print_usage
|
|
36
|
+
puts 'Boxwerk - Ruby package system with Box-powered constant isolation'
|
|
37
|
+
puts ''
|
|
38
|
+
puts 'Usage: boxwerk <command> [args...]'
|
|
39
|
+
puts ''
|
|
40
|
+
puts 'Commands:'
|
|
41
|
+
puts ' run <script.rb> [args...] Run a script in the root package context'
|
|
42
|
+
puts ' console [irb-args...] Start an IRB console in the root package context'
|
|
43
|
+
puts ' help Show this help message'
|
|
44
|
+
puts ''
|
|
45
|
+
puts 'All packages are loaded and wired before the command executes.'
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def run_command(args)
|
|
49
|
+
if args.empty?
|
|
50
|
+
puts 'Error: No script specified'
|
|
51
|
+
puts ''
|
|
52
|
+
puts 'Usage: boxwerk run <script.rb> [args...]'
|
|
53
|
+
exit 1
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
script_path = args[0]
|
|
57
|
+
script_args = args[1..-1] || []
|
|
58
|
+
|
|
59
|
+
# Verify script exists
|
|
60
|
+
unless File.exist?(script_path)
|
|
61
|
+
puts "Error: Script not found: #{script_path}"
|
|
62
|
+
exit 1
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Perform Boxwerk setup (find package.yml, build graph, boot packages)
|
|
66
|
+
graph = perform_setup
|
|
67
|
+
|
|
68
|
+
# Execute the script in the root package's box
|
|
69
|
+
root_package = graph.root
|
|
70
|
+
execute_in_box(root_package.box, script_path, script_args)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def console_command(args)
|
|
74
|
+
# Require IRB while we're still in root box context
|
|
75
|
+
require 'irb'
|
|
76
|
+
|
|
77
|
+
# Perform Boxwerk setup
|
|
78
|
+
graph = perform_setup
|
|
79
|
+
|
|
80
|
+
# Start IRB in the root package's box with provided args
|
|
81
|
+
root_package = graph.root
|
|
82
|
+
start_console_in_box(root_package.box, args)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def perform_setup
|
|
86
|
+
begin
|
|
87
|
+
Boxwerk::Setup.run!(start_dir: Dir.pwd)
|
|
88
|
+
rescue => e
|
|
89
|
+
puts "Error: #{e.message}"
|
|
90
|
+
exit 1
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def execute_in_box(box, script_path, script_args)
|
|
95
|
+
# Set ARGV for the script using eval
|
|
96
|
+
box.eval("ARGV.replace(#{script_args.inspect})")
|
|
97
|
+
|
|
98
|
+
# Run the script in the isolated box
|
|
99
|
+
absolute_script_path = File.expand_path(script_path)
|
|
100
|
+
box.require(absolute_script_path)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def start_console_in_box(box, irb_args = [])
|
|
104
|
+
puts '=' * 70
|
|
105
|
+
puts 'Boxwerk Console'
|
|
106
|
+
puts '=' * 70
|
|
107
|
+
puts ''
|
|
108
|
+
puts 'All packages have been loaded and wired.'
|
|
109
|
+
puts 'You are in the root package context.'
|
|
110
|
+
puts ''
|
|
111
|
+
puts 'Type "exit" or press Ctrl+D to quit.'
|
|
112
|
+
puts '=' * 70
|
|
113
|
+
puts ''
|
|
114
|
+
|
|
115
|
+
# Start IRB in the box context.
|
|
116
|
+
# TODO: This is currently broken. IRB runs the in the root box context.
|
|
117
|
+
# This should be fixed by calling `require 'irb'` inside the box, but
|
|
118
|
+
# that currently crashes the VM.
|
|
119
|
+
# Set ARGV to the provided IRB args so they can be processed by IRB.
|
|
120
|
+
# Always add --noautocomplete to disable autocomplete (currently broken with Ruby::Box)
|
|
121
|
+
# TODO: Enable autocomplete when it's not broken.
|
|
122
|
+
irb_args_with_noautocomplete = ['--noautocomplete'] + irb_args
|
|
123
|
+
box.eval(<<~RUBY)
|
|
124
|
+
ARGV.replace(#{irb_args_with_noautocomplete.inspect})
|
|
125
|
+
IRB.start
|
|
126
|
+
RUBY
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Boxwerk
|
|
4
|
+
# Builds and validates the package dependency graph
|
|
5
|
+
class Graph
|
|
6
|
+
attr_reader :packages, :root
|
|
7
|
+
|
|
8
|
+
def initialize(root_path)
|
|
9
|
+
@root_path = root_path
|
|
10
|
+
@packages = {}
|
|
11
|
+
@root = load_package('root', root_path)
|
|
12
|
+
resolve_dependencies(@root, [])
|
|
13
|
+
validate!
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Returns packages in topological order (leaves first)
|
|
17
|
+
def topological_order
|
|
18
|
+
visited = {}
|
|
19
|
+
order = []
|
|
20
|
+
|
|
21
|
+
@packages.each_value { |package| visit(package, visited, order, []) }
|
|
22
|
+
|
|
23
|
+
order
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
# Validate that the graph is acyclic
|
|
29
|
+
def validate!
|
|
30
|
+
visited = {}
|
|
31
|
+
rec_stack = {}
|
|
32
|
+
|
|
33
|
+
@packages.each_value do |package|
|
|
34
|
+
if has_cycle?(package, visited, rec_stack, [])
|
|
35
|
+
raise 'Circular dependency detected in package graph'
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
true
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def load_package(name, path)
|
|
43
|
+
return @packages[name] if @packages[name]
|
|
44
|
+
|
|
45
|
+
package = Package.new(name, path)
|
|
46
|
+
@packages[name] = package
|
|
47
|
+
package
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def resolve_dependencies(package, path)
|
|
51
|
+
if path.include?(package.name)
|
|
52
|
+
cycle = (path + [package.name]).join(' -> ')
|
|
53
|
+
raise "Circular dependency detected: #{cycle}"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
package.dependencies.each do |dep_path|
|
|
57
|
+
dep_name = File.basename(dep_path)
|
|
58
|
+
full_path = File.join(@root_path, dep_path)
|
|
59
|
+
|
|
60
|
+
unless File.directory?(full_path)
|
|
61
|
+
raise "Package not found: #{dep_path} (expected at #{full_path})"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
dep_package = load_package(dep_name, full_path)
|
|
65
|
+
resolve_dependencies(dep_package, path + [package.name])
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# DFS for topological sort
|
|
70
|
+
def visit(package, visited, order, path)
|
|
71
|
+
return if visited[package.name]
|
|
72
|
+
|
|
73
|
+
if path.include?(package.name)
|
|
74
|
+
raise "Circular dependency: #{(path + [package.name]).join(' -> ')}"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
visited[package.name] = true
|
|
78
|
+
|
|
79
|
+
package.dependencies.each do |dep_path|
|
|
80
|
+
dep_name = File.basename(dep_path)
|
|
81
|
+
dep_package = @packages[dep_name]
|
|
82
|
+
visit(dep_package, visited, order, path + [package.name]) if dep_package
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
order << package
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Cycle detection
|
|
89
|
+
def has_cycle?(package, visited, rec_stack, path)
|
|
90
|
+
return false if visited[package.name]
|
|
91
|
+
|
|
92
|
+
return true if rec_stack[package.name]
|
|
93
|
+
|
|
94
|
+
visited[package.name] = true
|
|
95
|
+
rec_stack[package.name] = true
|
|
96
|
+
|
|
97
|
+
package.dependencies.each do |dep_path|
|
|
98
|
+
dep_name = File.basename(dep_path)
|
|
99
|
+
dep_package = @packages[dep_name]
|
|
100
|
+
|
|
101
|
+
if dep_package &&
|
|
102
|
+
has_cycle?(dep_package, visited, rec_stack, path + [package.name])
|
|
103
|
+
return true
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
rec_stack[package.name] = false
|
|
108
|
+
false
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|