def _check_binary_dependencies(self): """Check if all required binaries are installed on the system. This reads a `require-binaries` entry in the script, parsed the same way as the `requires` entry. """ binary_dependencies = unpack_dependencies(self.installer.script.get("require-binaries")) for dependency in binary_dependencies: if isinstance(dependency, tuple): installed_binaries = { dependency_option: bool(system.find_executable(dependency_option)) for dependency_option in dependency } if not any(installed_binaries.values()): raise ScriptingError("This installer requires %s on your system" % " or ".join(dependency)) else: if not system.find_executable(dependency): raise ScriptingError("This installer requires %s on your system" % dependency)
def check_md5(self, data): """Checks compare the MD5 checksum of `file` and compare it to `value`""" self._check_required_params(['file', 'value'], data, 'check_md5') filename = self._substitute(data['file']) hash_string = self._killable_process(system.get_md5_hash, filename) if hash_string != data['value']: raise ScriptingError("MD5 checksum mismatch", data) self._iter_commands()
def _monitor_task(self, command): if not command.is_running: logger.debug("Return code: %s", command.return_code) if command.return_code != "0": raise ScriptingError("Command exited with code %s", command.return_code) self._iter_commands() return False return True
def validate_scripts(self): """Auto-fixes some script aspects and checks for mandatory fields""" for script in self.scripts: for item in ["description", "notes"]: script[item] = script.get(item) or "" for item in ["name", "runner", "version"]: if item not in script: logger.error("Invalid script: %s", script) raise ScriptingError('Missing field "%s" in install script' % item)
def get_runner_class(self, runner_name): """Runner the runner class from its name""" try: runner = import_runner(runner_name) except InvalidRunner as err: GLib.idle_add(self.parent.cancel_button.set_sensitive, True) raise ScriptingError( _("Invalid runner provided %s") % runner_name) from err return runner
def _check_required_params(params, command_data, command_name): """Verify presence of a list of parameters required by a command.""" if isinstance(params, str): params = [params] for param in params: if isinstance(param, tuple): param_present = any(key in command_data for key in param) if not param_present: raise ScriptingError( "One of %s parameter is mandatory for the %s command" % (" or ".join(param), command_name), command_data, ) else: if param not in command_data: raise ScriptingError( "The %s parameter is mandatory for the %s command" % (param, command_name), command_data, )
def install_runner(self, runner): logger.debug('Installing %s', runner.name) try: runner.install(version=self._get_runner_version(), downloader=self.parent.start_download, callback=self.install_runners) except (NonInstallableRunnerError, RunnerInstallationError) as ex: logger.error(ex.message) raise ScriptingError(ex.message)
def _check_required_params(self, params, command_data, command_name): """Verify presence of a list of parameters required by a command.""" if type(params) is str: params = [params] for param in params: if param not in command_data: raise ScriptingError( 'The "%s" parameter is mandatory for ' 'the %s command' % (param, command_name), command_data)
def _map_command(self, command_data): """Map a directive from the `installer` section to an internal method.""" command_name, command_params = self._get_command_name_and_params( command_data) if not hasattr(self, command_name): raise ScriptingError('The command "%s" does not exist.' % command_name) return getattr(self, command_name), command_params
def _append_steam_data_to_files(self, runner_class): data_path = self._get_steam_game_path(runner_class) if not data_path or not os.path.exists(data_path): raise ScriptingError("Unable to get Steam data for game") self.game_files[self.steam_data['file_id']] = os.path.abspath( os.path.join( data_path, self.steam_data['steam_rel_path'] ) ) self.iter_game_files()
def on_download_complete(self, _widget, _data, callback=None, callback_data=None): """Action called on a completed download.""" if callback: try: callback(**callback_data) except Exception as ex: # pylint: disable:broad-except raise ScriptingError(str(ex)) self.interpreter.abort_current_task = None self.interpreter.iter_game_files()
def check_hash(self, checksum, dest_file, dest_file_uri): """Checks the checksum of `file` and compare it to `value` Args: checksum (str): The checksum to look for (type:hash) dest_file (str): The path to the destination file dest_file_uri (str): The uri for the destination file """ try: hash_type, expected_hash = checksum.split(':', 1) except ValueError: raise ScriptingError( "Invalid checksum, expected format (type:hash) ", dest_file_uri) if system.get_file_checksum(dest_file, hash_type) != expected_hash: raise ScriptingError( hash_type.capitalize() + " checksum mismatch ", dest_file_uri)
def _check_required_params(params, command_data, command_name): """Verify presence of a list of parameters required by a command.""" if isinstance(params, str): params = [params] for param in params: if isinstance(param, tuple): param_present = False for key in param: if key in command_data: param_present = True if not param_present: raise ScriptingError( 'One of %s parameter is mandatory for the %s command' % (' or '.join(param), command_name), command_data) else: if param not in command_data: raise ScriptingError( 'The %s parameter is mandatory for the %s command' % (param, command_name), command_data)
def _download_file(self, game_file): """Download a file referenced in the installer script. Game files can be either a string, containing the location of the file to fetch or a dict with the following keys: - url : location of file, if not present, filename will be used this should be the case for local files. - filename : force destination filename when url is present or path of local file. """ # Setup file_id, file_uri and local filename file_id = list(game_file.keys())[0] if isinstance(game_file[file_id], dict): filename = game_file[file_id]['filename'] file_uri = game_file[file_id]['url'] else: file_uri = game_file[file_id] filename = os.path.basename(file_uri) if not filename: raise ScriptingError("No filename provided, please provide 'url' and 'filename' parameters in the script") if file_uri.startswith("/"): file_uri = "file://" + file_uri elif file_uri.startswith(("$WINESTEAM", "$STEAM")): # Download Steam data self._download_steam_data(file_uri, file_id) return # Check for file availability in PGA pga_uri = pga.check_for_file(self.game_slug, file_id) if pga_uri: file_uri = pga_uri # Setup destination path dest_file = os.path.join(self.cache_path, filename) logger.debug("Downloading [%s]: %s to %s", file_id, file_uri, dest_file) if file_uri.startswith("N/A"): # Ask the user where the file is located parts = file_uri.split(":", 1) if len(parts) == 2: message = parts[1] else: message = "Please select file '%s'" % file_id self.current_file_id = file_id self.parent.ask_user_for_file(message) return if os.path.exists(dest_file): os.remove(dest_file) # Change parent's status self.parent.set_status('') self.game_files[file_id] = dest_file self.parent.start_download(file_uri, dest_file)
def execute(self, data): """Run an executable file.""" args = [] terminal = None working_dir = None if isinstance(data, dict): self._check_required_params('file', data, 'execute') file_ref = data['file'] args_string = data.get('args', '') for arg in shlex.split(args_string): args.append(self._substitute(arg)) terminal = data.get('terminal') working_dir = data.get('working_dir') else: file_ref = data # Determine whether 'file' value is a file id or a path exec_path = self._get_file(file_ref) or self._substitute(file_ref) if not exec_path: raise ScriptingError("Unable to find file %s" % file_ref, file_ref) if not os.path.exists(exec_path): raise ScriptingError("Unable to find required executable", exec_path) if not os.access(exec_path, os.X_OK): self.chmodx(exec_path) if terminal: terminal = system.get_default_terminal() if not working_dir or not os.path.exists(working_dir): working_dir = self.target_path command = [exec_path] + args logger.debug("Executing %s" % command) thread = LutrisThread(command, env=runtime.get_env(), term=terminal, cwd=self.target_path, watch=False) self.abort_current_task = thread.killall thread.run() self.abort_current_task = None
def url(self): _url = "" if isinstance(self._file_meta, dict): if "url" not in self._file_meta: raise ScriptingError(_("missing field `url` for file `%s`") % self.id) _url = self._file_meta["url"] else: _url = self._file_meta if _url.startswith("/"): return "file://" + _url return _url
def on_files_confirmed(self, _button, file_box): """Call this when the user confirms the install files This will start the downloads. """ self.set_status("") self.continue_button.set_sensitive(False) try: file_box.start_all() self.continue_button.disconnect(self.continue_handler) except PermissionError as ex: self.continue_button.set_sensitive(True) raise ScriptingError("Unable to get files: %s" % ex)
def install_runner(self, runner): """Install runner required by the install script""" logger.debug("Installing %s", runner.name) try: runner.install( version=self._get_runner_version(), downloader=simple_downloader, callback=self.install_runners, ) except (NonInstallableRunnerError, RunnerInstallationError) as ex: logger.error(ex.message) raise ScriptingError(ex.message) from ex
def rename(self, params): """Rename file or folder.""" self._check_required_params(['src', 'dst'], params, 'rename') src, dst = self._get_move_paths(params) if not os.path.exists(src): raise ScriptingError( "Rename error, source path does not exist: %s" % src) if os.path.isdir(dst): os.rmdir(dst) # Remove if empty if os.path.exists(dst): raise ScriptingError( "Rename error, destination already exists: %s" % src) dst_dir = os.path.dirname(dst) # Pre-move on dest filesystem to avoid error with # os.rename through different filesystems temp_dir = os.path.join(dst_dir, "lutris_rename_temp") os.makedirs(temp_dir) self._killable_process(shutil.move, src, temp_dir) src = os.path.join(temp_dir, os.path.basename(src)) os.renames(src, dst)
def filename(self): if isinstance(self._file_meta, dict): if "filename" not in self._file_meta: raise ScriptingError(_("missing field `filename` in file `%s`") % self.id) return self._file_meta["filename"] if self._file_meta.startswith("N/A"): if self.uses_pga_cache() and os.path.isdir(self.cache_path): return self.cached_filename return "" if self.url.startswith("$STEAM"): return self.url return os.path.basename(self._file_meta)
def choose_installer(self): """Stage where we choose an install script.""" self.title_label.set_markup('<b>Select which version to install</b>') self.installer_choice_box = Gtk.VBox() self.installer_choice = 0 radio_group = None # Build list for index, script in enumerate(self.scripts): for item in ['description', 'notes']: script[item] = script.get(item) or '' for item in ['runner', 'version']: if item not in script: raise ScriptingError('Missing field "%s" in install script' % item) runner = script['runner'] version = script['version'] label = "{} ({})".format(version, runner) btn = Gtk.RadioButton.new_with_label_from_widget(radio_group, label) btn.connect('toggled', self.on_installer_toggled, index) self.installer_choice_box.pack_start(btn, False, False, 10) if not radio_group: radio_group = btn def _create_label(padding, text): label = Gtk.Label() label.set_max_width_chars(60) label.set_line_wrap(True) label.set_line_wrap_mode(Pango.WrapMode.WORD_CHAR) label.set_alignment(0, .5) label.set_margin_left(50) label.set_margin_right(50) label.set_markup(text) self.installer_choice_box.pack_start(label, True, True, padding) return label self.description_label = _create_label( 10, "<i><b>{}</b></i>".format(self.scripts[0]['description']) ) self.notes_label = _create_label( 5, "<i>{}</i>".format(self.scripts[0]['notes']) ) self.widget_box.pack_start(self.installer_choice_box, False, False, 10) self.installer_choice_box.show_all() self.continue_button.grab_focus() self.continue_button.show() self.continue_handler = self.continue_button.connect( 'clicked', self.on_installer_selected )
def _check_required_params(params, command_data, command_name): """Verify presence of a list of parameters required by a command.""" if isinstance(params, str): params = [params] for param in params: if isinstance(param, tuple): param_present = False for key in param: if key in command_data: param_present = True if not param_present: raise ScriptingError( _("One of {params} parameter is mandatory for the {cmd} command").format( params=_(" or ").join(param), cmd=command_name), command_data, ) else: if param not in command_data: raise ScriptingError( _("The {param} parameter is mandatory for the {cmd} command").format( param=param, cmd=command_name), command_data, )
def create_game_folder(self): """Create the game folder if needed and store if is was created""" if (self.installer.files and self.target_path and not system.path_exists(self.target_path) and self.installer.creates_game_folder): try: logger.debug("Creating destination path %s", self.target_path) os.makedirs(self.target_path) self.game_dir_created = True except PermissionError: raise ScriptingError( "Lutris does not have the necessary permissions to install to path:", self.target_path, )
def swap_gog_game_files(self): if not self.gogid: raise ScriptingError("The installer has no GOG ID!") links = self.get_gog_download_links() installer_file_id = None if links: for index, file in enumerate(self.files): file_id = list(file.keys())[0] file_meta = file[file_id] if ((isinstance(file_meta, str) and file_meta.startswith("N/A")) or (isinstance(file_meta, dict) and file_meta.get('url', '').startswith('N/A'))): logger.debug("Removing file %s", file_id) self.files.pop(index) installer_file_id = file_id break if not installer_file_id: raise ScriptingError( "Could not match a GOG installer file in the files") for index, link in enumerate(links): filename = link.split("?")[0].split("/")[-1] if filename.lower().endswith((".exe", ".sh")): file_id = installer_file_id else: file_id = "gog_file_%s" % index logger.debug("Adding GOG file %s as %s", filename, file_id) self.files.append({file_id: { "url": link, "filename": filename, }})
def _check_dependency(self): """When a game is a mod or an extension of another game, check that the base game is installed. If the game is available, install the game in the base game folder. The first game available listed in the dependencies is the one picked to base the installed on. """ if self.extends: dependencies = [self.extends] else: dependencies = strings.unpack_dependencies(self.requires) error_message = "You need to install {} before" for index, dependency in enumerate(dependencies): if isinstance(dependency, tuple): dependency_choices = [ self._get_installed_dependency(dep) for dep in dependency ] installed_games = [ dep for dep in dependency_choices if dep ] if not installed_games: raise ScriptingError( error_message.format(' or '.join(dependency)) ) if index == 0: self.target_path = installed_games[0]['directory'] self.requires = installed_games[0]['installer_slug'] else: game = self._get_installed_dependency(dependency) if not game: raise ScriptingError( error_message.format(dependency) ) if index == 0: self.target_path = game['directory'] self.requires = game['installer_slug']
def _substitute_config(self, script_config): """Substitute values such as $GAMEDIR in a config dict.""" config = {} for key in script_config: if not isinstance(key, str): raise ScriptingError("Game config key must be a string", key) value = script_config[key] if isinstance(value, list): config[key] = [self._substitute(i) for i in value] elif isinstance(value, dict): config[key] = {k: self._substitute(v) for (k, v) in value.items()} elif isinstance(value, bool): config[key] = value else: config[key] = self._substitute(value) return config
def on_runners_ready(self, _widget=None): """The runners are ready, proceed with file selection""" if self.interpreter.extras is None: extras = self.interpreter.get_extras() if extras: self.show_extras(extras) return try: self.interpreter.installer.prepare_game_files() except UnavailableGame as ex: raise ScriptingError(str(ex)) if not self.interpreter.installer.files: logger.debug("Installer doesn't require files") self.interpreter.launch_installer_commands() return self.show_installer_files_screen()
def write_file(self, params): """Write text to a file.""" self._check_required_params(["file", "content"], params, "write_file") # Get file dest_file_path = self._get_file(params["file"]) # Create dir if necessary basedir = os.path.dirname(dest_file_path) os.makedirs(basedir, exist_ok=True) mode = params.get("mode", "w") if not mode.startswith(("a", "w")): raise ScriptingError(_("Wrong value for write_file mode: '%s'") % mode) with open(dest_file_path, mode, encoding='utf-8') as dest_file: dest_file.write(self._substitute(params["content"]))
def get_game_config(self): """Return the game configuration""" if self.requires: # Load the base game config required_game = get_game_by_field(self.requires, field="installer_slug") if not required_game: required_game = get_game_by_field(self.requires, field="slug") if not required_game: raise ValueError( "No game matched '%s' on installer_slug or slug" % self.requires) base_config = LutrisConfig( runner_slug=self.runner, game_config_id=required_game["configpath"]) config = base_config.game_level else: config = {"game": {}} # Config update if "system" in self.script: config["system"] = self._substitute_config(self.script["system"]) if self.runner in self.script and self.script[self.runner]: config[self.runner] = self._substitute_config( self.script[self.runner]) launcher, launcher_config = self.get_game_launcher_config( self.interpreter.game_files) if launcher: config["game"][launcher] = launcher_config if "game" in self.script: try: config["game"].update(self.script["game"]) except ValueError: raise ScriptingError("Invalid 'game' section", self.script["game"]) config["game"] = self._substitute_config(config["game"]) if AUTO_ELF_EXE in config["game"].get("exe", ""): config["game"]["exe"] = find_linux_game_executable( self.interpreter.target_path, make_executable=True) elif AUTO_WIN32_EXE in config["game"].get("exe", ""): config["game"]["exe"] = find_windows_game_executable( self.interpreter.target_path) return config
def write_file(self, params): """Write text to a file.""" self._check_required_params(['file', 'content'], params, 'write_file') # Get file dest_file_path = self._get_file(params['file']) # Create dir if necessary basedir = os.path.dirname(dest_file_path) if not os.path.exists(basedir): os.makedirs(basedir) mode = params.get('mode', 'w') if not mode.startswith(('a', 'w')): raise ScriptingError("Wrong value for write_file mode: '%s'" % mode) with open(dest_file_path, mode) as dest_file: dest_file.write(self._substitute(params['content']))