|
@@ -0,0 +1,457 @@
|
|
|
|
+/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying
|
|
|
|
+ file LICENSE.rst or https://cmake.org/licensing for details. */
|
|
|
|
+
|
|
|
|
+#include "cmCPackAppImageGenerator.h"
|
|
|
|
+
|
|
|
|
+#include <algorithm>
|
|
|
|
+#include <cctype>
|
|
|
|
+#include <cstddef>
|
|
|
|
+#include <utility>
|
|
|
|
+#include <vector>
|
|
|
|
+
|
|
|
|
+#include <fcntl.h>
|
|
|
|
+
|
|
|
|
+#include "cmsys/FStream.hxx"
|
|
|
|
+
|
|
|
|
+#include "cmCPackLog.h"
|
|
|
|
+#include "cmELF.h"
|
|
|
|
+#include "cmGeneratedFileStream.h"
|
|
|
|
+#include "cmSystemTools.h"
|
|
|
|
+#include "cmValue.h"
|
|
|
|
+
|
|
|
|
+cmCPackAppImageGenerator::cmCPackAppImageGenerator() = default;
|
|
|
|
+
|
|
|
|
+cmCPackAppImageGenerator::~cmCPackAppImageGenerator() = default;
|
|
|
|
+
|
|
|
|
+int cmCPackAppImageGenerator::InitializeInternal()
|
|
|
|
+{
|
|
|
|
+ this->SetOptionIfNotSet("CPACK_APPIMAGE_TOOL_EXECUTABLE", "appimagetool");
|
|
|
|
+ this->AppimagetoolPath = cmSystemTools::FindProgram(
|
|
|
|
+ *this->GetOption("CPACK_APPIMAGE_TOOL_EXECUTABLE"));
|
|
|
|
+
|
|
|
|
+ if (this->AppimagetoolPath.empty()) {
|
|
|
|
+ cmCPackLogger(
|
|
|
|
+ cmCPackLog::LOG_ERROR,
|
|
|
|
+ "Cannot find AppImageTool: '"
|
|
|
|
+ << *this->GetOption("CPACK_APPIMAGE_TOOL_EXECUTABLE")
|
|
|
|
+ << "' check if it's installed, is executable, or is in your PATH"
|
|
|
|
+ << std::endl);
|
|
|
|
+
|
|
|
|
+ return 0;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ this->SetOptionIfNotSet("CPACK_APPIMAGE_PATCHELF_EXECUTABLE", "patchelf");
|
|
|
|
+ this->PatchElfPath = cmSystemTools::FindProgram(
|
|
|
|
+ *this->GetOption("CPACK_APPIMAGE_PATCHELF_EXECUTABLE"));
|
|
|
|
+
|
|
|
|
+ if (this->PatchElfPath.empty()) {
|
|
|
|
+ cmCPackLogger(
|
|
|
|
+ cmCPackLog::LOG_ERROR,
|
|
|
|
+ "Cannot find patchelf: '"
|
|
|
|
+ << *this->GetOption("CPACK_APPIMAGE_PATCHELF_EXECUTABLE")
|
|
|
|
+ << "' check if it's installed, is executable, or is in your PATH"
|
|
|
|
+ << std::endl);
|
|
|
|
+
|
|
|
|
+ return 0;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return Superclass::InitializeInternal();
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+int cmCPackAppImageGenerator::PackageFiles()
|
|
|
|
+{
|
|
|
|
+ cmCPackLogger(cmCPackLog::LOG_OUTPUT,
|
|
|
|
+ "AppDir: \"" << this->toplevel << "\"" << std::endl);
|
|
|
|
+
|
|
|
|
+ // Desktop file must be in the toplevel dir
|
|
|
|
+ auto const desktopFile = FindDesktopFile();
|
|
|
|
+ if (!desktopFile) {
|
|
|
|
+ cmCPackLogger(cmCPackLog::LOG_WARNING,
|
|
|
|
+ "A desktop file is required to build an AppImage, make sure "
|
|
|
|
+ "it's listed for install()."
|
|
|
|
+ << std::endl);
|
|
|
|
+ return 0;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ {
|
|
|
|
+ cmCPackLogger(cmCPackLog::LOG_OUTPUT,
|
|
|
|
+ "Found Desktop file: \"" << desktopFile.value() << "\""
|
|
|
|
+ << std::endl);
|
|
|
|
+ std::string desktopSymLink = this->toplevel + "/" +
|
|
|
|
+ cmSystemTools::GetFilenameName(desktopFile.value());
|
|
|
|
+ cmCPackLogger(cmCPackLog::LOG_OUTPUT,
|
|
|
|
+ "Desktop file destination: \"" << desktopSymLink << "\""
|
|
|
|
+ << std::endl);
|
|
|
|
+ auto status = cmSystemTools::CreateSymlink(
|
|
|
|
+ cmSystemTools::RelativePath(toplevel, *desktopFile), desktopSymLink);
|
|
|
|
+ if (status.IsSuccess()) {
|
|
|
|
+ cmCPackLogger(cmCPackLog::LOG_DEBUG,
|
|
|
|
+ "Desktop symbolic link created successfully."
|
|
|
|
+ << std::endl);
|
|
|
|
+ } else {
|
|
|
|
+ cmCPackLogger(cmCPackLog::LOG_ERROR,
|
|
|
|
+ "Error creating symbolic link." << status.GetString()
|
|
|
|
+ << std::endl);
|
|
|
|
+ return 0;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ auto const desktopEntry = ParseDesktopFile(*desktopFile);
|
|
|
|
+
|
|
|
|
+ {
|
|
|
|
+ // Prepare Icon file
|
|
|
|
+ auto const iconValue = desktopEntry.find("Icon");
|
|
|
|
+ if (iconValue == desktopEntry.end()) {
|
|
|
|
+ cmCPackLogger(cmCPackLog::LOG_ERROR,
|
|
|
|
+ "An Icon key is required to build an AppImage, make sure "
|
|
|
|
+ "the desktop file has a reference to one."
|
|
|
|
+ << std::endl);
|
|
|
|
+ return 0;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ auto icon = this->GetOption("CPACK_PACKAGE_ICON");
|
|
|
|
+ if (!icon) {
|
|
|
|
+ cmCPackLogger(cmCPackLog::LOG_ERROR,
|
|
|
|
+ "CPACK_PACKAGE_ICON is required to build an AppImage."
|
|
|
|
+ << std::endl);
|
|
|
|
+ return 0;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (!cmSystemTools::StringStartsWith(*icon, iconValue->second.c_str())) {
|
|
|
|
+ cmCPackLogger(cmCPackLog::LOG_ERROR,
|
|
|
|
+ "CPACK_PACKAGE_ICON must match the file name referenced "
|
|
|
|
+ "in the desktop file."
|
|
|
|
+ << std::endl);
|
|
|
|
+ return 0;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ auto const iconFile = FindFile(icon);
|
|
|
|
+ if (!iconFile) {
|
|
|
|
+ cmCPackLogger(cmCPackLog::LOG_ERROR,
|
|
|
|
+ "Could not find the Icon referenced in the desktop file: "
|
|
|
|
+ << *icon << std::endl);
|
|
|
|
+ return 0;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ cmCPackLogger(cmCPackLog::LOG_OUTPUT,
|
|
|
|
+ "Icon file: \"" << *iconFile << "\"" << std::endl);
|
|
|
|
+ std::string iconSymLink =
|
|
|
|
+ this->toplevel + "/" + cmSystemTools::GetFilenameName(*iconFile);
|
|
|
|
+ cmCPackLogger(cmCPackLog::LOG_OUTPUT,
|
|
|
|
+ "Icon link destination: \"" << iconSymLink << "\""
|
|
|
|
+ << std::endl);
|
|
|
|
+ auto status = cmSystemTools::CreateSymlink(
|
|
|
|
+ cmSystemTools::RelativePath(toplevel, *iconFile), iconSymLink);
|
|
|
|
+ if (status.IsSuccess()) {
|
|
|
|
+ cmCPackLogger(cmCPackLog::LOG_DEBUG,
|
|
|
|
+ "Icon symbolic link created successfully." << std::endl);
|
|
|
|
+ } else {
|
|
|
|
+ cmCPackLogger(cmCPackLog::LOG_ERROR,
|
|
|
|
+ "Error creating symbolic link." << status.GetString()
|
|
|
|
+ << std::endl);
|
|
|
|
+ return 0;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ std::string application;
|
|
|
|
+ {
|
|
|
|
+ // Prepare executable file
|
|
|
|
+ auto const execValue = desktopEntry.find("Exec");
|
|
|
|
+ if (execValue == desktopEntry.end() || execValue->second.empty()) {
|
|
|
|
+ cmCPackLogger(cmCPackLog::LOG_ERROR,
|
|
|
|
+ "An Exec key is required to build an AppImage, make sure "
|
|
|
|
+ "the desktop file has a reference to one."
|
|
|
|
+ << std::endl);
|
|
|
|
+ return 0;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ auto const execName =
|
|
|
|
+ cmSystemTools::SplitString(execValue->second, ' ').front();
|
|
|
|
+ auto const mainExecutable = FindFile(execName);
|
|
|
|
+
|
|
|
|
+ if (!mainExecutable) {
|
|
|
|
+ cmCPackLogger(
|
|
|
|
+ cmCPackLog::LOG_ERROR,
|
|
|
|
+ "Could not find the Executable referenced in the desktop file: "
|
|
|
|
+ << execName << std::endl);
|
|
|
|
+ return 0;
|
|
|
|
+ }
|
|
|
|
+ application = cmSystemTools::RelativePath(toplevel, *mainExecutable);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ std::string const appRunFile = this->toplevel + "/AppRun";
|
|
|
|
+ {
|
|
|
|
+ // AppRun script will run our application
|
|
|
|
+ cmGeneratedFileStream appRun(appRunFile);
|
|
|
|
+ appRun << R"sh(#! /usr/bin/env bash
|
|
|
|
+
|
|
|
|
+ # autogenerated by CPack
|
|
|
|
+
|
|
|
|
+ # make sure errors in sourced scripts will cause this script to stop
|
|
|
|
+ set -e
|
|
|
|
+
|
|
|
|
+ this_dir="$(readlink -f "$(dirname "$0")")"
|
|
|
|
+ )sh" << std::endl;
|
|
|
|
+ appRun << R"sh(exec "$this_dir"/)sh" << application << R"sh( "$@")sh"
|
|
|
|
+ << std::endl;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ mode_t permissions;
|
|
|
|
+ {
|
|
|
|
+ auto status = cmSystemTools::GetPermissions(appRunFile, permissions);
|
|
|
|
+ if (!status.IsSuccess()) {
|
|
|
|
+ cmCPackLogger(cmCPackLog::LOG_ERROR,
|
|
|
|
+ "Error getting AppRun permission: " << status.GetString()
|
|
|
|
+ << std::endl);
|
|
|
|
+ return 0;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ auto status =
|
|
|
|
+ cmSystemTools::SetPermissions(appRunFile, permissions | S_IXUSR);
|
|
|
|
+ if (!status.IsSuccess()) {
|
|
|
|
+ cmCPackLogger(cmCPackLog::LOG_ERROR,
|
|
|
|
+ "Error changing AppRun permission: " << status.GetString()
|
|
|
|
+ << std::endl);
|
|
|
|
+ return 0;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // Set RPATH to "$ORIGIN/../lib"
|
|
|
|
+ if (!ChangeRPath()) {
|
|
|
|
+ return 0;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // Run appimagetool
|
|
|
|
+ std::vector<std::string> command{
|
|
|
|
+ this->AppimagetoolPath,
|
|
|
|
+ this->toplevel,
|
|
|
|
+ };
|
|
|
|
+ command.emplace_back("../" + *this->GetOption("CPACK_PACKAGE_FILE_NAME") +
|
|
|
|
+ this->GetOutputExtension());
|
|
|
|
+
|
|
|
|
+ auto addOptionFlag = [&command, this](std::string const& op,
|
|
|
|
+ std::string commandFlag) {
|
|
|
|
+ auto opt = this->GetOption(op);
|
|
|
|
+ if (opt) {
|
|
|
|
+ command.emplace_back(commandFlag);
|
|
|
|
+ }
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ auto addOption = [&command, this](std::string const& op,
|
|
|
|
+ std::string commandFlag) {
|
|
|
|
+ auto opt = this->GetOption(op);
|
|
|
|
+ if (opt) {
|
|
|
|
+ command.emplace_back(commandFlag);
|
|
|
|
+ command.emplace_back(*opt);
|
|
|
|
+ }
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ auto addOptions = [&command, this](std::string const& op,
|
|
|
|
+ std::string commandFlag) {
|
|
|
|
+ auto opt = this->GetOption(op);
|
|
|
|
+ if (opt) {
|
|
|
|
+ auto const options = cmSystemTools::SplitString(*opt, ';');
|
|
|
|
+ for (auto const& mkOpt : options) {
|
|
|
|
+ command.emplace_back(commandFlag);
|
|
|
|
+ command.emplace_back(mkOpt);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ addOption("CPACK_APPIMAGE_UPDATE_INFORMATION", "--updateinformation");
|
|
|
|
+
|
|
|
|
+ addOptionFlag("CPACK_APPIMAGE_GUESS_UPDATE_INFORMATION", "--guess");
|
|
|
|
+
|
|
|
|
+ addOption("CPACK_APPIMAGE_COMPRESSOR", "--comp");
|
|
|
|
+
|
|
|
|
+ addOptions("CPACK_APPIMAGE_MKSQUASHFS_OPTIONS", "--mksquashfs-opt");
|
|
|
|
+
|
|
|
|
+ addOptionFlag("CPACK_APPIMAGE_NO_APPSTREAM", "--no-appstream");
|
|
|
|
+
|
|
|
|
+ addOption("CPACK_APPIMAGE_EXCLUDE_FILE", "--exclude-file");
|
|
|
|
+
|
|
|
|
+ addOption("CPACK_APPIMAGE_RUNTIME_FILE", "--runtime-file");
|
|
|
|
+
|
|
|
|
+ addOptionFlag("CPACK_APPIMAGE_SIGN", "--sign");
|
|
|
|
+
|
|
|
|
+ addOption("CPACK_APPIMAGE_SIGN_KEY", "--sign-key");
|
|
|
|
+
|
|
|
|
+ cmCPackLogger(cmCPackLog::LOG_OUTPUT,
|
|
|
|
+ "Running AppImageTool: "
|
|
|
|
+ << cmSystemTools::PrintSingleCommand(command) << std::endl);
|
|
|
|
+ int retVal = 1;
|
|
|
|
+ bool resS = cmSystemTools::RunSingleCommand(
|
|
|
|
+ command, nullptr, nullptr, &retVal, this->toplevel.c_str(),
|
|
|
|
+ cmSystemTools::OutputOption::OUTPUT_PASSTHROUGH);
|
|
|
|
+ if (!resS || retVal) {
|
|
|
|
+ cmCPackLogger(cmCPackLog::LOG_ERROR,
|
|
|
|
+ "Problem running appimagetool: " << this->AppimagetoolPath
|
|
|
|
+ << std::endl);
|
|
|
|
+ return 0;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return 1;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+cm::optional<std::string> cmCPackAppImageGenerator::FindFile(
|
|
|
|
+ std::string const& filename) const
|
|
|
|
+{
|
|
|
|
+ for (std::string const& file : this->files) {
|
|
|
|
+ if (cmSystemTools::GetFilenameName(file) == filename) {
|
|
|
|
+ cmCPackLogger(cmCPackLog::LOG_DEBUG, "Found file:" << file << std::endl);
|
|
|
|
+ return file;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ return cm::nullopt;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+cm::optional<std::string> cmCPackAppImageGenerator::FindDesktopFile() const
|
|
|
|
+{
|
|
|
|
+ cmValue desktopFileOpt = GetOption("CPACK_APPIMAGE_DESKTOP_FILE");
|
|
|
|
+ if (desktopFileOpt) {
|
|
|
|
+ return FindFile(*desktopFileOpt);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ for (std::string const& file : this->files) {
|
|
|
|
+ if (cmSystemTools::StringEndsWith(file, ".desktop")) {
|
|
|
|
+ cmCPackLogger(cmCPackLog::LOG_DEBUG,
|
|
|
|
+ "Found desktop file:" << file << std::endl);
|
|
|
|
+ return file;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return cm::nullopt;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+namespace {
|
|
|
|
+// Trim leading and trailing whitespace from a string
|
|
|
|
+std::string trim(std::string const& str)
|
|
|
|
+{
|
|
|
|
+ auto start = std::find_if_not(
|
|
|
|
+ str.begin(), str.end(), [](unsigned char c) { return std::isspace(c); });
|
|
|
|
+ auto end = std::find_if_not(str.rbegin(), str.rend(), [](unsigned char c) {
|
|
|
|
+ return std::isspace(c);
|
|
|
|
+ }).base();
|
|
|
|
+ return (start < end) ? std::string(start, end) : std::string();
|
|
|
|
+}
|
|
|
|
+} // namespace
|
|
|
|
+
|
|
|
|
+std::unordered_map<std::string, std::string>
|
|
|
|
+cmCPackAppImageGenerator::ParseDesktopFile(std::string const& filePath) const
|
|
|
|
+{
|
|
|
|
+ std::unordered_map<std::string, std::string> ret;
|
|
|
|
+
|
|
|
|
+ cmsys::ifstream file(filePath);
|
|
|
|
+ if (!file.is_open()) {
|
|
|
|
+ cmCPackLogger(cmCPackLog::LOG_ERROR,
|
|
|
|
+ "Failed to open desktop file:" << filePath << std::endl);
|
|
|
|
+ return ret;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ bool inDesktopEntry = false;
|
|
|
|
+ std::string line;
|
|
|
|
+ while (std::getline(file, line)) {
|
|
|
|
+ line = trim(line);
|
|
|
|
+
|
|
|
|
+ if (line.empty() || line[0] == '#') {
|
|
|
|
+ // Skip empty lines or comments
|
|
|
|
+ continue;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (line.front() == '[' && line.back() == ']') {
|
|
|
|
+ // We only care for [Desktop Entry] section
|
|
|
|
+ inDesktopEntry = (line == "[Desktop Entry]");
|
|
|
|
+ continue;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (inDesktopEntry) {
|
|
|
|
+ size_t delimiter_pos = line.find('=');
|
|
|
|
+ if (delimiter_pos == std::string::npos) {
|
|
|
|
+ cmCPackLogger(cmCPackLog::LOG_WARNING,
|
|
|
|
+ "Invalid desktop file line format: " << line
|
|
|
|
+ << std::endl);
|
|
|
|
+ continue;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ std::string key = trim(line.substr(0, delimiter_pos));
|
|
|
|
+ std::string value = trim(line.substr(delimiter_pos + 1));
|
|
|
|
+ if (!key.empty()) {
|
|
|
|
+ ret.emplace(key, value);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return ret;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+bool cmCPackAppImageGenerator::ChangeRPath()
|
|
|
|
+{
|
|
|
|
+ // AppImages are mounted in random locations so we need RPATH to resolve to
|
|
|
|
+ // that location
|
|
|
|
+ std::string const newRPath = "$ORIGIN/../lib";
|
|
|
|
+
|
|
|
|
+ for (std::string const& file : this->files) {
|
|
|
|
+ cmELF elf(file.c_str());
|
|
|
|
+
|
|
|
|
+ auto const type = elf.GetFileType();
|
|
|
|
+ switch (type) {
|
|
|
|
+ case cmELF::FileType::FileTypeExecutable:
|
|
|
|
+ case cmELF::FileType::FileTypeSharedLibrary: {
|
|
|
|
+ std::string oldRPath;
|
|
|
|
+ auto const* rpath = elf.GetRPath();
|
|
|
|
+ if (rpath) {
|
|
|
|
+ oldRPath = rpath->Value;
|
|
|
|
+ } else {
|
|
|
|
+ auto const* runpath = elf.GetRunPath();
|
|
|
|
+ if (runpath) {
|
|
|
|
+ oldRPath = runpath->Value;
|
|
|
|
+ } else {
|
|
|
|
+ oldRPath = "";
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (cmSystemTools::StringStartsWith(oldRPath, "$ORIGIN")) {
|
|
|
|
+ // Skip libraries with ORIGIN RPATH set
|
|
|
|
+ continue;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (!PatchElfSetRPath(file, newRPath)) {
|
|
|
|
+ return false;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ break;
|
|
|
|
+ }
|
|
|
|
+ default:
|
|
|
|
+ cmCPackLogger(cmCPackLog::LOG_DEBUG,
|
|
|
|
+ "ELF <" << file << "> type: " << type << std::endl);
|
|
|
|
+ break;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return true;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+bool cmCPackAppImageGenerator::PatchElfSetRPath(std::string const& file,
|
|
|
|
+ std::string const& rpath) const
|
|
|
|
+{
|
|
|
|
+ cmCPackLogger(cmCPackLog::LOG_DEBUG,
|
|
|
|
+ "Changing RPATH: " << file << " to: " << rpath << std::endl);
|
|
|
|
+ int retVal = 1;
|
|
|
|
+ bool resS = cmSystemTools::RunSingleCommand(
|
|
|
|
+ {
|
|
|
|
+ this->PatchElfPath,
|
|
|
|
+ "--set-rpath",
|
|
|
|
+ rpath,
|
|
|
|
+ file,
|
|
|
|
+ },
|
|
|
|
+ nullptr, nullptr, &retVal, nullptr,
|
|
|
|
+ cmSystemTools::OutputOption::OUTPUT_NONE);
|
|
|
|
+ if (!resS || retVal) {
|
|
|
|
+ cmCPackLogger(cmCPackLog::LOG_ERROR,
|
|
|
|
+ "Problem running patchelf to change RPATH: " << file
|
|
|
|
+ << std::endl);
|
|
|
|
+ return false;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return true;
|
|
|
|
+}
|