squared 0.0.1

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.
@@ -0,0 +1,427 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Squared
4
+ module Repo
5
+ class Workspace
6
+ include Common
7
+ include Format
8
+ include System
9
+ include Task
10
+ include ::Rake::DSL
11
+
12
+ TASK_NAME = {
13
+ build: [],
14
+ copy: [],
15
+ depend: [],
16
+ outdated: [],
17
+ refresh: [],
18
+ doc: [],
19
+ clean: []
20
+ }
21
+ TASK_BASE = {}
22
+ private_constant :TASK_NAME, :TASK_BASE
23
+
24
+ class << self
25
+ def implement(*objs)
26
+ objs.each do |obj|
27
+ next unless obj < Project::Base
28
+
29
+ project_kind.unshift(obj)
30
+ obj.tasks&.each do |val|
31
+ TASK_NAME[val] = []
32
+ TASK_BASE[val] = obj
33
+ end
34
+ end
35
+ end
36
+
37
+ def find(path: nil, ref: nil)
38
+ if ref.is_a?(::Symbol) || ref.is_a?(::String)
39
+ ret = project_kind.find { |proj| proj.to_sym == ref.to_sym }
40
+ return ret if ret
41
+ end
42
+ project_kind.find { |proj| proj.is_a?(path) } if path
43
+ end
44
+
45
+ attr_reader :project_kind
46
+ end
47
+
48
+ @project_kind = []
49
+
50
+ attr_reader :main, :root, :home, :series
51
+ attr_accessor :script, :manifest, :manifest_url, :exception, :styles, :verbose
52
+
53
+ def initialize(main)
54
+ prompt = lambda do |path|
55
+ return false unless path.directory?
56
+
57
+ confirm "#{log_title(:warn)} \"#{sub_style(path, :bold)}\" is not empty. Continue with installation? [y/N] "
58
+ end
59
+ @main = main.to_s
60
+ @home = if (val = env('REPO_HOME'))
61
+ home = Pathname.new(val)
62
+ if @main == home.basename.to_s
63
+ @root = home.parent
64
+ if home.exist?
65
+ @root = nil unless home.directory?
66
+ elsif !@root.exist?
67
+ @root.mkpath
68
+ elsif !empty?(@root)
69
+ if prompt.(@root)
70
+ @override = true
71
+ else
72
+ @root = nil
73
+ end
74
+ end
75
+ end
76
+ raise ArgumentError, message('REPO_HOME', val, hint: 'invalid') unless @root
77
+
78
+ home.realdirpath
79
+ elsif (val = env('REPO_ROOT'))
80
+ @root = Pathname.new(val).realdirpath
81
+ if !@root.exist?
82
+ @root.mkpath
83
+ elsif !empty?(@root)
84
+ raise ArgumentError, message('REPO_ROOT', val, hint: 'exist') unless prompt.(@root)
85
+
86
+ @override = true
87
+ end
88
+ @root.join(@main).realdirpath
89
+ else
90
+ empty?(pwd = Pathname.pwd) ? pwd.join(@main) : pwd
91
+ end
92
+ @root ||= @home.parent
93
+ @series = TASK_NAME.dup
94
+ @styles = {}
95
+ @project = {}
96
+ @exception = !env('PIPE_FAIL', ignore: '0').nil?
97
+ @pipe = !env('PIPE_OUT', ignore: '0').nil?
98
+ @verbose = true
99
+ end
100
+
101
+ def repo(url, manifest = 'latest')
102
+ @manifest_url = url
103
+ @manifest = manifest
104
+ self
105
+ end
106
+
107
+ def run(script, **kwargs)
108
+ @script = case (val = env('REPO_BUILD'))
109
+ when 'verbose'
110
+ "#{script}:verbose"
111
+ when 'silent'
112
+ @verbose = false
113
+ script
114
+ else
115
+ val || script
116
+ end
117
+ @dev = bool_match(env('REPO_DEV'), kwargs[:dev])
118
+ @prod = bool_match(env('REPO_PROD'), kwargs[:prod])
119
+ self
120
+ end
121
+
122
+ def add(name, dir = nil, **kwargs)
123
+ path = (dir || name).to_s
124
+ ref = kwargs[:ref]
125
+ project = if !ref.is_a?(::Class)
126
+ Workspace.find(path: root_path(path), ref: ref)
127
+ elsif ref < Project::Base
128
+ ref
129
+ end
130
+ instance = (project || Project::Base).new(name, path, self, **kwargs)
131
+ @project[n = name.to_sym] = instance
132
+ get(:project)[n] = instance
133
+ self
134
+ end
135
+
136
+ def build(default: nil, parallel: [])
137
+ group = {}
138
+ parent = {}
139
+ incl = Set.new
140
+ @project.each do |name, proj|
141
+ next unless proj.enabled?
142
+
143
+ @series.each do |key, items|
144
+ target = "#{name}:#{key}"
145
+ case key
146
+ when :build, :refresh, :depend, :outdated, :doc, :copy, :clean
147
+ next unless proj.has?(key) || ::Rake::Task.task_defined?(target)
148
+ else
149
+ next unless task_defined?(proj, key)
150
+ end
151
+ if proj.group
152
+ incl << proj.group
153
+ (group[:"#{key}:#{proj.group}"] ||= []).push(target)
154
+ else
155
+ items << target
156
+ end
157
+ next unless (base = find_base(proj))
158
+
159
+ id = base.to_sym.to_s
160
+ next if id == proj.group
161
+
162
+ incl << id
163
+ (parent[:"#{key}:#{id}"] ||= []).push(target)
164
+ end
165
+ proj.populate
166
+ end
167
+ @series.merge!(parent) if incl.size > 1
168
+ @series.merge!(group)
169
+ if @manifest_url && (empty?(@root) || @override)
170
+ branch = env('REPO_MANIFEST') || read_manifest
171
+ target = branch || @manifest
172
+ stage = nil
173
+ failfast = true
174
+ cmd = []
175
+ newline = ARGV.index { |val| val.start_with?('repo:') }.to_i > 0
176
+ parse_opts = lambda do |args|
177
+ collect_args(args, :opts).each do |val|
178
+ case val
179
+ when 'no-fail'
180
+ failfast = false
181
+ when 'force'
182
+ cmd << '--force-checkout'
183
+ when 'rebase'
184
+ cmd << '--rebase'
185
+ when 'detach'
186
+ cmd << '--detach'
187
+ when 'gc'
188
+ cmd << '--auto-gc'
189
+ when 'no-update'
190
+ cmd << '--no-manifest-update'
191
+ end
192
+ end
193
+ end
194
+ status = lambda do |val, alt = nil|
195
+ ver = branch || alt
196
+ ver ? message('repo', val.sub('{0}', 'opts*=force,rebase,detach,gc,no-update,no-fail'), ver) : 'inactive'
197
+ end
198
+
199
+ namespace :repo do |repo|
200
+ desc status.('all[{0}]')
201
+ task :all, [:opts] do |_, args|
202
+ parse_opts.(args)
203
+ stage ||= :all
204
+ repo[:sync].invoke
205
+ # rubocop:disable Style/CombinableLoops
206
+ @project.each_value { |proj| proj.depend if proj.enabled? }
207
+ @project.each_value do |proj|
208
+ next unless proj.enabled? && proj.build?
209
+
210
+ proj.has?(:dev) ? proj.refresh : proj.build
211
+ end
212
+ # rubocop:enable Style/CombinableLoops
213
+ end
214
+
215
+ desc status.("init[manifest?=#{target},{0}]", target)
216
+ task :init, [:manifest, :opts] do |_, args|
217
+ parse_opts.(args)
218
+ stage = :init
219
+ puts if newline
220
+ system("repo init -u #{@manifest_url} -m #{args.manifest || target}.xml", chdir: @root)
221
+ repo[:all].invoke
222
+ end
223
+
224
+ desc status.('sync[{0}]')
225
+ task :sync, [:opts] do |_, args|
226
+ raise LoadError, message('repo is not initialized', 'rake repo:init') unless branch || stage == :init
227
+
228
+ parse_opts.(args)
229
+ cmd << "-j#{ENV.fetch('REPO_JOBS', ::Rake::CpuCounter.count)}"
230
+ cmd << '--fail-fast' if failfast
231
+ puts if newline && stage != :init
232
+ begin
233
+ shell("repo sync #{cmd.join(' ')}", chdir: @root, exception: failfast)
234
+ rescue StandardError => e
235
+ emphasize(e, title: "rake stash repo:#{stage || :sync}")
236
+ raise
237
+ end
238
+ end
239
+ end
240
+ end
241
+ return unless enabled?
242
+
243
+ multi = env('REPO_SYNC', ignore: '0') ? [] : parallel.map(&:to_sym)
244
+ output = dev? ? :refresh : :build
245
+
246
+ task default: default || output
247
+
248
+ desc 'all[git?=rebase|stash]'
249
+ task :all, [:git] do |_, args|
250
+ sync = ->(key) { multi.include?(key) ? :"#{key}:sync" : key }
251
+ pull = case args.git
252
+ when 'rebase'
253
+ sync.(:rebase)
254
+ when 'stash'
255
+ invoke sync.(:stash)
256
+ sync.(:pull)
257
+ else
258
+ sync.(:pull)
259
+ end
260
+ invoke(pull, exception: @exception)
261
+ invoke(output, exception: @exception)
262
+ end
263
+
264
+ desc 'init'
265
+ task init: [:depend, output]
266
+
267
+ @series.each do |key, items|
268
+ next if items.empty?
269
+
270
+ unless (valid = multi.include?(key))
271
+ group = key.to_s
272
+ valid = multi.include?(group.split(':').first.to_sym) if group.include?(':')
273
+ end
274
+ if valid
275
+ desc "#{key} (thread)"
276
+ multitask key => items
277
+
278
+ desc "#{key} (sync)"
279
+ task "#{key}:sync": items
280
+ else
281
+ desc key.to_s
282
+ task key => items
283
+ end
284
+ end
285
+ end
286
+
287
+ def compose(name, &block)
288
+ namespace(name, &block)
289
+ self
290
+ end
291
+
292
+ def apply(&block)
293
+ instance_eval(&block)
294
+ self
295
+ end
296
+
297
+ def style(name, *args)
298
+ set = ->(k, v) { @styles[k.to_sym] = check_style(v, empty: false) }
299
+ if name.is_a?(Hash)
300
+ name.each { |k, v| set.(k, v || args.flatten) }
301
+ else
302
+ set.(name, args.flatten)
303
+ end
304
+ self
305
+ end
306
+
307
+ def env(key, equals: nil, ignore: nil)
308
+ ret = ENV.fetch("#{key}_#{@main.gsub(/[^\w]/, '_').upcase}", ENV[key]).to_s
309
+ return ret == equals.to_s if equals
310
+
311
+ ret.empty? || as_a(ignore).any? { |val| ret == val.to_s } ? nil : ret
312
+ end
313
+
314
+ def read_manifest
315
+ require 'rexml/document'
316
+ file = root_path('.repo/manifest.xml')
317
+ return unless file.exist?
318
+
319
+ doc = REXML::Document.new(File.read(file))
320
+ doc.elements['manifest/include'].attributes['name']&.sub('.xml', '')
321
+ end
322
+
323
+ def root_path(*args)
324
+ @root.join(*args)
325
+ end
326
+
327
+ def home_path(*args)
328
+ @home.join(*args)
329
+ end
330
+
331
+ def find_base(obj)
332
+ Workspace.project_kind.find { |proj| obj.instance_of?(proj) }
333
+ end
334
+
335
+ def bool_match(val, pat)
336
+ case val
337
+ when nil, ''
338
+ pat.is_a?(::Regexp) ? pat : nil
339
+ when '0'
340
+ false
341
+ when '1'
342
+ true
343
+ else
344
+ Regexp.new(val)
345
+ end
346
+ end
347
+
348
+ def empty?(dir)
349
+ return true if dir.empty? || dir.join('.repo').directory?
350
+ return true if dir.children.size == 1 && dir.join(dir.children.first).to_s == __FILE__
351
+
352
+ dir == @root && !env('REPO_ROOT').nil? && @root.children.none? do |path|
353
+ path.directory? && !path.basename.to_s.start_with?('.') && path.to_s != @home.to_s
354
+ end
355
+ end
356
+
357
+ def enabled?
358
+ !@series[:build].empty? || !@series[:doc].empty?
359
+ end
360
+
361
+ def multitask?
362
+ ::Rake::Task.tasks.any? do |item|
363
+ next unless item.already_invoked
364
+
365
+ item.name.end_with?(':sync') || (!item.name.include?(':') && !item.name.start_with?('all', 'init'))
366
+ end
367
+ end
368
+
369
+ def task_defined?(obj, key)
370
+ obj.is_a?(TASK_BASE[key])
371
+ end
372
+
373
+ def dev?(pat = nil, script: nil)
374
+ with?(pat, script: script, state: @dev)
375
+ end
376
+
377
+ def prod?(pat = nil, script: nil)
378
+ return false if @dev == true || !@prod
379
+
380
+ with?(pat, script: script, state: @prod)
381
+ end
382
+
383
+ def pipe?
384
+ @pipe
385
+ end
386
+
387
+ protected
388
+
389
+ def confirm(msg, agree: 'Y', cancel: 'N', attempts: 5, timeout: 15)
390
+ require 'readline'
391
+ require 'timeout'
392
+ Timeout.timeout(ENV.fetch('REPO_TIMEOUT', timeout).to_i) do
393
+ ret = false
394
+ begin
395
+ while (ch = Readline.readline(msg, true))
396
+ case ch.chomp.upcase
397
+ when agree
398
+ ret = true
399
+ break
400
+ when cancel
401
+ break
402
+ end
403
+ attempts -= 1
404
+ exit 1 unless attempts >= 0
405
+ end
406
+ rescue Interrupt
407
+ puts
408
+ exit 0
409
+ else
410
+ ret
411
+ end
412
+ end
413
+ end
414
+
415
+ private
416
+
417
+ def with?(pat, script: nil, state: nil)
418
+ pat = state if pat.nil?
419
+ return pat == true unless pat.is_a?(::Regexp)
420
+
421
+ pat.match?(script || @script)
422
+ end
423
+ end
424
+ end
425
+ end
426
+
427
+ Repo = Squared::Repo
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Squared
4
+ module Repo
5
+ class << self
6
+ def resolve(*args)
7
+ ret = args.map do |id|
8
+ if id.is_a?(::Symbol)
9
+ project id
10
+ else
11
+ (id = Pathname.new(id).realdirpath.to_s) rescue nil if id.is_a?(::String)
12
+ get(:project).find { |_, val| val.path.to_s == id.to_s }
13
+ end
14
+ end
15
+ ret.size == 1 ? ret.first : ret
16
+ end
17
+
18
+ def expect(name)
19
+ ret = project(name)
20
+ return ret if ret&.path&.directory?
21
+
22
+ raise NoMethodError, message('project is not initialized', name)
23
+ end
24
+
25
+ def project?(name)
26
+ !!project(name)&.enabled?
27
+ end
28
+
29
+ private
30
+
31
+ def project(name)
32
+ get(:project)[name.to_sym]
33
+ end
34
+ end
35
+ end
36
+ end
37
+
38
+ require_relative 'repo/workspace'
39
+ require_relative 'repo/project'
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Squared
4
+ VERSION = '0.0.1'
5
+ end
data/lib/squared.rb ADDED
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'squared/version'
4
+ require_relative 'squared/common'
5
+ require_relative 'squared/repo'
6
+ require_relative 'squared/config'
7
+
8
+ Repo::Workspace.implement(
9
+ Repo::Project::Git,
10
+ Repo::Project::Node,
11
+ Repo::Project::Python,
12
+ Repo::Project::Ruby
13
+ )
data/squared.gemspec ADDED
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/squared/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "squared"
7
+ spec.version = Squared::VERSION
8
+ spec.authors = ["An Pham"]
9
+ spec.email = ["anpham6@gmail.com"]
10
+
11
+ spec.summary = %q{Rake task generator for managing multi-language workspaces.}
12
+ spec.description = %q{Rake task generator for managing multi-language workspaces.}
13
+ spec.homepage = "https://github.com/anpham6/squared"
14
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.4.0")
15
+ spec.licenses = ["BSD-3-Clause"]
16
+
17
+ spec.metadata["homepage_uri"] = spec.homepage
18
+ spec.metadata["source_code_uri"] = spec.homepage
19
+ spec.metadata["documentation_uri"] = "https://squared.readthedocs.io"
20
+
21
+ spec.files = Dir["lib/**/*"] +
22
+ %w[LICENSE README.md README.ruby.md squared.gemspec]
23
+ spec.bindir = "exe"
24
+ spec.executables = []
25
+ spec.require_paths = ["lib"]
26
+
27
+ spec.add_dependency "rake"
28
+ spec.add_dependency "logger"
29
+ end
metadata ADDED
@@ -0,0 +1,95 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: squared
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - An Pham
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2024-10-29 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rake
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: logger
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ description: Rake task generator for managing multi-language workspaces.
42
+ email:
43
+ - anpham6@gmail.com
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - LICENSE
49
+ - README.md
50
+ - README.ruby.md
51
+ - lib/squared.rb
52
+ - lib/squared/common.rb
53
+ - lib/squared/common/class.rb
54
+ - lib/squared/common/format.rb
55
+ - lib/squared/common/shell.rb
56
+ - lib/squared/common/system.rb
57
+ - lib/squared/common/task.rb
58
+ - lib/squared/config.rb
59
+ - lib/squared/repo.rb
60
+ - lib/squared/repo/project.rb
61
+ - lib/squared/repo/project/base.rb
62
+ - lib/squared/repo/project/git.rb
63
+ - lib/squared/repo/project/node.rb
64
+ - lib/squared/repo/project/python.rb
65
+ - lib/squared/repo/project/ruby.rb
66
+ - lib/squared/repo/workspace.rb
67
+ - lib/squared/version.rb
68
+ - squared.gemspec
69
+ homepage: https://github.com/anpham6/squared
70
+ licenses:
71
+ - BSD-3-Clause
72
+ metadata:
73
+ homepage_uri: https://github.com/anpham6/squared
74
+ source_code_uri: https://github.com/anpham6/squared
75
+ documentation_uri: https://squared.readthedocs.io
76
+ post_install_message:
77
+ rdoc_options: []
78
+ require_paths:
79
+ - lib
80
+ required_ruby_version: !ruby/object:Gem::Requirement
81
+ requirements:
82
+ - - ">="
83
+ - !ruby/object:Gem::Version
84
+ version: 2.4.0
85
+ required_rubygems_version: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ requirements: []
91
+ rubygems_version: 3.5.22
92
+ signing_key:
93
+ specification_version: 4
94
+ summary: Rake task generator for managing multi-language workspaces.
95
+ test_files: []