u3d 0.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (124) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +2 -0
  3. data/.licenses.json +19 -0
  4. data/.rspec +1 -0
  5. data/.rubocop.yml +43 -0
  6. data/Gemfile +5 -0
  7. data/Gemfile.lock +98 -0
  8. data/LICENSE +21 -0
  9. data/LICENSE.fastlane +22 -0
  10. data/LOG_RULES.md +170 -0
  11. data/README.md +72 -0
  12. data/Rakefile +28 -0
  13. data/TODO.md +15 -0
  14. data/build.sh +5 -0
  15. data/config/log_rules.json +230 -0
  16. data/examples/Example1/.gitignore +19 -0
  17. data/examples/Example1/Assets/Editor.meta +9 -0
  18. data/examples/Example1/Assets/Editor/EditorRun.cs +23 -0
  19. data/examples/Example1/Assets/Editor/EditorRun.cs.meta +12 -0
  20. data/examples/Example1/Assets/Editor/FileSystemUtil.cs +26 -0
  21. data/examples/Example1/Assets/Editor/FileSystemUtil.cs.meta +12 -0
  22. data/examples/Example1/Assets/Scene1.unity +264 -0
  23. data/examples/Example1/Assets/Scene1.unity.meta +8 -0
  24. data/examples/Example1/Gemfile +8 -0
  25. data/examples/Example1/Gemfile.lock +165 -0
  26. data/examples/Example1/ProjectSettings/AudioManager.asset +16 -0
  27. data/examples/Example1/ProjectSettings/ClusterInputManager.asset +6 -0
  28. data/examples/Example1/ProjectSettings/DynamicsManager.asset +18 -0
  29. data/examples/Example1/ProjectSettings/EditorBuildSettings.asset +7 -0
  30. data/examples/Example1/ProjectSettings/EditorSettings.asset +14 -0
  31. data/examples/Example1/ProjectSettings/GraphicsSettings.asset +61 -0
  32. data/examples/Example1/ProjectSettings/InputManager.asset +295 -0
  33. data/examples/Example1/ProjectSettings/NavMeshAreas.asset +89 -0
  34. data/examples/Example1/ProjectSettings/NetworkManager.asset +8 -0
  35. data/examples/Example1/ProjectSettings/Physics2DSettings.asset +35 -0
  36. data/examples/Example1/ProjectSettings/ProjectSettings.asset +591 -0
  37. data/examples/Example1/ProjectSettings/ProjectVersion.txt +1 -0
  38. data/examples/Example1/ProjectSettings/QualitySettings.asset +180 -0
  39. data/examples/Example1/ProjectSettings/TagManager.asset +43 -0
  40. data/examples/Example1/ProjectSettings/TimeManager.asset +9 -0
  41. data/examples/Example1/ProjectSettings/UnityConnectSettings.asset +32 -0
  42. data/examples/Example1/README.md +5 -0
  43. data/examples/Example1/Rakefile +5 -0
  44. data/examples/Example1/fastlane/Fastfile +4 -0
  45. data/examples/Example1/fastlane/Pluginfile +1 -0
  46. data/examples/Example1/run.sh +1 -0
  47. data/examples/Example2/.gitignore +20 -0
  48. data/examples/Example2/Assets/Editor.meta +9 -0
  49. data/examples/Example2/Assets/Editor/EditorRun.cs +33 -0
  50. data/examples/Example2/Assets/Editor/EditorRun.cs.meta +12 -0
  51. data/examples/Example2/Assets/Editor/PostprocessBuildPlayer.cs +92 -0
  52. data/examples/Example2/Assets/Editor/PostprocessBuildPlayer.cs.meta +8 -0
  53. data/examples/Example2/Assets/Editor/PostprocessBuildPlayer_log.sh +31 -0
  54. data/examples/Example2/Assets/Editor/PostprocessBuildPlayer_log.sh.meta +8 -0
  55. data/examples/Example2/Assets/Editor/SimpleBuildSetup.cs +20 -0
  56. data/examples/Example2/Assets/Editor/SimpleBuildSetup.cs.meta +12 -0
  57. data/examples/Example2/Assets/Scene.unity +278 -0
  58. data/examples/Example2/Assets/Scene.unity.meta +8 -0
  59. data/examples/Example2/Gemfile +8 -0
  60. data/examples/Example2/Gemfile.lock +165 -0
  61. data/examples/Example2/ProjectSettings/AudioManager.asset +17 -0
  62. data/examples/Example2/ProjectSettings/ClusterInputManager.asset +6 -0
  63. data/examples/Example2/ProjectSettings/DynamicsManager.asset +19 -0
  64. data/examples/Example2/ProjectSettings/EditorBuildSettings.asset +10 -0
  65. data/examples/Example2/ProjectSettings/EditorSettings.asset +14 -0
  66. data/examples/Example2/ProjectSettings/GraphicsSettings.asset +63 -0
  67. data/examples/Example2/ProjectSettings/InputManager.asset +295 -0
  68. data/examples/Example2/ProjectSettings/NavMeshAreas.asset +89 -0
  69. data/examples/Example2/ProjectSettings/NetworkManager.asset +8 -0
  70. data/examples/Example2/ProjectSettings/Physics2DSettings.asset +36 -0
  71. data/examples/Example2/ProjectSettings/ProjectSettings.asset +591 -0
  72. data/examples/Example2/ProjectSettings/ProjectVersion.txt +1 -0
  73. data/examples/Example2/ProjectSettings/QualitySettings.asset +193 -0
  74. data/examples/Example2/ProjectSettings/TagManager.asset +43 -0
  75. data/examples/Example2/ProjectSettings/TimeManager.asset +9 -0
  76. data/examples/Example2/ProjectSettings/UnityConnectSettings.asset +34 -0
  77. data/examples/Example2/README.md +10 -0
  78. data/examples/Example2/fastlane/Fastfile +4 -0
  79. data/examples/Example2/fastlane/Pluginfile +1 -0
  80. data/exe/u3d +7 -0
  81. data/fastlane-plugin-u3d/.gitignore +10 -0
  82. data/fastlane-plugin-u3d/.licenses.json +9 -0
  83. data/fastlane-plugin-u3d/.rspec +3 -0
  84. data/fastlane-plugin-u3d/.rubocop.yml +253 -0
  85. data/fastlane-plugin-u3d/.travis.yml +4 -0
  86. data/fastlane-plugin-u3d/Gemfile +6 -0
  87. data/fastlane-plugin-u3d/LICENSE +21 -0
  88. data/fastlane-plugin-u3d/README.md +52 -0
  89. data/fastlane-plugin-u3d/Rakefile +9 -0
  90. data/fastlane-plugin-u3d/circle.yml +9 -0
  91. data/fastlane-plugin-u3d/fastlane-plugin-u3d.gemspec +31 -0
  92. data/fastlane-plugin-u3d/fastlane/Fastfile +3 -0
  93. data/fastlane-plugin-u3d/fastlane/Pluginfile +1 -0
  94. data/fastlane-plugin-u3d/lib/fastlane/plugin/u3d.rb +38 -0
  95. data/fastlane-plugin-u3d/lib/fastlane/plugin/u3d/actions/u3d_action.rb +80 -0
  96. data/fastlane-plugin-u3d/lib/fastlane/plugin/u3d/helper/u3d_helper.rb +34 -0
  97. data/fastlane-plugin-u3d/lib/fastlane/plugin/u3d/version.rb +27 -0
  98. data/fastlane-plugin-u3d/spec/spec_helper.rb +32 -0
  99. data/lib/u3d.rb +33 -0
  100. data/lib/u3d/cache.rb +120 -0
  101. data/lib/u3d/commands.rb +307 -0
  102. data/lib/u3d/commands_generator.rb +163 -0
  103. data/lib/u3d/downloader.rb +363 -0
  104. data/lib/u3d/iniparser.rb +83 -0
  105. data/lib/u3d/installer.rb +445 -0
  106. data/lib/u3d/log_analyzer.rb +221 -0
  107. data/lib/u3d/unity_version_number.rb +71 -0
  108. data/lib/u3d/unity_versions.rb +207 -0
  109. data/lib/u3d/utils.rb +121 -0
  110. data/lib/u3d/version.rb +31 -0
  111. data/lib/u3d_core.rb +30 -0
  112. data/lib/u3d_core/command_executor.rb +134 -0
  113. data/lib/u3d_core/command_runner.rb +93 -0
  114. data/lib/u3d_core/credentials.rb +116 -0
  115. data/lib/u3d_core/globals.rb +84 -0
  116. data/lib/u3d_core/helper.rb +149 -0
  117. data/lib/u3d_core/ui/disable_colors.rb +40 -0
  118. data/lib/u3d_core/ui/implementations/shell.rb +157 -0
  119. data/lib/u3d_core/ui/interface.rb +182 -0
  120. data/lib/u3d_core/ui/ui.rb +49 -0
  121. data/local_gem_install.sh +6 -0
  122. data/scripts/be +14 -0
  123. data/u3d.gemspec +41 -0
  124. metadata +388 -0
