xcoder 0.1.15 → 0.1.18
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +6 -0
- data/.rbenv-version +1 -0
- data/.rvmrc +1 -1
- data/Gemfile +10 -2
- data/README.md +110 -9
- data/Rakefile +2 -2
- data/bin/xcoder +74 -0
- data/lib/xcode/builder.rb +1 -2
- data/lib/xcode/builder/base_builder.rb +231 -102
- data/lib/xcode/builder/build_parser.rb +146 -0
- data/lib/xcode/builder/project_target_config_builder.rb +2 -2
- data/lib/xcode/builder/scheme_builder.rb +29 -12
- data/lib/xcode/buildspec.rb +286 -0
- data/lib/xcode/configuration_list.rb +24 -24
- data/lib/xcode/deploy/ftp.rb +56 -0
- data/lib/xcode/deploy/kickfolio.rb +18 -0
- data/lib/xcode/deploy/s3.rb +38 -0
- data/lib/xcode/deploy/ssh.rb +43 -0
- data/lib/xcode/deploy/templates/index.rhtml +22 -0
- data/lib/xcode/deploy/templates/manifest.rhtml +31 -0
- data/lib/xcode/deploy/testflight.rb +32 -27
- data/lib/xcode/deploy/web_assets.rb +39 -0
- data/lib/xcode/info_plist.rb +16 -0
- data/lib/xcode/keychain.rb +33 -10
- data/lib/xcode/platform.rb +65 -0
- data/lib/xcode/project.rb +7 -3
- data/lib/xcode/provisioning_profile.rb +38 -2
- data/lib/xcode/scheme.rb +44 -17
- data/lib/xcode/shell/command.rb +79 -5
- data/lib/xcode/terminal_output.rb +116 -0
- data/lib/xcode/test/formatters/junit_formatter.rb +7 -2
- data/lib/xcode/test/formatters/stdout_formatter.rb +34 -25
- data/lib/xcode/test/parsers/kif_parser.rb +87 -0
- data/lib/xcode/test/parsers/ocunit_parser.rb +3 -3
- data/lib/xcode/version.rb +1 -1
- data/lib/xcode/workspace.rb +13 -5
- data/lib/xcoder.rb +33 -31
- data/spec/TestProject/TestProject.xcodeproj/project.pbxproj +1627 -1015
- data/spec/TestWorkspace.xcworkspace/contents.xcworkspacedata +7 -0
- data/spec/builder_spec.rb +87 -71
- data/spec/deploy_spec.rb +63 -0
- data/spec/ocunit_parser_spec.rb +1 -1
- data/xcoder.gemspec +3 -1
- metadata +95 -19
- data/lib/xcode/buildfile.rb +0 -101
- data/lib/xcode/shell.rb +0 -26
- data/spec/deploy_testflight_spec.rb +0 -27
@@ -0,0 +1,116 @@
|
|
1
|
+
require 'colorize'
|
2
|
+
|
3
|
+
module Xcode
|
4
|
+
module TerminalOutput
|
5
|
+
@@colour_enabled = true
|
6
|
+
@@log_level = :info
|
7
|
+
|
8
|
+
LEVELS = [
|
9
|
+
:error,
|
10
|
+
:warning,
|
11
|
+
:notice,
|
12
|
+
:info,
|
13
|
+
:debug
|
14
|
+
]
|
15
|
+
|
16
|
+
def log_level
|
17
|
+
@@log_level
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.log_level=(level)
|
21
|
+
raise "Unknown log level #{level}, should be one of #{LEVELS.join(', ')}" unless LEVELS.include? level
|
22
|
+
@@log_level = level
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.included(base)
|
26
|
+
@@colour_supported = terminal_supports_colors?
|
27
|
+
end
|
28
|
+
|
29
|
+
def color_output= color_output
|
30
|
+
@@colour_enabled = color_output
|
31
|
+
end
|
32
|
+
|
33
|
+
def color_output?
|
34
|
+
@@colour_supported and @@colour_enabled
|
35
|
+
end
|
36
|
+
|
37
|
+
#
|
38
|
+
# Print an IO input interaction
|
39
|
+
#
|
40
|
+
def print_input message, level=:debug
|
41
|
+
return if LEVELS.index(level) > LEVELS.index(@@log_level)
|
42
|
+
puts format_lhs("", "", "<") + message, :default
|
43
|
+
end
|
44
|
+
|
45
|
+
#
|
46
|
+
# Print an IO output interaction
|
47
|
+
def print_output message, level=:debug
|
48
|
+
return if LEVELS.index(level) > LEVELS.index(@@log_level)
|
49
|
+
puts format_lhs("", "", ">") + message, :default
|
50
|
+
end
|
51
|
+
|
52
|
+
def print_system message, level=:debug
|
53
|
+
return if LEVELS.index(level) > LEVELS.index(@@log_level)
|
54
|
+
puts format_lhs("", "", "!") + message, :green
|
55
|
+
end
|
56
|
+
|
57
|
+
def format_lhs(left, right, terminator=":")
|
58
|
+
# "#{left.to_s.ljust(10)} #{right.rjust(6)}#{terminator} "
|
59
|
+
"#{right.to_s.rjust(7)}#{terminator} "
|
60
|
+
end
|
61
|
+
|
62
|
+
def print_task(task, message, level=:info, cr=true)
|
63
|
+
return if LEVELS.index(level) > LEVELS.index(@@log_level)
|
64
|
+
|
65
|
+
level_str = ""
|
66
|
+
case level
|
67
|
+
when :error
|
68
|
+
level_str = "ERROR"
|
69
|
+
color = :red
|
70
|
+
when :warning
|
71
|
+
level_str = "WARN"
|
72
|
+
color = :yellow
|
73
|
+
when :notice
|
74
|
+
level_str = "NOTICE"
|
75
|
+
color = :green
|
76
|
+
when :info
|
77
|
+
level_str = ""
|
78
|
+
color = :blue
|
79
|
+
else
|
80
|
+
color = :default
|
81
|
+
end
|
82
|
+
|
83
|
+
print format_lhs(task, level_str), color
|
84
|
+
print message, (level==:warning or level==:error or level==:notice) ? color : :default
|
85
|
+
|
86
|
+
if block_given?
|
87
|
+
yield
|
88
|
+
end
|
89
|
+
|
90
|
+
print "\n" if cr
|
91
|
+
end
|
92
|
+
|
93
|
+
def puts(text, color = :default)
|
94
|
+
color_params = color_output? ? color : {}
|
95
|
+
super(text.colorize(color_params))
|
96
|
+
end
|
97
|
+
|
98
|
+
def print(text, color = :default)
|
99
|
+
color_params = color_output? ? color : {}
|
100
|
+
super(text.colorize(color_params))
|
101
|
+
end
|
102
|
+
|
103
|
+
def self.terminal_supports_colors?
|
104
|
+
# No colors unless we are being run via a TTY
|
105
|
+
return false unless $stdout.isatty
|
106
|
+
|
107
|
+
# Check if the terminal supports colors
|
108
|
+
colors = `tput colors 2> /dev/null`.chomp
|
109
|
+
if $?.exitstatus == 0
|
110
|
+
colors.to_i >= 8
|
111
|
+
else
|
112
|
+
false
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -6,7 +6,7 @@ module Xcode
|
|
6
6
|
module Formatters
|
7
7
|
class JunitFormatter
|
8
8
|
def initialize(dir)
|
9
|
-
@dir = File.expand_path(dir)
|
9
|
+
@dir = File.expand_path(dir).to_s
|
10
10
|
FileUtils.mkdir_p(@dir)
|
11
11
|
end
|
12
12
|
|
@@ -42,12 +42,17 @@ module Xcode
|
|
42
42
|
end
|
43
43
|
end
|
44
44
|
|
45
|
-
File.open(
|
45
|
+
File.open(File.join(@dir, sanitize_filename("TEST-#{report.name}.xml")), 'w') do |current_file|
|
46
46
|
current_file.write xml.target!
|
47
47
|
end
|
48
48
|
|
49
49
|
end # write
|
50
50
|
|
51
|
+
private
|
52
|
+
def sanitize_filename(filename)
|
53
|
+
filename.gsub(/[^0-9A-Za-z.\-]/, '_')
|
54
|
+
end
|
55
|
+
|
51
56
|
end # JUnitFormatter
|
52
57
|
|
53
58
|
end # Formatters
|
@@ -2,44 +2,53 @@ module Xcode
|
|
2
2
|
module Test
|
3
3
|
module Formatters
|
4
4
|
class StdoutFormatter
|
5
|
+
include Xcode::TerminalOutput
|
5
6
|
|
6
|
-
def initialize
|
7
|
+
def initialize(options = {})
|
7
8
|
@errors = []
|
9
|
+
@test_count = 0
|
10
|
+
options.each { |k,v| self.send("#{k}=", v) }
|
8
11
|
end
|
9
12
|
|
10
13
|
def before(report)
|
11
|
-
|
14
|
+
print_task :test, "Begin tests", :info
|
12
15
|
end
|
13
16
|
|
14
17
|
def after(report)
|
15
|
-
|
16
|
-
@errors.
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
18
|
+
level = @errors.count>0 ? :error : :info
|
19
|
+
if @errors.count>0
|
20
|
+
print_task :test, "The following failures occured:", :warning
|
21
|
+
@errors.each do |e|
|
22
|
+
print_task :test, "[#{e.suite.name} #{e.name}]", :error
|
23
|
+
e.errors.each do |error|
|
24
|
+
print_task :test, " #{error[:message]}", :error
|
25
|
+
print_task :test, " at #{error[:location]}", :error
|
26
|
+
if error[:data].count>0
|
27
|
+
print_task :test, "\n Test Output:", :error
|
28
|
+
print_task :test, " > #{error[:data].join(" > ")}\n\n", :error
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# if there is left over data in the test report, show that
|
33
|
+
if e.data.count>0
|
34
|
+
print_task :test, "\n There was this trailing output after the above failures", :error
|
35
|
+
print_task :test, " > #{e.data.join(" > ")}\n\n", :error
|
24
36
|
end
|
25
|
-
end
|
26
|
-
|
27
|
-
# if there is left over data in the test report, show that
|
28
|
-
if e.data.count>0
|
29
|
-
puts "\n There was this trailing output after the above failures"
|
30
|
-
puts " > #{e.data.join(" > ")}\n\n"
|
31
37
|
end
|
32
38
|
end
|
33
39
|
|
34
|
-
|
40
|
+
print_task :test, "Finished in #{report.duration} seconds", :info
|
41
|
+
print_task :test, "#{@test_count} tests, #{@errors.count} failures", report.failed? ? :error : :info
|
35
42
|
end
|
36
43
|
|
37
44
|
def before_suite(suite)
|
38
|
-
|
45
|
+
print_task :test, "#{suite.name}: ", :info, false
|
39
46
|
end
|
40
47
|
|
41
48
|
def after_suite(suite)
|
42
|
-
|
49
|
+
color = (suite.total_passed_tests == suite.tests.count) ? :info : :error
|
50
|
+
#print_task :test, "#{suite.total_passed_tests}/#{suite.tests.count}", color
|
51
|
+
puts " [#{suite.total_passed_tests}/#{suite.tests.count}]", color
|
43
52
|
end
|
44
53
|
|
45
54
|
def before_test(test)
|
@@ -47,14 +56,14 @@ module Xcode
|
|
47
56
|
end
|
48
57
|
|
49
58
|
def after_test(test)
|
59
|
+
@test_count += 1
|
50
60
|
if test.passed?
|
51
|
-
print "."
|
61
|
+
print ".", :green
|
52
62
|
elsif test.failed?
|
53
|
-
print "F"
|
63
|
+
print "F", :red
|
54
64
|
@errors << test
|
55
|
-
end
|
56
|
-
|
57
|
-
end
|
65
|
+
end
|
66
|
+
end
|
58
67
|
|
59
68
|
end # StdoutFormatter
|
60
69
|
end # Formatters
|
@@ -0,0 +1,87 @@
|
|
1
|
+
require 'xcode/test/report'
|
2
|
+
require 'time'
|
3
|
+
|
4
|
+
module Xcode
|
5
|
+
module Test
|
6
|
+
module Parsers
|
7
|
+
|
8
|
+
class KIFParser
|
9
|
+
attr_accessor :report, :builder
|
10
|
+
|
11
|
+
def initialize(report = Xcode::Test::Report.new)
|
12
|
+
@report = report
|
13
|
+
@awaiting_scenario_name = false
|
14
|
+
yield self if block_given?
|
15
|
+
end
|
16
|
+
|
17
|
+
def flush
|
18
|
+
@report.finish
|
19
|
+
end
|
20
|
+
|
21
|
+
def <<(piped_row)
|
22
|
+
if @awaiting_scenario_name
|
23
|
+
if match = piped_row.match(/\[\d+\:.+\]\s(.+)/)
|
24
|
+
name = match[1].strip
|
25
|
+
@report.add_suite name, Time.now
|
26
|
+
@awaiting_scenario_name = false
|
27
|
+
end
|
28
|
+
return
|
29
|
+
end
|
30
|
+
|
31
|
+
case piped_row.force_encoding("UTF-8")
|
32
|
+
|
33
|
+
when /BEGIN KIF TEST RUN: (\d+) scenarios/
|
34
|
+
@report.start
|
35
|
+
|
36
|
+
when /BEGIN SCENARIO (\d+)\/(\d+) \(\d+ steps\)/
|
37
|
+
@awaiting_scenario_name = true
|
38
|
+
|
39
|
+
when /END OF SCENARIO \(duration (\d+\.\d+)s/
|
40
|
+
@report.in_current_suite do |suite|
|
41
|
+
suite.finish(Time.now)
|
42
|
+
end
|
43
|
+
|
44
|
+
when /(PASS|FAIL) \((\d+\.\d+s)\): (.+)/
|
45
|
+
duration = $2.to_f
|
46
|
+
name = $3.strip
|
47
|
+
@report.in_current_suite do |suite|
|
48
|
+
test = suite.add_test_case(name)
|
49
|
+
|
50
|
+
if $1 == 'PASS'
|
51
|
+
test.passed(duration)
|
52
|
+
else
|
53
|
+
test.failed(duration)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
when /KIF TEST RUN FINISHED: \d+ failures \(duration (\d+\.\d+)s\)/
|
58
|
+
@report.finish
|
59
|
+
|
60
|
+
when /(.*): error: -\[(\S+) (\S+)\] : (.*)/
|
61
|
+
message = $4
|
62
|
+
location = $1
|
63
|
+
@report.in_current_test do |test|
|
64
|
+
test.add_error(message, location)
|
65
|
+
end
|
66
|
+
|
67
|
+
# when /failed with exit code (\d+)/,
|
68
|
+
when /BUILD FAILED/
|
69
|
+
@report.finish
|
70
|
+
|
71
|
+
when /Segmentation fault/
|
72
|
+
@report.abort
|
73
|
+
|
74
|
+
when /the iPhoneSimulator platform does not currently support application-hosted tests/
|
75
|
+
raise "Application tests are not currently supported by the iphone simulator. If these are logic tests, try unsetting TEST_HOST in your project config"
|
76
|
+
else
|
77
|
+
@report.in_current_test do |test|
|
78
|
+
test << piped_row
|
79
|
+
end
|
80
|
+
end # case
|
81
|
+
|
82
|
+
end # <<
|
83
|
+
|
84
|
+
end # OCUnitParser
|
85
|
+
end # Parsers
|
86
|
+
end # Test
|
87
|
+
end # Xcode
|
@@ -7,17 +7,17 @@ module Xcode
|
|
7
7
|
|
8
8
|
class OCUnitParser
|
9
9
|
attr_accessor :report, :builder
|
10
|
-
|
10
|
+
|
11
11
|
def initialize(report = Xcode::Test::Report.new)
|
12
12
|
@report = report
|
13
13
|
yield self if block_given?
|
14
14
|
end
|
15
15
|
|
16
|
-
def
|
16
|
+
def close
|
17
17
|
@report.finish
|
18
18
|
end
|
19
19
|
|
20
|
-
def <<
|
20
|
+
def << piped_row
|
21
21
|
case piped_row.force_encoding("UTF-8")
|
22
22
|
|
23
23
|
when /Test Suite '(\S+)'.*started at\s+(.*)/
|
data/lib/xcode/version.rb
CHANGED
data/lib/xcode/workspace.rb
CHANGED
@@ -15,9 +15,13 @@ module Xcode
|
|
15
15
|
doc = Nokogiri::XML(open("#{@path}/contents.xcworkspacedata"))
|
16
16
|
doc.search("FileRef").each do |file|
|
17
17
|
location = file["location"]
|
18
|
-
if matcher = location.match(/^group:(
|
18
|
+
if (matcher = location.match(/^group:(.+\.xcodeproj)$/i))
|
19
19
|
project_path = "#{workspace_root}/#{matcher[1]}"
|
20
|
-
|
20
|
+
begin
|
21
|
+
@projects << Xcode::Project.new(project_path)
|
22
|
+
rescue => e
|
23
|
+
puts "Skipping project file #{project_path} referened by #{self} as it failed to parse"
|
24
|
+
end
|
21
25
|
end
|
22
26
|
end
|
23
27
|
end
|
@@ -41,8 +45,8 @@ module Xcode
|
|
41
45
|
# @return [Scheme] the specific scheme that matches the name specified
|
42
46
|
#
|
43
47
|
def scheme(name)
|
44
|
-
scheme = schemes.select {|t| t.name == name.to_s}.first
|
45
|
-
raise "No such scheme #{name}, available schemes are #{schemes.map {|t| t.
|
48
|
+
scheme = schemes.select {|t| t.name == name.to_s and t.parent == self}.first
|
49
|
+
raise "No such scheme #{name} in #{self}, available schemes are #{schemes.map {|t| t.to_s}.join(', ')}" if scheme.nil?
|
46
50
|
yield scheme if block_given?
|
47
51
|
scheme
|
48
52
|
end
|
@@ -58,10 +62,14 @@ module Xcode
|
|
58
62
|
#
|
59
63
|
def project(name)
|
60
64
|
project = @projects.select {|c| c.name == name.to_s}.first
|
61
|
-
raise "No such project #{name}, available projects are #{@projects.map {|c| c.name}.join(', ')}" if project.nil?
|
65
|
+
raise "No such project #{name} in #{self}, available projects are #{@projects.map {|c| c.name}.join(', ')}" if project.nil?
|
62
66
|
yield project if block_given?
|
63
67
|
project
|
64
68
|
end
|
69
|
+
|
70
|
+
def to_s
|
71
|
+
"#{name} (Workspace)"
|
72
|
+
end
|
65
73
|
|
66
74
|
def describe
|
67
75
|
puts "Workspace #{name} contains:"
|
data/lib/xcoder.rb
CHANGED
@@ -1,90 +1,92 @@
|
|
1
1
|
require 'find'
|
2
2
|
require 'fileutils'
|
3
|
+
require 'xcode/terminal_output'
|
3
4
|
require "xcode/version"
|
4
5
|
require "xcode/project"
|
5
6
|
require "xcode/info_plist"
|
6
|
-
require
|
7
|
+
require 'xcode/shell/command'
|
7
8
|
require 'plist'
|
8
9
|
require 'xcode/keychain'
|
9
10
|
require 'xcode/workspace'
|
10
|
-
require 'xcode/
|
11
|
+
require 'xcode/platform'
|
12
|
+
require 'multi_json'
|
11
13
|
|
12
14
|
module Xcode
|
13
|
-
|
15
|
+
|
14
16
|
@@projects = nil
|
15
17
|
@@workspaces = nil
|
16
18
|
@@sdks = nil
|
17
|
-
|
19
|
+
|
18
20
|
#
|
19
21
|
# Find all the projects within the current working directory.
|
20
|
-
#
|
22
|
+
#
|
21
23
|
# @return [Array<Project>] an array of the all the Projects found.
|
22
|
-
#
|
24
|
+
#
|
23
25
|
def self.projects
|
24
26
|
@@projects = parse_projects if @@projects.nil?
|
25
|
-
@@projects
|
27
|
+
@@projects
|
26
28
|
end
|
27
|
-
|
29
|
+
|
28
30
|
#
|
29
31
|
# Find all the workspaces within the current working directory.
|
30
|
-
#
|
32
|
+
#
|
31
33
|
# @return [Array<Workspaces>] an array of all the Workspaces found.
|
32
|
-
#
|
34
|
+
#
|
33
35
|
def self.workspaces
|
34
36
|
@@workspaces = parse_workspaces if @@workspaces.nil?
|
35
37
|
@@workspaces
|
36
38
|
end
|
37
|
-
|
39
|
+
|
38
40
|
#
|
39
41
|
# Find the project with the specified name within the current working directory.
|
40
|
-
#
|
42
|
+
#
|
41
43
|
# @note this method will raise an error when it is unable to find the project
|
42
44
|
# specified.
|
43
|
-
#
|
45
|
+
#
|
44
46
|
# @param [String] name of the project (e.g. NAME.xcodeproj) that is attempting
|
45
47
|
# to be found.
|
46
|
-
#
|
48
|
+
#
|
47
49
|
# @return [Project] the project found; an error is raise if a project is unable
|
48
50
|
# to be found.
|
49
|
-
#
|
51
|
+
#
|
50
52
|
def self.project(name)
|
51
53
|
name = name.to_s
|
52
|
-
|
54
|
+
|
53
55
|
return Xcode::Project.new(name) if name=~/\.xcodeproj/
|
54
|
-
|
56
|
+
|
55
57
|
self.projects.each do |p|
|
56
58
|
return p if p.name == name
|
57
59
|
end
|
58
60
|
raise "Unable to find a project named #{name}. However, I did find these projects: #{self.projects.map {|p| p.name}.join(', ') }"
|
59
61
|
end
|
60
|
-
|
62
|
+
|
61
63
|
#
|
62
64
|
# Find the workspace with the specified name within the current working directory.
|
63
|
-
#
|
65
|
+
#
|
64
66
|
# @note this method will raise an error when it is unable to find the workspace
|
65
67
|
# specified.
|
66
|
-
#
|
68
|
+
#
|
67
69
|
# @param [String] name of the workspace (e.g. NAME.xcworkspace) that is attempting
|
68
70
|
# to be found.
|
69
|
-
#
|
71
|
+
#
|
70
72
|
# @return [Project] the workspace found; an error is raise if a workspace is unable
|
71
73
|
# to be found.
|
72
|
-
#
|
74
|
+
#
|
73
75
|
def self.workspace(name)
|
74
76
|
name = name.to_s
|
75
|
-
|
77
|
+
|
76
78
|
return Xcode::Workspace.new(name) if name=~/\.xcworkspace/
|
77
|
-
|
79
|
+
|
78
80
|
self.workspaces.each do |p|
|
79
81
|
return p if p.name == name
|
80
82
|
end
|
81
83
|
raise "Unable to find a workspace named #{name}. However, I did find these workspaces: #{self.workspaces.map {|p| p.name}.join(', ') }"
|
82
84
|
end
|
83
|
-
|
85
|
+
|
84
86
|
#
|
85
87
|
# @param [String] dir the path to search for projects; defaults to using
|
86
88
|
# the current working directory.
|
87
|
-
#
|
89
|
+
#
|
88
90
|
# @return [Array<Project>] the projects found at the specified directory.
|
89
91
|
#
|
90
92
|
def self.find_projects(dir='.')
|
@@ -99,17 +101,17 @@ module Xcode
|
|
99
101
|
parse_sdks if @@sdks.nil?
|
100
102
|
@@sdks.values.include? sdk
|
101
103
|
end
|
102
|
-
|
104
|
+
|
103
105
|
#
|
104
106
|
# Available SDKs available on this particular system.
|
105
|
-
#
|
107
|
+
#
|
106
108
|
# @return [Array<String>] the available SDKs on the current system.
|
107
|
-
#
|
109
|
+
#
|
108
110
|
def self.available_sdks
|
109
111
|
parse_sdks if @@sdks.nil?
|
110
112
|
@@sdks
|
111
113
|
end
|
112
|
-
|
114
|
+
|
113
115
|
private
|
114
116
|
def self.parse_sdks
|
115
117
|
@@sdks = {}
|
@@ -126,7 +128,7 @@ module Xcode
|
|
126
128
|
end
|
127
129
|
end
|
128
130
|
end
|
129
|
-
|
131
|
+
|
130
132
|
def self.parse_workspaces(dir='.')
|
131
133
|
projects = []
|
132
134
|
Find.find(dir) do |path|
|