u3d 0.9

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.
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