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,226 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "tsort"
|
|
4
|
+
require "json"
|
|
5
|
+
require "fileutils"
|
|
6
|
+
|
|
7
|
+
module Rwm
|
|
8
|
+
class DependencyGraph
|
|
9
|
+
include TSort
|
|
10
|
+
|
|
11
|
+
attr_reader :packages, :edges
|
|
12
|
+
|
|
13
|
+
def initialize
|
|
14
|
+
@packages = {} # name => Package
|
|
15
|
+
@edges = {} # name => [dep_name, ...]
|
|
16
|
+
@dependents = {} # name => [dependent_name, ...]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def add_package(package)
|
|
20
|
+
@packages[package.name] = package
|
|
21
|
+
@edges[package.name] ||= []
|
|
22
|
+
@dependents[package.name] ||= []
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def add_edge(from_name, to_name)
|
|
26
|
+
@edges[from_name] ||= []
|
|
27
|
+
@edges[from_name] << to_name unless @edges[from_name].include?(to_name)
|
|
28
|
+
@dependents[to_name] ||= []
|
|
29
|
+
@dependents[to_name] << from_name unless @dependents[to_name].include?(from_name)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Dependencies of a package (what it depends on)
|
|
33
|
+
def dependencies(name)
|
|
34
|
+
@edges[name] || []
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Direct dependents of a package (what depends on it)
|
|
38
|
+
def direct_dependents(name)
|
|
39
|
+
@dependents[name] || []
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Walk the graph to find all transitive dependents
|
|
43
|
+
def transitive_dependents(name)
|
|
44
|
+
visited = Set.new
|
|
45
|
+
queue = [name]
|
|
46
|
+
|
|
47
|
+
until queue.empty?
|
|
48
|
+
current = queue.shift
|
|
49
|
+
direct_dependents(current).each do |dep|
|
|
50
|
+
next if visited.include?(dep)
|
|
51
|
+
|
|
52
|
+
visited << dep
|
|
53
|
+
queue << dep
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
visited.to_a
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Topological sort (dependencies before dependents)
|
|
61
|
+
def topological_order
|
|
62
|
+
tsort
|
|
63
|
+
rescue TSort::Cyclic => e
|
|
64
|
+
raise CycleError, [e.message]
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Group packages into execution levels — packages at the same level
|
|
68
|
+
# have no interdependencies and can run in parallel
|
|
69
|
+
def execution_levels
|
|
70
|
+
return [] if @packages.empty?
|
|
71
|
+
|
|
72
|
+
remaining = @packages.keys.dup
|
|
73
|
+
levels = []
|
|
74
|
+
|
|
75
|
+
until remaining.empty?
|
|
76
|
+
# Find packages whose deps are all already placed in earlier levels
|
|
77
|
+
placed = levels.flatten
|
|
78
|
+
level = remaining.select do |name|
|
|
79
|
+
dependencies(name).all? { |dep| placed.include?(dep) }
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
raise CycleError, ["Unable to resolve execution levels — possible cycle"] if level.empty?
|
|
83
|
+
|
|
84
|
+
levels << level.sort
|
|
85
|
+
remaining -= level
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
levels
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Load graph from cached .rwm/graph.json, falling back to build.
|
|
92
|
+
# Auto-rebuilds when any package Gemfile is newer than the cache.
|
|
93
|
+
def self.load(workspace)
|
|
94
|
+
path = workspace.graph_path
|
|
95
|
+
unless File.exist?(path)
|
|
96
|
+
return build_and_save(workspace)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
if stale?(path, workspace.packages)
|
|
100
|
+
return build_and_save(workspace)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
data = JSON.parse(File.read(path))
|
|
104
|
+
graph = new
|
|
105
|
+
|
|
106
|
+
workspace.packages.each { |pkg| graph.add_package(pkg) }
|
|
107
|
+
|
|
108
|
+
data["edges"]&.each do |name, deps|
|
|
109
|
+
deps.each { |dep| graph.add_edge(name, dep) }
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
graph
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def self.build_and_save(workspace)
|
|
116
|
+
graph = build(workspace)
|
|
117
|
+
graph.save(workspace.graph_path, workspace.root)
|
|
118
|
+
graph
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def self.stale?(graph_path, packages)
|
|
122
|
+
graph_mtime = File.mtime(graph_path)
|
|
123
|
+
packages.any? { |pkg| File.mtime(pkg.gemfile_path) > graph_mtime }
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
private_class_method :stale?, :build_and_save
|
|
127
|
+
|
|
128
|
+
# Build graph from a workspace by parsing all Gemfiles
|
|
129
|
+
def self.build(workspace)
|
|
130
|
+
graph = new
|
|
131
|
+
|
|
132
|
+
workspace.packages.each { |pkg| graph.add_package(pkg) }
|
|
133
|
+
|
|
134
|
+
workspace.packages.each do |pkg|
|
|
135
|
+
deps = GemfileParser.parse(pkg.gemfile_path, workspace.packages)
|
|
136
|
+
deps.each { |dep| graph.add_edge(pkg.name, dep.name) }
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
graph
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Serialize to JSON for .rwm/graph.json
|
|
143
|
+
def to_json_data
|
|
144
|
+
{
|
|
145
|
+
"version" => 1,
|
|
146
|
+
"generated_at" => Time.now.iso8601,
|
|
147
|
+
"packages" => @packages.transform_values do |pkg|
|
|
148
|
+
{ "name" => pkg.name, "type" => pkg.type.to_s, "path" => pkg.relative_path(@workspace_root || "") }
|
|
149
|
+
end,
|
|
150
|
+
"edges" => @edges.transform_values(&:sort)
|
|
151
|
+
}
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def save(path, workspace_root)
|
|
155
|
+
@workspace_root = workspace_root
|
|
156
|
+
dir = File.dirname(path)
|
|
157
|
+
FileUtils.mkdir_p(dir)
|
|
158
|
+
File.write(path, JSON.pretty_generate(to_json_data) + "\n")
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def self.load_from_file(path)
|
|
162
|
+
data = JSON.parse(File.read(path))
|
|
163
|
+
graph = new
|
|
164
|
+
|
|
165
|
+
# We can't reconstruct full Package objects from JSON alone,
|
|
166
|
+
# but we can load the structure for validation
|
|
167
|
+
data["edges"]&.each do |name, deps|
|
|
168
|
+
graph.instance_variable_get(:@edges)[name] = deps
|
|
169
|
+
deps.each do |dep|
|
|
170
|
+
graph.instance_variable_get(:@dependents)[dep] ||= []
|
|
171
|
+
graph.instance_variable_get(:@dependents)[dep] << name
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
graph
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def to_dot(workspace_root)
|
|
179
|
+
lines = []
|
|
180
|
+
lines << "digraph rwm {"
|
|
181
|
+
lines << " rankdir=LR;"
|
|
182
|
+
lines << " node [shape=box];"
|
|
183
|
+
|
|
184
|
+
@packages.each_value do |pkg|
|
|
185
|
+
lines << " \"#{pkg.name}\" [label=\"#{pkg.name} (#{pkg.type})\"];"
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
@edges.each do |from, deps|
|
|
189
|
+
deps.each do |to|
|
|
190
|
+
lines << " \"#{from}\" -> \"#{to}\";"
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
lines << "}"
|
|
195
|
+
lines.join("\n") + "\n"
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def to_mermaid(workspace_root)
|
|
199
|
+
lines = []
|
|
200
|
+
lines << "graph LR"
|
|
201
|
+
|
|
202
|
+
@packages.each_value do |pkg|
|
|
203
|
+
lines << " #{pkg.name}[\"#{pkg.name} (#{pkg.type})\"]"
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
@edges.each do |from, deps|
|
|
207
|
+
deps.each do |to|
|
|
208
|
+
lines << " #{from} --> #{to}"
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
lines.join("\n") + "\n"
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
private
|
|
216
|
+
|
|
217
|
+
# TSort interface
|
|
218
|
+
def tsort_each_node(&block)
|
|
219
|
+
@packages.each_key(&block)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def tsort_each_child(node, &block)
|
|
223
|
+
(@edges[node] || []).each(&block)
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
end
|
data/lib/rwm/errors.rb
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rwm
|
|
4
|
+
class Error < StandardError; end
|
|
5
|
+
|
|
6
|
+
class WorkspaceNotFoundError < Error
|
|
7
|
+
def initialize
|
|
8
|
+
super("Not a git repository. rwm uses the git root as the workspace root. Run `git init` first, then `rwm init`.")
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
class CycleError < Error
|
|
13
|
+
attr_reader :cycles
|
|
14
|
+
|
|
15
|
+
def initialize(cycles)
|
|
16
|
+
@cycles = cycles
|
|
17
|
+
super("Dependency cycle detected: #{cycles.map { |c| c.join(" → ") }.join(", ")}")
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
class ConventionError < Error
|
|
22
|
+
attr_reader :violations
|
|
23
|
+
|
|
24
|
+
def initialize(violations)
|
|
25
|
+
@violations = violations
|
|
26
|
+
super("Convention violations:\n#{violations.map { |v| " - #{v}" }.join("\n")}")
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
class PackageNotFoundError < Error
|
|
31
|
+
def initialize(name)
|
|
32
|
+
super("Package not found: #{name}")
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
class PackageExistsError < Error
|
|
37
|
+
def initialize(name)
|
|
38
|
+
super("Package already exists: #{name}")
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
class BootstrapError < Error; end
|
|
43
|
+
end
|
data/lib/rwm/gemfile.rb
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Bundler DSL extension for rwm workspaces.
|
|
4
|
+
#
|
|
5
|
+
# Usage in a package Gemfile:
|
|
6
|
+
#
|
|
7
|
+
# require "rwm/gemfile"
|
|
8
|
+
#
|
|
9
|
+
# source "https://rubygems.org"
|
|
10
|
+
# gemspec
|
|
11
|
+
#
|
|
12
|
+
# rwm_lib "auth"
|
|
13
|
+
|
|
14
|
+
require "bundler"
|
|
15
|
+
|
|
16
|
+
module Rwm
|
|
17
|
+
module GemfileDsl
|
|
18
|
+
def rwm_workspace_root
|
|
19
|
+
@rwm_workspace_root ||= `git rev-parse --show-toplevel 2>/dev/null`.strip.tap do |root|
|
|
20
|
+
raise "rwm: not inside a git repository" if root.empty?
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def rwm_lib(name, **opts)
|
|
25
|
+
path = File.join(rwm_workspace_root, "libs", name.to_s)
|
|
26
|
+
gem(name.to_s, **opts, path: path)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
Bundler::Dsl.prepend(Rwm::GemfileDsl)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler"
|
|
4
|
+
|
|
5
|
+
module Rwm
|
|
6
|
+
class GemfileParser
|
|
7
|
+
# Parse a Gemfile and extract path dependencies that match known packages
|
|
8
|
+
def self.parse(gemfile_path, known_packages)
|
|
9
|
+
new(gemfile_path, known_packages).parse
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def initialize(gemfile_path, known_packages)
|
|
13
|
+
@gemfile_path = gemfile_path
|
|
14
|
+
@known_packages = known_packages
|
|
15
|
+
@package_by_path = index_packages_by_path
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def parse
|
|
19
|
+
dsl = Bundler::Dsl.new
|
|
20
|
+
dsl.eval_gemfile(@gemfile_path)
|
|
21
|
+
deps = dsl.dependencies
|
|
22
|
+
|
|
23
|
+
gemfile_dir = File.expand_path(File.dirname(@gemfile_path))
|
|
24
|
+
|
|
25
|
+
deps.each_with_object([]) do |dep, result|
|
|
26
|
+
source = dep.source
|
|
27
|
+
next unless source.is_a?(Bundler::Source::Path)
|
|
28
|
+
|
|
29
|
+
# Resolve the path relative to the Gemfile's directory
|
|
30
|
+
dep_path = File.expand_path(source.path.to_s, gemfile_dir)
|
|
31
|
+
|
|
32
|
+
# Skip self-references (gemspec directive points to own directory)
|
|
33
|
+
next if dep_path == gemfile_dir
|
|
34
|
+
|
|
35
|
+
matched = @package_by_path[dep_path]
|
|
36
|
+
result << matched if matched
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def index_packages_by_path
|
|
43
|
+
@known_packages.each_with_object({}) do |pkg, index|
|
|
44
|
+
index[pkg.path] = pkg
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module Rwm
|
|
6
|
+
class GitHooks
|
|
7
|
+
PRE_PUSH_HOOK = <<~BASH
|
|
8
|
+
#!/bin/bash
|
|
9
|
+
bundle exec rwm check
|
|
10
|
+
BASH
|
|
11
|
+
|
|
12
|
+
POST_COMMIT_HOOK = <<~BASH
|
|
13
|
+
#!/bin/bash
|
|
14
|
+
# Only rebuild graph if a Gemfile changed in this commit
|
|
15
|
+
if git diff-tree --no-commit-id --name-only -r HEAD 2>/dev/null | grep -q 'Gemfile'; then
|
|
16
|
+
bundle exec rwm graph
|
|
17
|
+
fi
|
|
18
|
+
BASH
|
|
19
|
+
|
|
20
|
+
def initialize(workspace_root)
|
|
21
|
+
@root = workspace_root
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def setup
|
|
25
|
+
hooks_dir = File.join(@root, ".git", "hooks")
|
|
26
|
+
return false unless File.directory?(File.join(@root, ".git"))
|
|
27
|
+
|
|
28
|
+
FileUtils.mkdir_p(hooks_dir)
|
|
29
|
+
install_hook(hooks_dir, "pre-push", PRE_PUSH_HOOK)
|
|
30
|
+
install_hook(hooks_dir, "post-commit", POST_COMMIT_HOOK)
|
|
31
|
+
true
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def install_hook(hooks_dir, name, content)
|
|
37
|
+
path = File.join(hooks_dir, name)
|
|
38
|
+
|
|
39
|
+
if File.exist?(path)
|
|
40
|
+
existing = File.read(path)
|
|
41
|
+
return if existing.include?("bundle exec rwm")
|
|
42
|
+
# Append to existing hook
|
|
43
|
+
File.open(path, "a") do |f|
|
|
44
|
+
f.puts
|
|
45
|
+
f.puts "# rwm hooks"
|
|
46
|
+
f.puts content.lines.drop(1).join # skip shebang
|
|
47
|
+
end
|
|
48
|
+
else
|
|
49
|
+
File.write(path, content)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
File.chmod(0o755, path)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
|
|
6
|
+
module Rwm
|
|
7
|
+
class Overcommit
|
|
8
|
+
RWM_HOOKS = {
|
|
9
|
+
"PrePush" => {
|
|
10
|
+
"CustomScript" => {
|
|
11
|
+
"enabled" => true
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"PostCommit" => {
|
|
15
|
+
"CustomScript" => {
|
|
16
|
+
"enabled" => true
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
PRE_PUSH_SCRIPT = <<~BASH
|
|
22
|
+
#!/bin/bash
|
|
23
|
+
bundle exec rwm check
|
|
24
|
+
BASH
|
|
25
|
+
|
|
26
|
+
POST_COMMIT_SCRIPT = <<~BASH
|
|
27
|
+
#!/bin/bash
|
|
28
|
+
# Only rebuild graph if a Gemfile changed in this commit
|
|
29
|
+
if git diff-tree --no-commit-id --name-only -r HEAD 2>/dev/null | grep -q 'Gemfile'; then
|
|
30
|
+
bundle exec rwm graph
|
|
31
|
+
fi
|
|
32
|
+
BASH
|
|
33
|
+
|
|
34
|
+
def initialize(workspace_root)
|
|
35
|
+
@root = workspace_root
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Sets up overcommit: installs hooks, configures .overcommit.yml,
|
|
39
|
+
# creates hook scripts, and signs the config.
|
|
40
|
+
# Returns true if overcommit was installed successfully.
|
|
41
|
+
def setup
|
|
42
|
+
configure_hooks
|
|
43
|
+
create_hook_scripts
|
|
44
|
+
installed = install_hooks
|
|
45
|
+
sign_config if installed
|
|
46
|
+
installed
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def install_hooks
|
|
52
|
+
system("bundle", "exec", "overcommit", "--install", chdir: @root,
|
|
53
|
+
out: File::NULL, err: File::NULL)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def sign_config
|
|
57
|
+
system("bundle", "exec", "overcommit", "--sign", chdir: @root,
|
|
58
|
+
out: File::NULL, err: File::NULL)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def configure_hooks
|
|
62
|
+
config_path = File.join(@root, ".overcommit.yml")
|
|
63
|
+
|
|
64
|
+
existing = if File.exist?(config_path)
|
|
65
|
+
YAML.safe_load(File.read(config_path)) || {}
|
|
66
|
+
else
|
|
67
|
+
{}
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
merged = deep_merge(existing, RWM_HOOKS)
|
|
71
|
+
File.write(config_path, YAML.dump(merged))
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def create_hook_scripts
|
|
75
|
+
create_script("pre_push", "rwm_check", PRE_PUSH_SCRIPT)
|
|
76
|
+
create_script("post_commit", "rwm_graph", POST_COMMIT_SCRIPT)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def create_script(hook_dir, name, content)
|
|
80
|
+
dir = File.join(@root, ".git-hooks", hook_dir)
|
|
81
|
+
FileUtils.mkdir_p(dir)
|
|
82
|
+
path = File.join(dir, name)
|
|
83
|
+
File.write(path, content)
|
|
84
|
+
File.chmod(0o755, path)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def deep_merge(base, override)
|
|
88
|
+
result = base.dup
|
|
89
|
+
override.each do |key, value|
|
|
90
|
+
if result[key].is_a?(Hash) && value.is_a?(Hash)
|
|
91
|
+
result[key] = deep_merge(result[key], value)
|
|
92
|
+
else
|
|
93
|
+
result[key] = value
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
result
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
data/lib/rwm/package.rb
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
require "open3"
|
|
5
|
+
|
|
6
|
+
module Rwm
|
|
7
|
+
class Package
|
|
8
|
+
attr_reader :name, :path, :type
|
|
9
|
+
|
|
10
|
+
def initialize(name:, path:, type:)
|
|
11
|
+
@name = name
|
|
12
|
+
@path = File.expand_path(path)
|
|
13
|
+
@type = type.to_sym
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def lib?
|
|
17
|
+
type == :lib
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def app?
|
|
21
|
+
type == :app
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def has_rakefile?
|
|
25
|
+
File.exist?(File.join(path, "Rakefile"))
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def has_rake_task?(task)
|
|
29
|
+
return false unless has_rakefile?
|
|
30
|
+
|
|
31
|
+
output, _, status = Open3.capture3("bundle", "exec", "rake", "-P", chdir: path)
|
|
32
|
+
return false unless status.success?
|
|
33
|
+
|
|
34
|
+
output.lines.any? { |line| line.strip == "rake #{task}" }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def gemfile_path
|
|
38
|
+
File.join(path, "Gemfile")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def gemspec_path
|
|
42
|
+
Dir.glob(File.join(path, "*.gemspec")).first
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def relative_path(workspace_root)
|
|
46
|
+
Pathname.new(path).relative_path_from(Pathname.new(workspace_root)).to_s
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def to_s
|
|
50
|
+
"#{name} (#{type})"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def ==(other)
|
|
54
|
+
other.is_a?(Package) && name == other.name && path == other.path
|
|
55
|
+
end
|
|
56
|
+
alias eql? ==
|
|
57
|
+
|
|
58
|
+
def hash
|
|
59
|
+
[name, path].hash
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
data/lib/rwm/rake.rb
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Rake DSL extension for rwm workspaces.
|
|
4
|
+
#
|
|
5
|
+
# Usage in a package Rakefile:
|
|
6
|
+
#
|
|
7
|
+
# require "rwm/rake"
|
|
8
|
+
#
|
|
9
|
+
# cacheable_task :spec do
|
|
10
|
+
# sh "bundle exec rspec"
|
|
11
|
+
# end
|
|
12
|
+
#
|
|
13
|
+
# cacheable_task :build, output: "pkg/*.gem" do
|
|
14
|
+
# sh "gem build *.gemspec"
|
|
15
|
+
# end
|
|
16
|
+
|
|
17
|
+
require "rake"
|
|
18
|
+
require "json"
|
|
19
|
+
|
|
20
|
+
module Rwm
|
|
21
|
+
module RakeCache
|
|
22
|
+
@declarations = {}
|
|
23
|
+
|
|
24
|
+
class << self
|
|
25
|
+
attr_reader :declarations
|
|
26
|
+
|
|
27
|
+
def register(task_name, output:)
|
|
28
|
+
@declarations[task_name.to_s] = { "output" => output }
|
|
29
|
+
ensure_cache_config_task
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def reset!
|
|
33
|
+
@declarations = {}
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def ensure_cache_config_task
|
|
37
|
+
return if Rake::Task.task_defined?("rwm:cache_config")
|
|
38
|
+
|
|
39
|
+
Rake::Task.define_task("rwm:cache_config") do
|
|
40
|
+
puts JSON.generate(Rwm::RakeCache.declarations)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Top-level DSL method available in Rakefiles
|
|
48
|
+
def cacheable_task(name, output: nil, &block)
|
|
49
|
+
Rwm::RakeCache.register(name, output: output)
|
|
50
|
+
Rake::Task.define_task(name, &block)
|
|
51
|
+
end
|