ruby_workspace_manager 0.2.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/LICENSE.txt +21 -0
- data/README.md +32 -0
- data/bin/rwm +6 -0
- data/lib/rwm/affected_detector.rb +102 -0
- data/lib/rwm/cli.rb +98 -0
- data/lib/rwm/commands/affected.rb +52 -0
- data/lib/rwm/commands/bootstrap.rb +147 -0
- data/lib/rwm/commands/check.rb +27 -0
- data/lib/rwm/commands/graph.rb +46 -0
- data/lib/rwm/commands/info.rb +41 -0
- data/lib/rwm/commands/init.rb +113 -0
- data/lib/rwm/commands/list.rb +45 -0
- data/lib/rwm/commands/new.rb +150 -0
- data/lib/rwm/commands/run.rb +132 -0
- data/lib/rwm/convention_checker.rb +70 -0
- data/lib/rwm/dependency_graph.rb +226 -0
- data/lib/rwm/errors.rb +43 -0
- data/lib/rwm/gemfile.rb +32 -0
- data/lib/rwm/gemfile_parser.rb +48 -0
- data/lib/rwm/git_hooks.rb +55 -0
- data/lib/rwm/overcommit.rb +99 -0
- data/lib/rwm/package.rb +62 -0
- data/lib/rwm/rake.rb +51 -0
- data/lib/rwm/task_cache.rb +114 -0
- data/lib/rwm/task_runner.rb +148 -0
- data/lib/rwm/version.rb +5 -0
- data/lib/rwm/vscode_workspace.rb +57 -0
- data/lib/rwm/workspace.rb +73 -0
- data/lib/rwm.rb +19 -0
- metadata +72 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "optparse"
|
|
5
|
+
|
|
6
|
+
module Rwm
|
|
7
|
+
module Commands
|
|
8
|
+
class Init
|
|
9
|
+
GEMFILE_TEMPLATE = <<~GEMFILE
|
|
10
|
+
# frozen_string_literal: true
|
|
11
|
+
|
|
12
|
+
source "https://rubygems.org"
|
|
13
|
+
|
|
14
|
+
gem "ruby_workspace_manager"
|
|
15
|
+
GEMFILE
|
|
16
|
+
|
|
17
|
+
RAKEFILE_TEMPLATE = <<~RAKEFILE
|
|
18
|
+
# frozen_string_literal: true
|
|
19
|
+
|
|
20
|
+
task :bootstrap do
|
|
21
|
+
puts "Add your workspace-level bootstrap steps here."
|
|
22
|
+
puts "This task runs during `rwm bootstrap` — use it for binstubs, shared tooling, etc."
|
|
23
|
+
end
|
|
24
|
+
RAKEFILE
|
|
25
|
+
|
|
26
|
+
def initialize(argv)
|
|
27
|
+
@argv = argv
|
|
28
|
+
@vscode = false
|
|
29
|
+
parse_options
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def run
|
|
33
|
+
root = Dir.pwd
|
|
34
|
+
|
|
35
|
+
create_directories(root)
|
|
36
|
+
create_gemfile(root)
|
|
37
|
+
create_rakefile(root)
|
|
38
|
+
update_gitignore(root)
|
|
39
|
+
|
|
40
|
+
if @vscode
|
|
41
|
+
generate_vscode_workspace(root)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
puts "Workspace initialized. Running bootstrap..."
|
|
45
|
+
puts
|
|
46
|
+
|
|
47
|
+
# Call bootstrap as the last step
|
|
48
|
+
require "rwm/commands/bootstrap"
|
|
49
|
+
Commands::Bootstrap.new([]).run
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def parse_options
|
|
55
|
+
OptionParser.new do |opts|
|
|
56
|
+
opts.on("--vscode", "Generate VSCode .code-workspace file") do
|
|
57
|
+
@vscode = true
|
|
58
|
+
end
|
|
59
|
+
end.parse!(@argv)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def create_directories(root)
|
|
63
|
+
%w[libs apps].each do |dir|
|
|
64
|
+
path = File.join(root, dir)
|
|
65
|
+
unless File.directory?(path)
|
|
66
|
+
FileUtils.mkdir_p(path)
|
|
67
|
+
puts "Created #{dir}/"
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def create_gemfile(root)
|
|
73
|
+
path = File.join(root, "Gemfile")
|
|
74
|
+
return if File.exist?(path)
|
|
75
|
+
|
|
76
|
+
File.write(path, GEMFILE_TEMPLATE)
|
|
77
|
+
puts "Created Gemfile"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def create_rakefile(root)
|
|
81
|
+
path = File.join(root, "Rakefile")
|
|
82
|
+
return if File.exist?(path)
|
|
83
|
+
|
|
84
|
+
File.write(path, RAKEFILE_TEMPLATE)
|
|
85
|
+
puts "Created Rakefile"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def generate_vscode_workspace(root)
|
|
89
|
+
VscodeWorkspace.new(root).generate([])
|
|
90
|
+
puts "Created #{File.basename(root)}.code-workspace"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def update_gitignore(root)
|
|
94
|
+
path = File.join(root, ".gitignore")
|
|
95
|
+
entry = ".rwm/"
|
|
96
|
+
|
|
97
|
+
if File.exist?(path)
|
|
98
|
+
content = File.read(path)
|
|
99
|
+
return if content.lines.any? { |line| line.strip == entry }
|
|
100
|
+
|
|
101
|
+
File.open(path, "a") do |f|
|
|
102
|
+
f.puts unless content.end_with?("\n")
|
|
103
|
+
f.puts entry
|
|
104
|
+
end
|
|
105
|
+
puts "Added #{entry} to .gitignore"
|
|
106
|
+
else
|
|
107
|
+
File.write(path, "#{entry}\n")
|
|
108
|
+
puts "Created .gitignore"
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rwm
|
|
4
|
+
module Commands
|
|
5
|
+
class List
|
|
6
|
+
def initialize(argv)
|
|
7
|
+
@argv = argv
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def run
|
|
11
|
+
workspace = Workspace.find
|
|
12
|
+
packages = workspace.packages
|
|
13
|
+
|
|
14
|
+
if packages.empty?
|
|
15
|
+
puts "No packages found."
|
|
16
|
+
return 0
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
graph = DependencyGraph.load(workspace)
|
|
20
|
+
|
|
21
|
+
# Calculate column widths
|
|
22
|
+
name_width = [packages.map { |p| p.name.length }.max, 4].max
|
|
23
|
+
type_width = 4
|
|
24
|
+
path_width = [packages.map { |p| p.relative_path(workspace.root).length }.max, 4].max
|
|
25
|
+
|
|
26
|
+
# Header
|
|
27
|
+
puts format("%-#{name_width}s %-#{type_width}s %-#{path_width}s %s",
|
|
28
|
+
"Name", "Type", "Path", "Dependencies")
|
|
29
|
+
puts format("%-#{name_width}s %-#{type_width}s %-#{path_width}s %s",
|
|
30
|
+
"-" * name_width, "-" * type_width, "-" * path_width, "-" * 12)
|
|
31
|
+
|
|
32
|
+
# Rows
|
|
33
|
+
packages.each do |pkg|
|
|
34
|
+
deps = graph.dependencies(pkg.name)
|
|
35
|
+
dep_str = deps.empty? ? "(none)" : deps.sort.join(", ")
|
|
36
|
+
|
|
37
|
+
puts format("%-#{name_width}s %-#{type_width}s %-#{path_width}s %s",
|
|
38
|
+
pkg.name, pkg.type, pkg.relative_path(workspace.root), dep_str)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
0
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module Rwm
|
|
6
|
+
module Commands
|
|
7
|
+
class New
|
|
8
|
+
def initialize(argv)
|
|
9
|
+
@argv = argv
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def run
|
|
13
|
+
type = @argv.shift
|
|
14
|
+
name = @argv.shift
|
|
15
|
+
|
|
16
|
+
unless %w[app lib].include?(type)
|
|
17
|
+
$stderr.puts "Usage: rwm new <app|lib> <name>"
|
|
18
|
+
return 1
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
unless name && name.match?(/\A[a-z][a-z0-9_]*\z/)
|
|
22
|
+
$stderr.puts "Package name must start with a lowercase letter and contain only lowercase letters, digits, and underscores."
|
|
23
|
+
return 1
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
workspace = Workspace.find
|
|
27
|
+
dir = type == "lib" ? "libs" : "apps"
|
|
28
|
+
pkg_path = File.join(workspace.root, dir, name)
|
|
29
|
+
|
|
30
|
+
raise PackageExistsError, name if File.directory?(pkg_path)
|
|
31
|
+
|
|
32
|
+
scaffold(pkg_path, name, type)
|
|
33
|
+
update_vscode_workspace(workspace)
|
|
34
|
+
|
|
35
|
+
puts "Created #{type} '#{name}' at #{dir}/#{name}/"
|
|
36
|
+
puts
|
|
37
|
+
puts "Next steps:"
|
|
38
|
+
puts " cd #{dir}/#{name}"
|
|
39
|
+
puts " bundle install"
|
|
40
|
+
puts " rwm graph # rebuild the dependency graph"
|
|
41
|
+
0
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def update_vscode_workspace(workspace)
|
|
47
|
+
vscode = VscodeWorkspace.new(workspace.root)
|
|
48
|
+
return unless File.exist?(vscode.file_path)
|
|
49
|
+
|
|
50
|
+
# Use a fresh Workspace to pick up the newly scaffolded package
|
|
51
|
+
fresh_workspace = Workspace.find(workspace.root)
|
|
52
|
+
VscodeWorkspace.new(fresh_workspace.root).generate(fresh_workspace.packages)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def scaffold(pkg_path, name, type)
|
|
56
|
+
FileUtils.mkdir_p(File.join(pkg_path, "lib", name))
|
|
57
|
+
FileUtils.mkdir_p(File.join(pkg_path, "spec"))
|
|
58
|
+
|
|
59
|
+
write_gemfile(pkg_path, name)
|
|
60
|
+
write_gemspec(pkg_path, name, type)
|
|
61
|
+
write_rakefile(pkg_path, name)
|
|
62
|
+
write_lib_entry(pkg_path, name)
|
|
63
|
+
write_spec_helper(pkg_path)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def write_gemfile(pkg_path, name)
|
|
67
|
+
File.write(File.join(pkg_path, "Gemfile"), <<~GEMFILE)
|
|
68
|
+
# frozen_string_literal: true
|
|
69
|
+
|
|
70
|
+
source "https://rubygems.org"
|
|
71
|
+
|
|
72
|
+
gemspec
|
|
73
|
+
|
|
74
|
+
group :development, :test do
|
|
75
|
+
gem "rake"
|
|
76
|
+
gem "rspec"
|
|
77
|
+
gem "ruby_workspace_manager"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
require "rwm/gemfile"
|
|
81
|
+
# rwm_lib "some_dependency"
|
|
82
|
+
GEMFILE
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def write_gemspec(pkg_path, name, type)
|
|
86
|
+
lines = [
|
|
87
|
+
'# frozen_string_literal: true',
|
|
88
|
+
'',
|
|
89
|
+
'Gem::Specification.new do |spec|',
|
|
90
|
+
" spec.name = \"#{name}\"",
|
|
91
|
+
' spec.version = "0.1.0"',
|
|
92
|
+
' spec.authors = ["TODO: Your name"]',
|
|
93
|
+
" spec.summary = \"TODO: Summary of #{name}\"",
|
|
94
|
+
'',
|
|
95
|
+
]
|
|
96
|
+
lines << ' spec.files = Dir.glob("lib/**/*")' if type == "lib"
|
|
97
|
+
lines.concat([
|
|
98
|
+
' spec.require_paths = ["lib"]',
|
|
99
|
+
' spec.required_ruby_version = ">= 3.4.0"',
|
|
100
|
+
'end',
|
|
101
|
+
'',
|
|
102
|
+
])
|
|
103
|
+
File.write(File.join(pkg_path, "#{name}.gemspec"), lines.join("\n"))
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def write_rakefile(pkg_path, name)
|
|
107
|
+
File.write(File.join(pkg_path, "Rakefile"), <<~RAKEFILE)
|
|
108
|
+
# frozen_string_literal: true
|
|
109
|
+
|
|
110
|
+
require "rwm/rake"
|
|
111
|
+
|
|
112
|
+
cacheable_task :spec do
|
|
113
|
+
sh "bundle exec rspec"
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
task :bootstrap do
|
|
117
|
+
puts "Add bootstrap steps for #{name} here."
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
task default: :spec
|
|
121
|
+
RAKEFILE
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def write_lib_entry(pkg_path, name)
|
|
125
|
+
File.write(File.join(pkg_path, "lib", "#{name}.rb"), <<~RUBY)
|
|
126
|
+
# frozen_string_literal: true
|
|
127
|
+
|
|
128
|
+
module #{camelize(name)}
|
|
129
|
+
end
|
|
130
|
+
RUBY
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def write_spec_helper(pkg_path)
|
|
134
|
+
File.write(File.join(pkg_path, "spec", "spec_helper.rb"), <<~RUBY)
|
|
135
|
+
# frozen_string_literal: true
|
|
136
|
+
|
|
137
|
+
RSpec.configure do |config|
|
|
138
|
+
config.expect_with :rspec do |expectations|
|
|
139
|
+
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
RUBY
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def camelize(name)
|
|
146
|
+
name.split("_").map(&:capitalize).join
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
|
|
5
|
+
module Rwm
|
|
6
|
+
module Commands
|
|
7
|
+
class Run
|
|
8
|
+
def initialize(argv)
|
|
9
|
+
@argv = argv
|
|
10
|
+
@affected_only = false
|
|
11
|
+
@committed_only = false
|
|
12
|
+
@no_cache = false
|
|
13
|
+
@buffered = false
|
|
14
|
+
@concurrency = nil
|
|
15
|
+
parse_options
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def run
|
|
19
|
+
task = @argv.shift
|
|
20
|
+
|
|
21
|
+
unless task
|
|
22
|
+
$stderr.puts "Usage: rwm run <task> [<package>] [--affected] [--no-cache] [--buffered] [--concurrency N]"
|
|
23
|
+
return 1
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
package_name = @argv.shift
|
|
27
|
+
|
|
28
|
+
workspace = Workspace.find
|
|
29
|
+
graph = DependencyGraph.load(workspace)
|
|
30
|
+
|
|
31
|
+
packages = if package_name
|
|
32
|
+
pkg = workspace.find_package(package_name)
|
|
33
|
+
unless pkg
|
|
34
|
+
$stderr.puts "Unknown package: #{package_name}"
|
|
35
|
+
return 1
|
|
36
|
+
end
|
|
37
|
+
[pkg]
|
|
38
|
+
elsif @affected_only
|
|
39
|
+
detector = AffectedDetector.new(workspace, graph, committed_only: @committed_only)
|
|
40
|
+
affected = detector.affected_packages
|
|
41
|
+
if affected.empty?
|
|
42
|
+
puts "No affected packages. Nothing to run."
|
|
43
|
+
return 0
|
|
44
|
+
end
|
|
45
|
+
puts "Running on #{affected.size} affected package(s)..."
|
|
46
|
+
affected
|
|
47
|
+
else
|
|
48
|
+
workspace.packages
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
if packages.empty?
|
|
52
|
+
puts "No packages found."
|
|
53
|
+
return 0
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Filter to packages that have the requested rake task
|
|
57
|
+
runnable = packages.select { |pkg| pkg.has_rake_task?(task) }
|
|
58
|
+
if runnable.empty?
|
|
59
|
+
puts "No packages with a `#{task}` rake task found."
|
|
60
|
+
return 0
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Auto-detect cacheable tasks unless --no-cache
|
|
64
|
+
cache = TaskCache.new(workspace, graph) unless @no_cache
|
|
65
|
+
if cache
|
|
66
|
+
cacheable, not_cacheable = runnable.partition { |pkg| cache.cacheable?(pkg, task) }
|
|
67
|
+
cached, uncached = cacheable.partition { |pkg| cache.cached?(pkg, task) }
|
|
68
|
+
cached.each { |pkg| puts "[#{pkg.name}] cached" }
|
|
69
|
+
runnable = uncached + not_cacheable
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
if runnable.empty?
|
|
73
|
+
puts "All packages cached. Nothing to run."
|
|
74
|
+
return 0
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
puts "Running `rake #{task}` across #{runnable.size} package(s)..."
|
|
78
|
+
puts
|
|
79
|
+
|
|
80
|
+
runner_opts = { packages: runnable, buffered: @buffered }
|
|
81
|
+
runner_opts[:concurrency] = @concurrency if @concurrency
|
|
82
|
+
runner = TaskRunner.new(graph, **runner_opts)
|
|
83
|
+
runner.run_task(task)
|
|
84
|
+
|
|
85
|
+
# Store cache for successful cacheable packages
|
|
86
|
+
if cache
|
|
87
|
+
runner.results.each do |result|
|
|
88
|
+
next unless result.success
|
|
89
|
+
|
|
90
|
+
pkg = workspace.find_package(result.package_name)
|
|
91
|
+
cache.store(pkg, task) if cache.cacheable?(pkg, task)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
puts
|
|
96
|
+
if runner.success?
|
|
97
|
+
puts "All packages passed."
|
|
98
|
+
0
|
|
99
|
+
else
|
|
100
|
+
failed = runner.failed_results
|
|
101
|
+
$stderr.puts "#{failed.size} package(s) failed:"
|
|
102
|
+
failed.each { |r| $stderr.puts " - #{r.package_name}" }
|
|
103
|
+
1
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
private
|
|
108
|
+
|
|
109
|
+
def parse_options
|
|
110
|
+
parser = OptionParser.new do |opts|
|
|
111
|
+
opts.on("--affected", "Only run on affected packages") do
|
|
112
|
+
@affected_only = true
|
|
113
|
+
end
|
|
114
|
+
opts.on("--committed", "Only consider committed changes (with --affected)") do
|
|
115
|
+
@committed_only = true
|
|
116
|
+
end
|
|
117
|
+
opts.on("--no-cache", "Bypass task caching even for cacheable tasks") do
|
|
118
|
+
@no_cache = true
|
|
119
|
+
end
|
|
120
|
+
opts.on("--buffered", "Buffer output per-package and print on completion") do
|
|
121
|
+
@buffered = true
|
|
122
|
+
end
|
|
123
|
+
opts.on("--concurrency N", Integer, "Max parallel workers (default: processor count)") do |n|
|
|
124
|
+
@concurrency = n
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
parser.order!(@argv)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rwm
|
|
4
|
+
class ConventionChecker
|
|
5
|
+
def initialize(graph)
|
|
6
|
+
@graph = graph
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def check!
|
|
10
|
+
violations = []
|
|
11
|
+
violations.concat(check_no_lib_depends_on_app)
|
|
12
|
+
violations.concat(check_no_app_depends_on_app)
|
|
13
|
+
violations.concat(check_no_cycles)
|
|
14
|
+
|
|
15
|
+
raise ConventionError, violations unless violations.empty?
|
|
16
|
+
|
|
17
|
+
true
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def check
|
|
21
|
+
check!
|
|
22
|
+
[]
|
|
23
|
+
rescue ConventionError => e
|
|
24
|
+
e.violations
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def check_no_lib_depends_on_app
|
|
30
|
+
violations = []
|
|
31
|
+
|
|
32
|
+
@graph.packages.each do |name, pkg|
|
|
33
|
+
next unless pkg.lib?
|
|
34
|
+
|
|
35
|
+
@graph.dependencies(name).each do |dep_name|
|
|
36
|
+
dep = @graph.packages[dep_name]
|
|
37
|
+
next unless dep&.app?
|
|
38
|
+
|
|
39
|
+
violations << "lib '#{name}' depends on app '#{dep_name}' — libs cannot depend on apps"
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
violations
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def check_no_app_depends_on_app
|
|
47
|
+
violations = []
|
|
48
|
+
|
|
49
|
+
@graph.packages.each do |name, pkg|
|
|
50
|
+
next unless pkg.app?
|
|
51
|
+
|
|
52
|
+
@graph.dependencies(name).each do |dep_name|
|
|
53
|
+
dep = @graph.packages[dep_name]
|
|
54
|
+
next unless dep&.app?
|
|
55
|
+
|
|
56
|
+
violations << "app '#{name}' depends on app '#{dep_name}' — apps cannot depend on other apps"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
violations
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def check_no_cycles
|
|
64
|
+
@graph.topological_order
|
|
65
|
+
[]
|
|
66
|
+
rescue CycleError => e
|
|
67
|
+
e.cycles.map { |c| "Dependency cycle: #{c}" }
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|