@@ -0,0 +1,363 @@
1
+ ## --- BEGIN LICENSE BLOCK ---
2
+ # Copyright (c) 2016-present WeWantToKnow AS
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ # of this software and associated documentation files (the "Software"), to deal
6
+ # in the Software without restriction, including without limitation the rights
7
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ # copies of the Software, and to permit persons to whom the Software is
9
+ # furnished to do so, subject to the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be included in all
12
+ # copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ # SOFTWARE.
21
+ ## --- END LICENSE BLOCK ---
22
+
23
+ require 'net/http'
24
+ require 'u3d/iniparser'
25
+ require 'u3d/utils'
26
+
27
+ module U3d
28
+ # Take care of downloading files and packages
29
+ module Downloader
30
+ # Name of the directory for the package downloading
31
+ DOWNLOAD_DIRECTORY = 'Unity_Packages'.freeze
32
+ # Path to the directory for the package downloading
33
+ DOWNLOAD_PATH = "#{ENV['HOME']}/Downloads".freeze
34
+ # Regex to get the name of a package out of its file name
35
+ UNITY_MODULE_FILE_REGEX = %r{\/([\w\-_\.\+]+\.(?:pkg|exe|zip|sh|deb))}
36
+
37
+ class << self
38
+ def hash_validation(expected: nil, actual: nil)
39
+ if expected
40
+ if expected != actual
41
+ UI.verbose "Expected hash is #{expected}, file hash is #{actual}"
42
+ UI.important 'File looks corrupted (wrong hash)'
43
+ return false
44
+ end
45
+ else
46
+ UI.verbose 'No hash validation available. File is assumed correct but may not be.'
47
+ end
48
+ true
49
+ end
50
+
51
+ def size_validation(expected: nil, actual: nil)
52
+ if expected
53
+ if expected != actual
54
+ UI.verbose "Expected size is #{expected}, file size is #{actual}"
55
+ UI.important 'File looks corrupted (wrong size)'
56
+ return false
57
+ end
58
+ else
59
+ UI.verbose 'No size validation available. File is assumed correct but may not be.'
60
+ end
61
+ true
62
+ end
63
+
64
+ def download_package(path, url, size: nil)
65
+ File.open(path, 'wb') do |f|
66
+ uri = URI(url)
67
+ current = 0
68
+ Net::HTTP.start(uri.host, uri.port) do |http|
69
+ request = Net::HTTP::Get.new uri
70
+ http.request request do |response|
71
+ begin
72
+ size ||= Integer(response['Content-Length'])
73
+ rescue ArgumentError
74
+ UI.verbose 'Unable to get length of file in download'
75
+ end
76
+ started_at = Time.now.to_i - 1
77
+ response.read_body do |segment|
78
+ f.write(segment)
79
+ current += segment.length
80
+ next unless UI.interactive?
81
+ if size
82
+ Utils.print_progress(current, size, started_at)
83
+ else
84
+ Utils.print_progress_nosize(current, started_at)
85
+ end
86
+ end
87
+ end
88
+ end
89
+ print "\n" if UI.interactive?
90
+ end
91
+ rescue Interrupt => e
92
+ # Ensure that the file is deleted if download is aborted
93
+ File.delete path
94
+ raise e
95
+ end
96
+ end
97
+
98
+ class MacDownloader
99
+ class << self
100
+ # Downloads all packages available for given version
101
+ def download_all(version, cached_versions)
102
+ if cached_versions[version].nil?
103
+ UI.error "No version #{version} was found in cache. It might need updating."
104
+ return nil
105
+ end
106
+ files = []
107
+ ini_file = INIparser.load_ini(version, cached_versions)
108
+ ini_file.keys.each do |k|
109
+ result = download_specific(k, version, cached_versions)
110
+ files << [k, result[0], result[1]] unless result.nil?
111
+ end
112
+ files
113
+ end
114
+
115
+ # Downloads a specific package for given version
116
+ def download_specific(package, version, cached_versions)
117
+ if cached_versions[version].nil?
118
+ UI.error "No version #{version} was found in cache. It might need updating."
119
+ return nil
120
+ end
121
+
122
+ ini_file = INIparser.load_ini(version, cached_versions)
123
+ if ini_file[package].empty?
124
+ UI.error "No package \"#{package}\" was found for version #{version}."
125
+ return nil
126
+ end
127
+
128
+ url = cached_versions[version]
129
+ dir = File.join(DOWNLOAD_PATH, DOWNLOAD_DIRECTORY, version)
130
+ Utils.ensure_dir(dir)
131
+ return [get_package(package, ini_file, dir, url), ini_file[package]]
132
+ end
133
+
134
+ private #---------------------------------------------------------------
135
+
136
+ def get_package(name, ini_file, main_dir, base_url)
137
+ file_name = UNITY_MODULE_FILE_REGEX.match(ini_file[name]['url'])[1]
138
+ file_path = File.expand_path(file_name, main_dir)
139
+
140
+ # Check if file already exists and validate it
141
+ if File.file?(file_path)
142
+ if Downloader.size_validation(expected: ini_file[name]['size'], actual: File.size(file_path)) &&
143
+ Downloader.hash_validation(expected: ini_file[name]['md5'], actual: Utils.hashfile(file_path))
144
+ UI.important "#{name.capitalize} already downloaded at #{file_path}"
145
+ return file_path
146
+ else
147
+ UI.verbose "Deleting existing file at #{file_path}"
148
+ File.delete(file_path)
149
+ end
150
+ end
151
+
152
+ # Download file
153
+ url = base_url + ini_file[name]['url']
154
+ UI.header "Downloading #{name}"
155
+ UI.verbose 'Downloading from ' + url.to_s.cyan.underline
156
+ Downloader.download_package(file_path, url, size: ini_file[name]['size'])
157
+
158
+ # Validation download
159
+ if Downloader.size_validation(expected: ini_file[name]['size'], actual: File.size(file_path)) &&
160
+ Downloader.hash_validation(expected: ini_file[name]['md5'], actual: Utils.hashfile(file_path))
161
+ UI.success "Successfully downloaded #{name}."
162
+ else
163
+ File.delete(file_path)
164
+ raise 'Download failed: file is corrupted, deleting it.'
165
+ end
166
+
167
+ file_path
168
+ end
169
+
170
+ def all_local_files(version)
171
+ files = []
172
+ ini_file = INIparser.load_ini(version, {}, offline: true)
173
+ ini_file.keys.each do |k|
174
+ result = local_file(k, version)
175
+ files << [k, result[0], result[1]] unless result.nil?
176
+ end
177
+ files
178
+ end
179
+
180
+ def local_file(package, version)
181
+ ini_file = INIparser.load_ini(version, {}, offline: true)
182
+ if ini_file[package].empty?
183
+ UI.error "No package \"#{package}\" was found for version #{version}."
184
+ return nil
185
+ end
186
+
187
+ dir = File.join(DOWNLOAD_PATH, DOWNLOAD_DIRECTORY, version)
188
+ raise "Main directory #{dir} does not exist. Nothing has been downloaded for version #{version}" unless Dir.exist?(dir)
189
+
190
+ file_name = UNITY_MODULE_FILE_REGEX.match(ini_file[package]['url'])[1]
191
+ file_path = File.expand_path(file_name, dir)
192
+
193
+ unless File.file?(file_path)
194
+ UI.error "Package #{package} has not been downloaded"
195
+ return nil
196
+ end
197
+
198
+ unless Downloader.size_validation(expected: ini_file[package]['size'], actual: File.size(file_path)) &&
199
+ Downloader.hash_validation(expected: ini_file[package]['md5'], actual: Utils.hashfile(file_path))
200
+ UI.error "File at #{file_path} is corrupted, deleting it"
201
+ File.delete(file_path)
202
+ return nil
203
+ end
204
+
205
+ return [file_path, ini_file[package]]
206
+ end
207
+ end
208
+ end
209
+
210
+ class LinuxDownloader
211
+ class << self
212
+ def download(version, cached_versions)
213
+ if cached_versions[version].nil?
214
+ UI.error "No version #{version} was found in cache. It might need updating."
215
+ return nil
216
+ end
217
+ url = cached_versions[version]
218
+ dir = File.join(DOWNLOAD_PATH, DOWNLOAD_DIRECTORY, version)
219
+ Utils.ensure_dir(dir)
220
+ file_name = UNITY_MODULE_FILE_REGEX.match(url)[1]
221
+ file_path = File.expand_path(file_name, dir)
222
+
223
+ # Check if file already exists
224
+ # Note: without size or hash validation, the file is assumed to be correct
225
+ if File.file?(file_path)
226
+ UI.important "File already downloaded at #{file_path}"
227
+ return file_path
228
+ end
229
+
230
+ # Download file
231
+ UI.header "Downloading Unity #{version}"
232
+ UI.verbose 'Downloading from ' + url.to_s.cyan.underline
233
+ Downloader.download_package(file_path, url)
234
+ U3dCore::CommandExecutor.execute(command: "chmod a+x #{file_path}")
235
+ file_path
236
+ end
237
+
238
+ def local_file(version)
239
+ dir = File.join(DOWNLOAD_PATH, DOWNLOAD_DIRECTORY, version)
240
+ raise "Main directory #{dir} does not exist. Nothing has been downloaded for version #{version}" unless Dir.exist?(dir)
241
+ find_cmd = "find #{dir}/ -maxdepth 2 -name '*.sh'"
242
+ files = U3dCore::CommandExecutor.execute(command: find_cmd).split("\n")
243
+ return files[0] unless files.empty?
244
+ raise 'No file has been downloaded'
245
+ end
246
+ end
247
+ end
248
+
249
+ class WindowsDownloader
250
+ class << self
251
+ def download_all(version, cached_versions)
252
+ if cached_versions[version].nil?
253
+ UI.error "No version #{version} was found in cache. It might need updating."
254
+ return nil
255
+ end
256
+ files = []
257
+ ini_file = INIparser.load_ini(version, cached_versions)
258
+ ini_file.keys.each do |k|
259
+ result = download_specific(k, version, cached_versions)
260
+ files << [k, result[0], result[1]] unless result.nil?
261
+ end
262
+ files
263
+ end
264
+
265
+ # Downloads a specific package for given version
266
+ def download_specific(package, version, cached_versions)
267
+ if cached_versions[version].nil?
268
+ UI.error "No version #{version} was found in cache. It might need updating."
269
+ return nil
270
+ end
271
+
272
+ ini_file = INIparser.load_ini(version, cached_versions)
273
+ if ini_file[package].empty?
274
+ UI.error "No package \"#{package}\" was found for version #{version}."
275
+ return nil
276
+ end
277
+
278
+ url = cached_versions[version]
279
+ dir = File.join(DOWNLOAD_PATH, DOWNLOAD_DIRECTORY, version)
280
+ Utils.ensure_dir(dir)
281
+ return [get_package(package, ini_file, dir, url), ini_file[package]]
282
+ end
283
+
284
+ def all_local_files(version)
285
+ files = []
286
+ ini_file = INIparser.load_ini(version, {}, offline: true)
287
+ ini_file.keys.each do |k|
288
+ result = local_file(k, version)
289
+ files << [k, result[0], result[1]] unless result.nil?
290
+ end
291
+ files
292
+ end
293
+
294
+ def local_file(package, version)
295
+ ini_file = INIparser.load_ini(version, {}, offline: true)
296
+ if ini_file[package].empty?
297
+ UI.error "No package \"#{package}\" was found for version #{version}."
298
+ return nil
299
+ end
300
+
301
+ dir = File.join(DOWNLOAD_PATH, DOWNLOAD_DIRECTORY, version)
302
+ raise "Main directory #{dir} does not exist. Nothing has been downloaded for version #{version}" unless Dir.exist?(dir)
303
+
304
+ file_name = UNITY_MODULE_FILE_REGEX.match(ini_file[package]['url'])[1]
305
+ file_path = File.expand_path(file_name, dir)
306
+
307
+ unless File.file?(file_path)
308
+ UI.error "Package #{package} has not been downloaded"
309
+ return nil
310
+ end
311
+
312
+ rounded_size = (File.size(file_path).to_f / 1024).floor
313
+ unless Downloader.size_validation(expected: ini_file[package]['size'], actual: rounded_size) &&
314
+ Downloader.hash_validation(expected: ini_file[package]['md5'], actual: Utils.hashfile(file_path))
315
+ UI.error "File at #{file_path} is corrupted, deleting it"
316
+ File.delete(file_path)
317
+ return nil
318
+ end
319
+
320
+ return [file_path, ini_file[package]]
321
+ end
322
+
323
+ private #---------------------------------------------------------------
324
+
325
+ def get_package(name, ini_file, main_dir, base_url)
326
+ file_name = UNITY_MODULE_FILE_REGEX.match(ini_file[name]['url'])[1]
327
+ file_path = File.expand_path(file_name, main_dir)
328
+
329
+ # Check if file already exists and validate it
330
+ if File.file?(file_path)
331
+ rounded_size = (File.size(file_path).to_f / 1024).floor
332
+ if Downloader.size_validation(expected: ini_file[name]['size'], actual: rounded_size) &&
333
+ Downloader.hash_validation(expected: ini_file[name]['md5'], actual: Utils.hashfile(file_path))
334
+ UI.important "File already downloaded at #{file_path}"
335
+ return file_path
336
+ else
337
+ UI.verbose 'Deleting existing file'
338
+ File.delete(file_path)
339
+ end
340
+ end
341
+
342
+ # Download file
343
+ url = base_url + ini_file[name]['url']
344
+ UI.header "Downloading #{name}"
345
+ UI.verbose 'Downloading from ' + url.to_s.cyan.underline
346
+ Downloader.download_package(file_path, url, size: ini_file[name]['size'] * 1024)
347
+
348
+ # Validation download
349
+ rounded_size = (File.size(file_path).to_f / 1024).floor
350
+ if Downloader.size_validation(expected: ini_file[name]['size'], actual: rounded_size) &&
351
+ Downloader.hash_validation(expected: ini_file[name]['md5'], actual: Utils.hashfile(file_path))
352
+ UI.success "Successfully downloaded #{name}."
353
+ else
354
+ File.delete(file_path)
355
+ raise 'Download failed: file is corrupted, deleting it.'
356
+ end
357
+
358
+ file_path
359
+ end
360
+ end
361
+ end
362
+ end
363
+ end
@@ -0,0 +1,83 @@
1
+ ## --- BEGIN LICENSE BLOCK ---
2
+ # Copyright (c) 2016-present WeWantToKnow AS
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ # of this software and associated documentation files (the "Software"), to deal
6
+ # in the Software without restriction, including without limitation the rights
7
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ # copies of the Software, and to permit persons to whom the Software is
9
+ # furnished to do so, subject to the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be included in all
12
+ # copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ # SOFTWARE.
21
+ ## --- END LICENSE BLOCK ---
22
+
23
+ require 'inifile'
24
+ require 'u3d/utils'
25
+ require 'u3d_core/helper'
26
+
27
+ module U3d
28
+ # Load and parse INI files
29
+ module INIparser
30
+ #####################################################
31
+ # @!group INI parameters to load and save ini files
32
+ #####################################################
33
+ INI_NAME = 'unity-%{version}-%{os}.ini'.freeze
34
+ INI_LINUX_PATH = File.join(ENV['HOME'], '.u3d', 'ini_files').freeze
35
+ INI_MAC_PATH = File.join(ENV['HOME'], 'Library', 'Application Support', 'u3d', 'ini_files').freeze
36
+ INI_WIN_PATH = File.join(ENV['HOME'], 'AppData', 'Local', 'u3d', 'ini_files').freeze
37
+
38
+ class << self
39
+ def load_ini(version, cached_versions, os: U3dCore::Helper.operating_system, offline: false)
40
+ unless os == :win || os == :mac
41
+ raise ArgumentError, "OS #{os.id2name} does not use ini files"
42
+ end
43
+ os = if os == :mac
44
+ 'osx'
45
+ else
46
+ os.id2name
47
+ end
48
+ ini_name = INI_NAME % { version: version, os: os }
49
+ Utils.ensure_dir(default_ini_path)
50
+ ini_path = File.expand_path(ini_name, default_ini_path)
51
+ unless File.file?(ini_path)
52
+ raise "INI file does not exist at #{ini_path}" if offline
53
+ uri = URI(cached_versions[version] + ini_name)
54
+ File.open(ini_path, 'wb') do |f|
55
+ data = Net::HTTP.get(uri)
56
+ data.tr!("\"", '')
57
+ data.gsub!(/Note:.+\n/, '')
58
+ f.write(data)
59
+ end
60
+ end
61
+ begin
62
+ result = IniFile.load(ini_path).to_h
63
+ rescue => e
64
+ raise "Could not parse INI data (#{e})"
65
+ end
66
+ result
67
+ end
68
+
69
+ private
70
+
71
+ def default_ini_path
72
+ case U3dCore::Helper.operating_system
73
+ when :linux
74
+ INI_LINUX_PATH
75
+ when :mac
76
+ INI_MAC_PATH
77
+ when :win
78
+ INI_WIN_PATH
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,445 @@
1
+ ## --- BEGIN LICENSE BLOCK ---
2
+ # Copyright (c) 2016-present WeWantToKnow AS
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ # of this software and associated documentation files (the "Software"), to deal
6
+ # in the Software without restriction, including without limitation the rights
7
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ # copies of the Software, and to permit persons to whom the Software is
9
+ # furnished to do so, subject to the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be included in all
12
+ # copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ # SOFTWARE.
21
+ ## --- END LICENSE BLOCK ---
22
+
23
+ require 'u3d/utils'
24
+ require 'fileutils'
25
+ require 'file-tail'
26
+
27
+ # Mac specific only right now
28
+ module U3d
29
+ DEFAULT_LINUX_INSTALL = '/opt/'.freeze
30
+ DEFAULT_MAC_INSTALL = '/'.freeze
31
+ DEFAULT_WINDOWS_INSTALL = 'C:/Program Files/'.freeze
32
+ UNITY_DIR = "Unity_%s".freeze
33
+ UNITY_DIR_CHECK = /Unity_\d+\.\d+\.\d+[a-z]\d+/
34
+
35
+ class Installation
36
+ def self.create(path: nil)
37
+ if Helper.mac?
38
+ MacInstallation.new path
39
+ elsif Helper.linux?
40
+ LinuxInstallation.new path
41
+ else
42
+ WindowsInstallation.new path
43
+ end
44
+ end
45
+ end
46
+
47
+ class MacInstallation < Installation
48
+ attr_reader :path
49
+
50
+ require 'plist'
51
+
52
+ def initialize(path: nil)
53
+ @path = path
54
+ end
55
+
56
+ def version
57
+ plist['CFBundleVersion']
58
+ end
59
+
60
+ def default_log_file
61
+ "#{ENV['HOME']}/Library/Logs/Unity/Editor.log"
62
+ end
63
+
64
+ def exe_path
65
+ "#{path}/Contents/MacOS/Unity"
66
+ end
67
+
68
+ def packages
69
+ if Utils.parse_unity_version(version)[0].to_i <= 4
70
+ # Unity < 5 doesn't have packages
71
+ return []
72
+ end
73
+ fpath = File.expand_path('../PlaybackEngines', path)
74
+ raise "Unity installation does not seem correct. Couldn't locate PlaybackEngines." unless Dir.exist? fpath
75
+ Dir.entries(fpath).select { |e| File.directory?(File.join(fpath, e)) && !(e == '.' || e == '..') }
76
+ end
77
+
78
+ private
79
+
80
+ def plist
81
+ @plist ||= Plist.parse_xml("#{@path}/Contents/Info.plist")
82
+ end
83
+ end
84
+
85
+ class LinuxInstallation < Installation
86
+ attr_reader :path
87
+
88
+ def initialize(path: nil)
89
+ @path = path
90
+ end
91
+
92
+ def version
93
+ # I don't find an easy way to extract the version on Linux
94
+ require 'rexml/document'
95
+ fpath = "#{path}/Data/PlaybackEngines/LinuxStandaloneSupport/ivy.xml"
96
+ raise "Couldn't find file #{fpath}" unless File.exist? fpath
97
+ doc = REXML::Document.new(File.read(fpath))
98
+ version = REXML::XPath.first(doc, 'ivy-module/info/@e:unityVersion').value
99
+ if m = version.match(/^(.*)x(.*)Linux$/)
100
+ version = "#{m[1]}#{m[2]}"
101
+ end
102
+ version
103
+ end
104
+
105
+ def default_log_file
106
+ "#{ENV['HOME']}/.config/unity3d/Editor.log"
107
+ end
108
+
109
+ def exe_path
110
+ "#{path}/Unity"
111
+ end
112
+
113
+ def packages
114
+ false
115
+ end
116
+ end
117
+
118
+ class WindowsInstallation < Installation
119
+ attr_reader :path
120
+
121
+ def initialize(path: nil)
122
+ @path = path
123
+ end
124
+
125
+ def version
126
+ require 'rexml/document'
127
+ fpath = "#{path}/Editor/Data/PlaybackEngines/windowsstandalonesupport/ivy.xml"
128
+ raise "Couldn't find file #{fpath}" unless File.exist? fpath
129
+ doc = REXML::Document.new(File.read(fpath))
130
+ version = REXML::XPath.first(doc, 'ivy-module/info/@e:unityVersion').value
131
+
132
+ version
133
+ end
134
+
135
+ def default_log_file
136
+ if @logfile.nil?
137
+ begin
138
+ loc_appdata = Utils.windows_local_appdata
139
+ log_dir = File.expand_path('Unity/Editor/', loc_appdata)
140
+ UI.important "Log directory (#{log_dir}) does not exist" unless Dir.exist? log_dir
141
+ @logfile = File.expand_path('Editor.log', log_dir)
142
+ rescue RuntimeError => ex
143
+ UI.error "Unable to retrieve the editor logfile: #{ex}"
144
+ end
145
+ end
146
+ @logfile
147
+ end
148
+
149
+ def exe_path
150
+ File.join(@path, 'Editor', 'Unity.exe')
151
+ end
152
+
153
+ def packages
154
+ # Unity prior to Unity5 did not have package
155
+ return [] if Utils.parse_unity_version(version)[0].to_i <= 4
156
+ fpath = "#{path}/Editor/Data/PlaybackEngines/"
157
+ raise "Unity installation does not seem correct. Couldn't locate PlaybackEngines." unless Dir.exist? fpath
158
+ Dir.entries(fpath).select { |e| File.directory?(File.join(fpath, e)) && !(e == '.' || e == '..') }
159
+ end
160
+ end
161
+
162
+ class Runner
163
+ def run(installation, args, raw_logs: false)
164
+ require 'fileutils'
165
+
166
+ log_file = find_logFile_in_args(args)
167
+
168
+ if log_file # we wouldn't want to do that for the default log file.
169
+ File.delete(log_file) if File.exist?(log_file)
170
+ else
171
+ log_file = installation.default_log_file
172
+ end
173
+
174
+ FileUtils.touch(log_file)
175
+
176
+ tail_thread = Thread.new do
177
+ begin
178
+ if raw_logs
179
+ pipe(log_file) { |l| UI.message l.rstrip }
180
+ else
181
+ analyzer = LogAnalyzer.new
182
+ pipe(log_file) { |l| analyzer.parse_line l }
183
+ end
184
+ rescue => e
185
+ UI.error "Failure while trying to pipe #{log_file}: #{e.message}"
186
+ e.backtrace.each { |l| UI.error " #{l}" }
187
+ end
188
+ end
189
+
190
+ begin
191
+ args.unshift(installation.exe_path)
192
+ if Helper.windows?
193
+ args.map! { |a| a =~ / / ? "\"#{a}\"" : a }
194
+ else
195
+ args.map!(&:shellescape)
196
+ end
197
+ U3dCore::CommandExecutor.execute(command: args)
198
+ ensure
199
+ sleep 0.5
200
+ Thread.kill(tail_thread)
201
+ end
202
+ end
203
+
204
+ def find_logFile_in_args(args)
205
+ find_arg_in_args('-logFile', args)
206
+ end
207
+
208
+ def find_projectpath_in_args(args)
209
+ find_arg_in_args('-projectpath', args)
210
+ end
211
+
212
+ def find_arg_in_args(arg_to_find, args)
213
+ raise 'Only arguments of type array supported right now' unless args.is_a?(Array)
214
+ args.each_with_index do |arg, index|
215
+ return args[index + 1] if arg == arg_to_find && index < args.count - 1
216
+ end
217
+ nil
218
+ end
219
+
220
+ private
221
+
222
+ def pipe(file)
223
+ File.open(file, 'r') do |f|
224
+ f.extend File::Tail
225
+ f.interval = 0.1
226
+ f.max_interval = 0.4
227
+ f.backward 100
228
+ f.tail { |l| yield l }
229
+ end
230
+ end
231
+ end
232
+
233
+ class Installer
234
+ def self.create
235
+ installer = if Helper.mac?
236
+ MacInstaller.new
237
+ elsif Helper.linux?
238
+ LinuxInstaller.new
239
+ else
240
+ WindowsInstaller.new
241
+ end
242
+ if UI.interactive?
243
+ unclean = []
244
+ installer.installed.each { |unity| unclean << unity unless unity.path =~ UNITY_DIR_CHECK }
245
+ if !unclean.empty? && UI.confirm("#{unclean.count} Unity installation should be moved. Proceed?")
246
+ unclean.each { |unity| installer.sanitize_install(unity) }
247
+ end
248
+ end
249
+ installer
250
+ end
251
+
252
+ def self.install_module(file_path, version, installation_path: nil, info: {})
253
+ extension = File.extname(file_path)
254
+ if extension == '.pkg'
255
+ path = installation_path || DEFAULT_MAC_INSTALL
256
+ MacInstaller.install_pkg(
257
+ file_path,
258
+ version: version,
259
+ target_path: path
260
+ )
261
+ elsif extension == '.exe'
262
+ path = installation_path || File.join(DEFAULT_WINDOWS_INSTALL, UNITY_DIR % version)
263
+ WindowsInstaller.install_exe(
264
+ file_path,
265
+ installation_path: path,
266
+ info: info
267
+ )
268
+ elsif extension == '.sh'
269
+ path = installation_path || File.join(DEFAULT_LINUX_INSTALL, UNITY_DIR % version)
270
+ LinuxInstaller.install_sh(
271
+ file_path,
272
+ installation_path: path
273
+ )
274
+ else
275
+ raise "File type #{extension} not yet supported"
276
+ end
277
+ end
278
+ end
279
+
280
+ class MacInstaller
281
+ def sanitize_install(unity)
282
+ source_path = File.expand_path('..', unity.path)
283
+ parent = File.expand_path('..', source_path)
284
+ new_path = File.join(parent, UNITY_DIR % unity.version)
285
+ UI.important "Moving #{source_path} to #{new_path}..."
286
+ source_path = "\"#{source_path}\"" if source_path =~ / /
287
+ new_path = "\"#{new_path}\"" if new_path =~ / /
288
+ U3dCore::CommandExecutor.execute(command: "mv #{source_path} #{new_path}", admin: true)
289
+ rescue => e
290
+ UI.error "Unable to move #{source_path} to #{new_path}: #{e}"
291
+ else
292
+ UI.success "Successfully moved #{source_path} to #{new_path}"
293
+ end
294
+
295
+ def installed
296
+ unless (`mdutil -s /` =~ /disabled/).nil?
297
+ $stderr.puts 'Please enable Spotlight indexing for /Applications.'
298
+ exit(1)
299
+ end
300
+
301
+ bundle_identifiers = ['com.unity3d.UnityEditor4.x', 'com.unity3d.UnityEditor5.x']
302
+
303
+ mdfind_args = bundle_identifiers.map { |bi| "kMDItemCFBundleIdentifier == '#{bi}'" }.join(' || ')
304
+
305
+ cmd = "mdfind \"#{mdfind_args}\" 2>/dev/null"
306
+ UI.verbose cmd
307
+ versions = `#{cmd}`.split("\n").map { |path| MacInstallation.new(path: path) }
308
+
309
+ # sorting should take into account stable/patch etc
310
+ versions.sort! { |x, y| x.version <=> y.version }
311
+ end
312
+
313
+ def self.install_pkg(file_path, version: nil, target_path: nil)
314
+ target_path ||= DEFAULT_MAC_INSTALL
315
+ command = "installer -pkg #{file_path.shellescape} -target #{target_path.shellescape}"
316
+ unity = Installer.create.installed.find { |u| u.version == version }
317
+ if unity.nil?
318
+ UI.verbose "No Unity install for version #{version} was found"
319
+ U3dCore::CommandExecutor.execute(command: command, admin: true)
320
+ else
321
+ begin
322
+ path = File.expand_path('..', unity.path)
323
+ temp_path = File.join(target_path, 'Applications', 'Unity')
324
+ move_to_temp = (temp_path != path)
325
+ if move_to_temp
326
+ UI.verbose "Temporary switching location of #{path} to #{temp_path} for installation purpose"
327
+ FileUtils.mv path, temp_path
328
+ end
329
+ U3dCore::CommandExecutor.execute(command: command, admin: true)
330
+ ensure
331
+ FileUtils.mv temp_path, path if move_to_temp
332
+ end
333
+ end
334
+ rescue => e
335
+ UI.error "Failed to install pkg at #{file_path}: #{e}"
336
+ else
337
+ UI.success "Successfully installed package from #{file_path}"
338
+ end
339
+ end
340
+
341
+ class LinuxInstaller
342
+ def sanitize_install(unity)
343
+ source_path = File.expand_path(unity.path)
344
+ parent = File.expand_path('..', source_path)
345
+ new_path = File.join(parent, UNITY_DIR % unity.version)
346
+ UI.important "Moving #{source_path} to #{new_path}..."
347
+ source_path = "\"#{source_path}\"" if source_path =~ / /
348
+ new_path = "\"#{new_path}\"" if new_path =~ / /
349
+ U3dCore::CommandExecutor.execute(command: "mv #{source_path} #{new_path}", admin: true)
350
+ rescue => e
351
+ UI.error "Unable to move #{source_path} to #{new_path}: #{e}"
352
+ else
353
+ UI.success "Successfully moved #{source_path} to #{new_path}"
354
+ end
355
+
356
+ def installed
357
+ find = File.join(DEFAULT_LINUX_INSTALL, 'Unity*')
358
+ versions = Dir[find].map { |path| LinuxInstallation.new(path: path) }
359
+
360
+ # sorting should take into account stable/patch etc
361
+ versions.sort! { |x, y| x.version <=> y.version }
362
+ end
363
+
364
+ def self.install_sh(file, installation_path: nil)
365
+ cmd = file.shellescape
366
+ if installation_path
367
+ Utils.ensure_dir(installation_path)
368
+ U3dCore::CommandExecutor.execute(command: "cd #{installation_path}; #{cmd}", admin: true)
369
+ else
370
+ U3dCore::CommandExecutor.execute(command: cmd, admin: true)
371
+ end
372
+ rescue => e
373
+ UI.error "Failed to install bash file at #{file_path}: #{e}"
374
+ else
375
+ UI.success 'Installation successful'
376
+ end
377
+ end
378
+
379
+ class WindowsInstaller
380
+ def sanitize_install(unity)
381
+ source_path = File.expand_path(unity.path)
382
+ parent = File.expand_path('..', source_path)
383
+ new_path = File.join(parent, UNITY_DIR % unity.version)
384
+ UI.important "Moving #{source_path} to #{new_path}..."
385
+ source_path.tr!('/', '\\')
386
+ new_path.tr!('/', '\\')
387
+ source_path = "\"" + source_path + "\"" if source_path =~ / /
388
+ new_path = "\"" + new_path + "\"" if new_path =~ / /
389
+ U3dCore::CommandExecutor.execute(command: "move #{source_path} #{new_path}", admin: true)
390
+ rescue => e
391
+ UI.error "Unable to move #{source_path} to #{new_path}: #{e}"
392
+ else
393
+ UI.success "Successfully moved #{source_path} to #{new_path}"
394
+ end
395
+
396
+ def installed
397
+ find = File.join(DEFAULT_WINDOWS_INSTALL, 'Unity*', 'Editor', 'Uninstall.exe')
398
+ versions = Dir[find].map { |path| WindowsInstallation.new(path: File.expand_path('../..', path)) }
399
+
400
+ # sorting should take into account stable/patch etc
401
+ versions.sort! { |x, y| x.version <=> y.version }
402
+ end
403
+
404
+ def self.install_exe(file_path, installation_path: nil, info: {})
405
+ installation_path ||= DEFAULT_WINDOWS_INSTALL
406
+ final_path = installation_path.tr('/', '\\')
407
+ Utils.ensure_dir(final_path)
408
+ begin
409
+ command = nil
410
+ if info['cmd']
411
+ command = info['cmd']
412
+ command.sub!(/{FILENAME}/, file_path)
413
+ command.sub!(/{INSTDIR}/, final_path)
414
+ command.sub!(/{DOCDIR}/, final_path)
415
+ command.sub!(/{MODULEDIR}/, final_path)
416
+ command.sub!(/\/D=/, '/S /D=') unless /\/S/ =~ command
417
+ end
418
+ command ||= file_path.to_s
419
+ U3dCore::CommandExecutor.execute(command: command, admin: true)
420
+ rescue => e
421
+ UI.error "Failed to install exe at #{file_path}: #{e}"
422
+ else
423
+ UI.success "Successfully installed #{info['title']}"
424
+ end
425
+ end
426
+ end
427
+
428
+ class UnityProject
429
+ attr_reader :path
430
+
431
+ def initialize(path)
432
+ @path = path
433
+ end
434
+
435
+ def exist?
436
+ Dir.exist?("#{@path}/Assets") && Dir.exist?("#{@path}/ProjectSettings")
437
+ end
438
+
439
+ def editor_version
440
+ require 'yaml'
441
+ yaml = YAML.load(File.read("#{@path}/ProjectSettings/ProjectVersion.txt"))
442
+ yaml['m_EditorVersion']
443
+ end
444
+ end
445
+ end