# frozen_string_literal: true ## --- BEGIN LICENSE BLOCK --- # Copyright (c) 2016-present WeWantToKnow AS # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. ## --- END LICENSE BLOCK --- require 'u3d/utils' require 'u3d_core/admin_tools' require 'u3d_core/core_ext/string' require 'u3d/installation' require 'fileutils' require 'file-tail' require 'pathname' require 'zip' module U3d DEFAULT_LINUX_INSTALL = '/opt/' DEFAULT_MAC_INSTALL = '/' DEFAULT_WINDOWS_INSTALL = 'C:/Program Files/' UNITY_DIR = "Unity_%s" UNITY_DIR_LONG = "Unity_%s_%s" UNITY_DIR_LINUX = "unity-editor-%s" UNITY_DIR_LINUX_LONG = "unity-editor-%s_%s" class Installer def self.create if Helper.mac? MacInstaller.new elsif Helper.linux? LinuxInstaller.new else WindowsInstaller.new end end def self.sanitize_installs(installer) installer.sanitize_installs end def self.install_modules(files, version, installation_path: nil) installer = Installer.create files.each do |name, file, info| UI.header "Installing #{info.name} (#{name})" UI.message "Installing with #{file}" installer.install(file, version, installation_path: installation_path, info: info) end end def self.uninstall(unity: nil) installer = Installer.create installer.uninstall(unity: unity) end end class BaseInstaller def sanitize_installs return unless UI.interactive? || Helper.test? unclean = [] installed.each { |unity| unclean << unity unless unity.clean_install? } return if unclean.empty? UI.important("u3d can optionally standardize the existing Unity installation names and locations.") UI.important("Check the documentation for more information:") UI.important("** https://github.com/DragonBox/u3d/blob/master/README.md#default-installation-paths **") unclean.each { |unity| sanitize_install(unity, dry_run: true) } return unless UI.confirm("#{unclean.count} Unity installation(s) will be moved. Proceed??") unclean.each { |unity| sanitize_install(unity) } end def installed_sorted_by_versions list = installed return [] if list.empty? list.sort { |a, b| UnityVersionComparator.new(a.version) <=> UnityVersionComparator.new(b.version) } end protected def install_po(file_path, version, info: nil) unity = installed.find { |u| u.version == version } root_path = package_destination(info, unity.root_path) target_path = File.join(root_path, File.basename(file_path)) Utils.ensure_dir(File.dirname(target_path)) UI.verbose "Copying #{file_path} to #{target_path}" FileUtils.cp(file_path, target_path) UI.success "Successfully installed language file #{File.basename(file_path)}" end def install_zip(file_path, version, info: nil) unity = installed.find { |u| u.version == version } root_path = package_destination(info, unity.root_path) UI.verbose("Unzipping #{file_path} to #{root_path}") unless File.directory?(root_path) Utils.get_write_access(File.dirname(root_path)) do Utils.ensure_dir(root_path) end end Zip::File.open(file_path) do |zip_file| zip_file.each do |entry| target_path = File.join(root_path, entry.name) Utils.ensure_dir(File.dirname(target_path)) zip_file.extract(entry, target_path) unless File.exist?(target_path) end end if info&.rename_from && info&.rename_to rename_from = info.rename_from.gsub(/{UNITY_PATH}/, unity.root_path) rename_to = info.rename_to.gsub(/{UNITY_PATH}/, unity.root_path) Utils.ensure_dir(rename_to) UI.verbose("Renaming from #{rename_from} to #{rename_to}") if File.file? rename_from FileUtils.mv(rename_from, rename_to) else Dir.glob("#{rename_from}/*").each { |path| FileUtils.mv(path, rename_to) } end end UI.success "Successfully unizpped #{File.basename(file_path)} at #{root_path}" end def package_destination(info, unity_root_path) if info&.destination info.destination.gsub(/{UNITY_PATH}/, unity_root_path) else unity_root_path end end # extra installation paths are stored in U3D_EXTRA_PATHS environment variable, # following a standard PATH variable format. # Returns an array of ruby style paths def extra_installation_paths return [] if ENV['U3D_EXTRA_PATHS'].nil? ENV['U3D_EXTRA_PATHS'].strip.split(File::PATH_SEPARATOR).map { |p| File.expand_path p } end def find_installations_with_path(default_root_path: '', postfix: [], &block) ([default_root_path] | extra_installation_paths).map do |path| UI.verbose "Looking for installed Unity version under #{path}" pattern = File.join([path] + postfix) Dir.glob(pattern).map(&block) end.flatten end end # deprecated class CommonInstaller def self.sanitize_install(source_path, new_path, command, dry_run: false) UI.deprecated("Use U3dCore::AdminTools.move_files") U3dCore::AdminTools.move_file(source_path, new_path, command, dry_run: dry_run) end end class MacInstaller < BaseInstaller def sanitize_install(unity, long: false, dry_run: false) source_path = unity.root_path parent = File.expand_path('..', source_path) dir_name = format(long ? UNITY_DIR_LONG : UNITY_DIR, version: unity.version, build_number: unity.build_number) new_path = File.join(parent, dir_name) moved = U3dCore::AdminTools.move_os_file(:mac, source_path, new_path, dry_run: dry_run) unity.root_path = new_path if moved && !dry_run end def installed paths = (list_installed_paths + spotlight_installed_paths).uniq paths.map { |path| MacInstallation.new(root_path: path) } end def install(file_path, version, installation_path: nil, info: nil) extension = File.extname(file_path) raise "Installation of #{extension} files is not supported on Mac" unless %w[.zip .po .pkg .dmg].include? extension path = installation_path || DEFAULT_MAC_INSTALL case extension when '.po' install_po(file_path, version, info: info) when '.zip' install_zip(file_path, version, info: info) when '.dmg' UI.important "Skipping installation of #{file_path} for now" else install_pkg(file_path, version: version, target_path: path) end end def install_pkg(file_path, version: nil, target_path: nil) target_path ||= DEFAULT_MAC_INSTALL command = "installer -pkg #{file_path.shellescape} -target #{target_path.shellescape}" unity = installed.find { |u| u.version == version } temp_path = File.join(target_path, 'Applications', 'Unity') if unity.nil? UI.verbose "No Unity install for version #{version} was found" U3dCore::CommandExecutor.execute(command: command, admin: true) destination_path = File.join(target_path, 'Applications', format(UNITY_DIR, version: version)) FileUtils.mv temp_path, destination_path else UI.verbose "Unity install for version #{version} found under #{unity.root_path}" begin path = unity.root_path move_to_temp = (temp_path != path) if move_to_temp UI.verbose "Temporary switching location of #{path} to #{temp_path} for installation purpose" FileUtils.mv path, temp_path end U3dCore::CommandExecutor.execute(command: command, admin: true) ensure FileUtils.mv temp_path, path if move_to_temp end end rescue StandardError => e UI.error "Failed to install pkg at #{file_path}: #{e}" else UI.success "Successfully installed package from #{file_path}" end def uninstall(unity: nil) UI.verbose("Uninstalling Unity at '#{unity.root_path}'...") command = "rm -r #{unity.root_path.argescape}" U3dCore::CommandExecutor.execute(command: command, admin: true) rescue StandardError => e UI.error "Failed to uninstall unity at #{unity.path}: #{e}" else UI.success "Successfully uninstalled '#{unity.root_path}'" end private def list_installed_paths paths = find_installations_with_path( default_root_path: DEFAULT_MAC_INSTALL, postfix: %w[ Applications Unity* Unity.app ] ) { |u| Pathname.new(u).parent.to_s } UI.verbose "Found list_installed_paths: #{paths}" paths end def spotlight_installed_paths unless (`mdutil -s /` =~ /disabled/).nil? UI.important 'Please enable Spotlight indexing for /Applications.' return [] end bundle_identifiers = ['com.unity3d.UnityEditor4.x', 'com.unity3d.UnityEditor5.x'] mdfind_args = bundle_identifiers.map { |bi| "kMDItemCFBundleIdentifier == '#{bi}'" }.join(' || ') cmd = "mdfind \"#{mdfind_args}\" 2>/dev/null" UI.verbose cmd paths = `#{cmd}`.split("\n") paths = paths.map { |u| Pathname.new(u).parent.to_s } UI.verbose "Found spotlight_installed_paths: #{paths}" paths end end # rubocop:disable Metrics/ClassLength class LinuxInstaller < BaseInstaller def sanitize_install(unity, long: false, dry_run: false) source_path = File.expand_path(unity.root_path) parent = File.expand_path('..', source_path) dir_name = format(long ? UNITY_DIR_LINUX_LONG : UNITY_DIR_LINUX, version: unity.version, build_number: unity.build_number) new_path = File.join(parent, dir_name) moved = U3dCore::AdminTools.move_os_file(:linux, source_path, new_path, dry_run: dry_run) unity.root_path = new_path if moved && !dry_run end def installed paths = (list_installed_paths + debian_installed_paths).uniq paths.map { |path| LinuxInstallation.new(root_path: path) } end def install(file_path, version, installation_path: nil, info: nil) extension = File.extname(file_path) raise "Installation of #{extension} files is not supported on Linux" unless ['.zip', '.po', '.sh', '.xz', '.pkg'].include? extension case extension when '.sh' path = installation_path || DEFAULT_LINUX_INSTALL install_sh(file_path, installation_path: path) when '.xz' new_path = File.join(DEFAULT_LINUX_INSTALL, format(UNITY_DIR_LINUX, version: version)) path = installation_path || new_path install_xz(file_path, installation_path: path) when '.pkg' new_path = File.join(DEFAULT_LINUX_INSTALL, format(UNITY_DIR_LINUX, version: version)) path = installation_path || new_path install_pkg(file_path, installation_path: path) when '.po' install_po(file_path, version, info: info) when '.zip' install_zip(file_path, version, info: info) end # Forces sanitation for installation of 'weird' versions eg 5.6.1xf1Linux unity = installed.select { |u| u.version == version }.first if unity sanitize_install(unity) else UI.error "Unity was not installed properly" end end def install_sh(file, installation_path: nil) cmd = file.shellescape U3dCore::CommandExecutor.execute(command: "chmod a+x #{cmd}") if installation_path command = "cd #{installation_path.shellescape}; #{cmd}" command = "mkdir -p #{installation_path.shellescape}; #{command}" unless File.directory? installation_path U3dCore::CommandExecutor.execute(command: command, admin: true) else U3dCore::CommandExecutor.execute(command: cmd, admin: true) end rescue StandardError => e UI.error "Failed to install sh file #{file} at #{installation_path}: #{e}" else UI.success 'Installation successful' end def install_xz(file, installation_path: nil) raise 'Missing installation_path' unless installation_path command = "cd #{installation_path.shellescape}; tar xf #{file.shellescape}" command = "mkdir -p #{installation_path.shellescape}; #{command}" unless File.directory? installation_path U3dCore::CommandExecutor.execute(command: command, admin: true) rescue StandardError => e UI.error "Failed to install xz file #{file} at #{installation_path}: #{e}" else UI.success 'Installation successful' end def install_pkg(file, installation_path: nil) raise 'Missing installation_path' unless installation_path raise 'Only able to install pkg on top of existing Unity installs' unless File.exist? installation_path raise 'Missing 7z' if `which 7z`.empty? Dir.mktmpdir do |tmp_dir| UI.verbose "Working in tmp dir #{tmp_dir}" command = "7z -aos -t* -o#{tmp_dir.shellescape} e #{file.shellescape}" U3dCore::CommandExecutor.execute(command: command) target_location = pkg_install_path(installation_path, "#{tmp_dir}/PackageInfo") # raise "Path for #{target_location} already exists" if path File.exist? target_location command = "cd #{target_location.shellescape}; gzip -dc #{tmp_dir}/Payload | cpio -i '*' -" command = "mkdir -p #{target_location.shellescape}; #{command}" # unless File.directory? installation_path U3dCore::CommandExecutor.execute(command: command, admin: true) end rescue StandardError => e UI.verbose(e.backtrace.join("\n")) UI.error "Failed to install pkg file #{file} at #{installation_path}: #{e}" else UI.success 'Installation successful' end def uninstall(unity: nil) UI.verbose("Uninstalling Unity at '#{unity.root_path}'...") command = "rm -r #{unity.root_path}" U3dCore::CommandExecutor.execute(command: command, admin: true) rescue StandardError => e UI.error "Failed to uninstall unity at #{unity.path}: #{e}" else UI.success "Successfully uninstalled '#{unity.root_path}'" end private def pkg_install_path(unity_root_path, pinfo_path) raise "PackageInfo not found under #{pinfo_path}" unless File.exist? pinfo_path pinfo = File.read(pinfo_path) require 'rexml/document' d = REXML::Document.new(pinfo) identifier = d.root.attributes['identifier'] case identifier when 'com.unity3d.Documentation' "#{unity_root_path}/Editor/Data/" when 'com.unity3d.StandardAssets' "#{unity_root_path}/Editor/Standard Assets/" when 'com.unity3d.ExampleProject' unity_root_path else install_location = d.root.attributes['install-location'] raise "Not sure how to install this module with identifier #{identifier} install-location: #{install_location}" unless install_location.start_with? '/Applications/Unity/' install_location.gsub(%(/Applications/Unity), "#{unity_root_path}/Editor/Data") end end def list_installed_paths paths = find_installations_with_path( default_root_path: DEFAULT_LINUX_INSTALL, postfix: %w[ unity-editor-* Editor ] ) { |u| Pathname.new(u).parent.to_s } UI.verbose "Found list_installed_paths: #{paths}" paths end def debian_installed_paths paths = find_installations_with_path( default_root_path: DEFAULT_LINUX_INSTALL, postfix: %w[ Unity Editor ] ) { |u| Pathname.new(u).parent.to_s } UI.verbose "Found debian_installed_paths: #{paths}" paths end end # rubocop:enable Metrics/ClassLength class WindowsInstaller < BaseInstaller def sanitize_install(unity, long: false, dry_run: false) source_path = File.expand_path(unity.root_path) parent = File.expand_path('..', source_path) dir_name = format(long ? UNITY_DIR_LONG : UNITY_DIR, version: unity.version, build_number: unity.build_number) new_path = File.join(parent, dir_name) moved = U3dCore::AdminTools.move_os_file(:win, source_path, new_path, dry_run: dry_run) unity.root_path = new_path if moved && !dry_run end def installed find_installations_with_path( default_root_path: DEFAULT_WINDOWS_INSTALL, postfix: %w[ Unity* Editor Uninstall.exe ] ) { |path| WindowsInstallation.new(root_path: File.expand_path('../..', path)) } end def install(file_path, version, installation_path: nil, info: nil) extension = File.extname(file_path) raise "Installation of #{extension} files is not supported on Windows" unless %w[.po .zip .exe .msi].include? extension path = installation_path || File.join(DEFAULT_WINDOWS_INSTALL, format(UNITY_DIR, version: version)) case extension when '.po' install_po(file_path, version, info: info) when '.zip' install_zip(file_path, version, info: info) else install_exe(file_path, installation_path: path, info: info) end end def install_exe(file_path, installation_path: nil, info: nil) installation_path ||= DEFAULT_WINDOWS_INSTALL final_path = U3dCore::Helper.windows_path(installation_path) Utils.ensure_dir(final_path) begin command = nil if info.command command = info.command if /msiexec/ =~ command command.sub!(/{FILENAME}/, "\"#{U3dCore::Helper.windows_path(file_path)}\"") else command.sub!(/{FILENAME}/, file_path.argescape) end command.sub!(/{INSTDIR}/, final_path) command.sub!(/{DOCDIR}/, final_path) command.sub!(/{MODULEDIR}/, final_path) command.sub!(%r{/D=}, '/S /D=') unless %r{/S} =~ command end command ||= file_path.argescape U3dCore::CommandExecutor.execute(command: command, admin: true) rescue StandardError => e UI.error "Failed to install package at #{file_path}: #{e}" else UI.success "Successfully installed #{info.name}" end end def uninstall(unity: nil) UI.verbose("Uninstalling Unity at '#{unity.root_path}'...") uninstall_exe = File.join(unity.root_path, 'Editor', 'Uninstall.exe') command = "#{uninstall_exe.argescape} /S" UI.message("Although the uninstall process completed, it takes a few seconds before the files are actually removed") U3dCore::CommandExecutor.execute(command: command, admin: true) rescue StandardError => e UI.error "Failed to uninstall unity at #{unity.path}: #{e}" else UI.success "Successfully uninstalled '#{unity.root_path}'" end end class LinuxDependencies # see https://forum.unity3d.com/threads/unity-on-linux-release-notes-and-known-issues.350256/ DEPENDENCIES = [ 'gconf-service', 'lib32gcc1', 'lib32stdc++6', 'libasound2', 'libc6', 'libc6-i386', 'libcairo2', 'libcap2', 'libcups2', 'libdbus-1-3', 'libexpat1', 'libfontconfig1', 'libfreetype6', 'libgcc1', 'libgconf-2-4', 'libgdk-pixbuf2.0-0', 'libgl1-mesa-glx', 'libglib2.0-0', 'libglu1-mesa', 'libgtk2.0-0', 'libnspr4', 'libnss3', 'libpango1.0-0', 'libstdc++6', 'libx11-6', 'libxcomposite1', 'libxcursor1', 'libxdamage1', 'libxext6', 'libxfixes3', 'libxi6', 'libxrandr2', 'libxrender1', 'libxtst6', 'zlib1g', 'debconf', 'npm', 'libpq5' # missing from original list ].freeze def self.install if `which dpkg` != '' prefix = 'apt-get -y install' elsif `which rpm` != '' prefix = 'yum -y install' else raise 'Cannot install dependencies on your Linux distribution' end return if UI.interactive? && !(UI.confirm "Install dependencies? (#{DEPENDENCIES.length} dependency(ies) to install)") U3dCore::CommandExecutor.execute(command: "#{prefix} #{DEPENDENCIES.join(' ')}", admin: true) end end end