cmCPackAppImageGenerator.cxx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459
  1. /* Distributed under the OSI-approved BSD 3-Clause License. See accompanying
  2. file LICENSE.rst or https://cmake.org/licensing for details. */
  3. #include "cmCPackAppImageGenerator.h"
  4. #include <algorithm>
  5. #include <cctype>
  6. #include <cstddef>
  7. #include <utility>
  8. #include <vector>
  9. #include <fcntl.h>
  10. #include <sys/types.h>
  11. #include "cmsys/FStream.hxx"
  12. #include "cmCPackLog.h"
  13. #include "cmELF.h"
  14. #include "cmGeneratedFileStream.h"
  15. #include "cmSystemTools.h"
  16. #include "cmValue.h"
  17. cmCPackAppImageGenerator::cmCPackAppImageGenerator() = default;
  18. cmCPackAppImageGenerator::~cmCPackAppImageGenerator() = default;
  19. int cmCPackAppImageGenerator::InitializeInternal()
  20. {
  21. this->SetOptionIfNotSet("CPACK_APPIMAGE_TOOL_EXECUTABLE", "appimagetool");
  22. this->AppimagetoolPath = cmSystemTools::FindProgram(
  23. *this->GetOption("CPACK_APPIMAGE_TOOL_EXECUTABLE"));
  24. if (this->AppimagetoolPath.empty()) {
  25. cmCPackLogger(
  26. cmCPackLog::LOG_ERROR,
  27. "Cannot find AppImageTool: '"
  28. << *this->GetOption("CPACK_APPIMAGE_TOOL_EXECUTABLE")
  29. << "' check if it's installed, is executable, or is in your PATH"
  30. << std::endl);
  31. return 0;
  32. }
  33. this->SetOptionIfNotSet("CPACK_APPIMAGE_PATCHELF_EXECUTABLE", "patchelf");
  34. this->PatchElfPath = cmSystemTools::FindProgram(
  35. *this->GetOption("CPACK_APPIMAGE_PATCHELF_EXECUTABLE"));
  36. if (this->PatchElfPath.empty()) {
  37. cmCPackLogger(
  38. cmCPackLog::LOG_ERROR,
  39. "Cannot find patchelf: '"
  40. << *this->GetOption("CPACK_APPIMAGE_PATCHELF_EXECUTABLE")
  41. << "' check if it's installed, is executable, or is in your PATH"
  42. << std::endl);
  43. return 0;
  44. }
  45. return Superclass::InitializeInternal();
  46. }
  47. int cmCPackAppImageGenerator::PackageFiles()
  48. {
  49. cmCPackLogger(cmCPackLog::LOG_OUTPUT,
  50. "AppDir: \"" << this->toplevel << "\"" << std::endl);
  51. // Desktop file must be in the toplevel dir
  52. auto const desktopFile = FindDesktopFile();
  53. if (!desktopFile) {
  54. cmCPackLogger(cmCPackLog::LOG_WARNING,
  55. "A desktop file is required to build an AppImage, make sure "
  56. "it's listed for install()."
  57. << std::endl);
  58. return 0;
  59. }
  60. {
  61. cmCPackLogger(cmCPackLog::LOG_OUTPUT,
  62. "Found Desktop file: \"" << desktopFile.value() << "\""
  63. << std::endl);
  64. std::string desktopSymLink = this->toplevel + "/" +
  65. cmSystemTools::GetFilenameName(desktopFile.value());
  66. cmCPackLogger(cmCPackLog::LOG_OUTPUT,
  67. "Desktop file destination: \"" << desktopSymLink << "\""
  68. << std::endl);
  69. auto status = cmSystemTools::CreateSymlink(
  70. cmSystemTools::RelativePath(toplevel, *desktopFile), desktopSymLink);
  71. if (status.IsSuccess()) {
  72. cmCPackLogger(cmCPackLog::LOG_DEBUG,
  73. "Desktop symbolic link created successfully."
  74. << std::endl);
  75. } else {
  76. cmCPackLogger(cmCPackLog::LOG_ERROR,
  77. "Error creating symbolic link." << status.GetString()
  78. << std::endl);
  79. return 0;
  80. }
  81. }
  82. auto const desktopEntry = ParseDesktopFile(*desktopFile);
  83. {
  84. // Prepare Icon file
  85. auto const iconValue = desktopEntry.find("Icon");
  86. if (iconValue == desktopEntry.end()) {
  87. cmCPackLogger(cmCPackLog::LOG_ERROR,
  88. "An Icon key is required to build an AppImage, make sure "
  89. "the desktop file has a reference to one."
  90. << std::endl);
  91. return 0;
  92. }
  93. auto icon = this->GetOption("CPACK_PACKAGE_ICON");
  94. if (!icon) {
  95. cmCPackLogger(cmCPackLog::LOG_ERROR,
  96. "CPACK_PACKAGE_ICON is required to build an AppImage."
  97. << std::endl);
  98. return 0;
  99. }
  100. if (!cmSystemTools::StringStartsWith(*icon, iconValue->second.c_str())) {
  101. cmCPackLogger(cmCPackLog::LOG_ERROR,
  102. "CPACK_PACKAGE_ICON must match the file name referenced "
  103. "in the desktop file."
  104. << std::endl);
  105. return 0;
  106. }
  107. auto const iconFile = FindFile(icon);
  108. if (!iconFile) {
  109. cmCPackLogger(cmCPackLog::LOG_ERROR,
  110. "Could not find the Icon referenced in the desktop file: "
  111. << *icon << std::endl);
  112. return 0;
  113. }
  114. cmCPackLogger(cmCPackLog::LOG_OUTPUT,
  115. "Icon file: \"" << *iconFile << "\"" << std::endl);
  116. std::string iconSymLink =
  117. this->toplevel + "/" + cmSystemTools::GetFilenameName(*iconFile);
  118. cmCPackLogger(cmCPackLog::LOG_OUTPUT,
  119. "Icon link destination: \"" << iconSymLink << "\""
  120. << std::endl);
  121. auto status = cmSystemTools::CreateSymlink(
  122. cmSystemTools::RelativePath(toplevel, *iconFile), iconSymLink);
  123. if (status.IsSuccess()) {
  124. cmCPackLogger(cmCPackLog::LOG_DEBUG,
  125. "Icon symbolic link created successfully." << std::endl);
  126. } else {
  127. cmCPackLogger(cmCPackLog::LOG_ERROR,
  128. "Error creating symbolic link." << status.GetString()
  129. << std::endl);
  130. return 0;
  131. }
  132. }
  133. std::string application;
  134. {
  135. // Prepare executable file
  136. auto const execValue = desktopEntry.find("Exec");
  137. if (execValue == desktopEntry.end() || execValue->second.empty()) {
  138. cmCPackLogger(cmCPackLog::LOG_ERROR,
  139. "An Exec key is required to build an AppImage, make sure "
  140. "the desktop file has a reference to one."
  141. << std::endl);
  142. return 0;
  143. }
  144. auto const execName =
  145. cmSystemTools::SplitString(execValue->second, ' ').front();
  146. auto const mainExecutable = FindFile(execName);
  147. if (!mainExecutable) {
  148. cmCPackLogger(
  149. cmCPackLog::LOG_ERROR,
  150. "Could not find the Executable referenced in the desktop file: "
  151. << execName << std::endl);
  152. return 0;
  153. }
  154. application = cmSystemTools::RelativePath(toplevel, *mainExecutable);
  155. }
  156. std::string const appRunFile = this->toplevel + "/AppRun";
  157. {
  158. // AppRun script will run our application
  159. cmGeneratedFileStream appRun(appRunFile);
  160. appRun << R"sh(#! /usr/bin/env bash
  161. # autogenerated by CPack
  162. # make sure errors in sourced scripts will cause this script to stop
  163. set -e
  164. this_dir="$(readlink -f "$(dirname "$0")")"
  165. )sh" << std::endl;
  166. appRun << R"sh(exec "$this_dir"/)sh" << application << R"sh( "$@")sh"
  167. << std::endl;
  168. }
  169. mode_t permissions;
  170. {
  171. auto status = cmSystemTools::GetPermissions(appRunFile, permissions);
  172. if (!status.IsSuccess()) {
  173. cmCPackLogger(cmCPackLog::LOG_ERROR,
  174. "Error getting AppRun permission: " << status.GetString()
  175. << std::endl);
  176. return 0;
  177. }
  178. }
  179. auto status =
  180. cmSystemTools::SetPermissions(appRunFile, permissions | S_IXUSR);
  181. if (!status.IsSuccess()) {
  182. cmCPackLogger(cmCPackLog::LOG_ERROR,
  183. "Error changing AppRun permission: " << status.GetString()
  184. << std::endl);
  185. return 0;
  186. }
  187. // Set RPATH to "$ORIGIN/../lib"
  188. if (!ChangeRPath()) {
  189. return 0;
  190. }
  191. // Run appimagetool
  192. std::vector<std::string> command{
  193. this->AppimagetoolPath,
  194. this->toplevel,
  195. };
  196. command.emplace_back("../" + *this->GetOption("CPACK_PACKAGE_FILE_NAME") +
  197. this->GetOutputExtension());
  198. auto addOptionFlag = [&command, this](std::string const& op,
  199. std::string commandFlag) {
  200. auto opt = this->GetOption(op);
  201. if (opt) {
  202. command.emplace_back(commandFlag);
  203. }
  204. };
  205. auto addOption = [&command, this](std::string const& op,
  206. std::string commandFlag) {
  207. auto opt = this->GetOption(op);
  208. if (opt) {
  209. command.emplace_back(commandFlag);
  210. command.emplace_back(*opt);
  211. }
  212. };
  213. auto addOptions = [&command, this](std::string const& op,
  214. std::string commandFlag) {
  215. auto opt = this->GetOption(op);
  216. if (opt) {
  217. auto const options = cmSystemTools::SplitString(*opt, ';');
  218. for (auto const& mkOpt : options) {
  219. command.emplace_back(commandFlag);
  220. command.emplace_back(mkOpt);
  221. }
  222. }
  223. };
  224. addOption("CPACK_APPIMAGE_UPDATE_INFORMATION", "--updateinformation");
  225. addOptionFlag("CPACK_APPIMAGE_GUESS_UPDATE_INFORMATION", "--guess");
  226. addOption("CPACK_APPIMAGE_COMPRESSOR", "--comp");
  227. addOptions("CPACK_APPIMAGE_MKSQUASHFS_OPTIONS", "--mksquashfs-opt");
  228. addOptionFlag("CPACK_APPIMAGE_NO_APPSTREAM", "--no-appstream");
  229. addOption("CPACK_APPIMAGE_EXCLUDE_FILE", "--exclude-file");
  230. addOption("CPACK_APPIMAGE_RUNTIME_FILE", "--runtime-file");
  231. addOptionFlag("CPACK_APPIMAGE_SIGN", "--sign");
  232. addOption("CPACK_APPIMAGE_SIGN_KEY", "--sign-key");
  233. cmCPackLogger(cmCPackLog::LOG_OUTPUT,
  234. "Running AppImageTool: "
  235. << cmSystemTools::PrintSingleCommand(command) << std::endl);
  236. int retVal = 1;
  237. bool resS = cmSystemTools::RunSingleCommand(
  238. command, nullptr, nullptr, &retVal, this->toplevel.c_str(),
  239. cmSystemTools::OutputOption::OUTPUT_PASSTHROUGH);
  240. if (!resS || retVal) {
  241. cmCPackLogger(cmCPackLog::LOG_ERROR,
  242. "Problem running appimagetool: " << this->AppimagetoolPath
  243. << std::endl);
  244. return 0;
  245. }
  246. return 1;
  247. }
  248. cm::optional<std::string> cmCPackAppImageGenerator::FindFile(
  249. std::string const& filename) const
  250. {
  251. for (std::string const& file : this->files) {
  252. if (cmSystemTools::GetFilenameName(file) == filename) {
  253. cmCPackLogger(cmCPackLog::LOG_DEBUG, "Found file:" << file << std::endl);
  254. return file;
  255. }
  256. }
  257. return cm::nullopt;
  258. }
  259. cm::optional<std::string> cmCPackAppImageGenerator::FindDesktopFile() const
  260. {
  261. cmValue desktopFileOpt = GetOption("CPACK_APPIMAGE_DESKTOP_FILE");
  262. if (desktopFileOpt) {
  263. return FindFile(*desktopFileOpt);
  264. }
  265. for (std::string const& file : this->files) {
  266. if (cmSystemTools::StringEndsWith(file, ".desktop")) {
  267. cmCPackLogger(cmCPackLog::LOG_DEBUG,
  268. "Found desktop file:" << file << std::endl);
  269. return file;
  270. }
  271. }
  272. return cm::nullopt;
  273. }
  274. namespace {
  275. // Trim leading and trailing whitespace from a string
  276. std::string trim(std::string const& str)
  277. {
  278. auto start = std::find_if_not(
  279. str.begin(), str.end(), [](unsigned char c) { return std::isspace(c); });
  280. auto end = std::find_if_not(str.rbegin(), str.rend(), [](unsigned char c) {
  281. return std::isspace(c);
  282. }).base();
  283. return (start < end) ? std::string(start, end) : std::string();
  284. }
  285. } // namespace
  286. std::unordered_map<std::string, std::string>
  287. cmCPackAppImageGenerator::ParseDesktopFile(std::string const& filePath) const
  288. {
  289. std::unordered_map<std::string, std::string> ret;
  290. cmsys::ifstream file(filePath);
  291. if (!file.is_open()) {
  292. cmCPackLogger(cmCPackLog::LOG_ERROR,
  293. "Failed to open desktop file:" << filePath << std::endl);
  294. return ret;
  295. }
  296. bool inDesktopEntry = false;
  297. std::string line;
  298. while (std::getline(file, line)) {
  299. line = trim(line);
  300. if (line.empty() || line[0] == '#') {
  301. // Skip empty lines or comments
  302. continue;
  303. }
  304. if (line.front() == '[' && line.back() == ']') {
  305. // We only care for [Desktop Entry] section
  306. inDesktopEntry = (line == "[Desktop Entry]");
  307. continue;
  308. }
  309. if (inDesktopEntry) {
  310. size_t delimiter_pos = line.find('=');
  311. if (delimiter_pos == std::string::npos) {
  312. cmCPackLogger(cmCPackLog::LOG_WARNING,
  313. "Invalid desktop file line format: " << line
  314. << std::endl);
  315. continue;
  316. }
  317. std::string key = trim(line.substr(0, delimiter_pos));
  318. std::string value = trim(line.substr(delimiter_pos + 1));
  319. if (!key.empty()) {
  320. ret.emplace(key, value);
  321. }
  322. }
  323. }
  324. return ret;
  325. }
  326. bool cmCPackAppImageGenerator::ChangeRPath()
  327. {
  328. // AppImages are mounted in random locations so we need RPATH to resolve to
  329. // that location
  330. std::string const newRPath = "$ORIGIN/../lib";
  331. for (std::string const& file : this->files) {
  332. cmELF elf(file.c_str());
  333. auto const type = elf.GetFileType();
  334. switch (type) {
  335. case cmELF::FileType::FileTypeExecutable:
  336. case cmELF::FileType::FileTypeSharedLibrary: {
  337. std::string oldRPath;
  338. auto const* rpath = elf.GetRPath();
  339. if (rpath) {
  340. oldRPath = rpath->Value;
  341. } else {
  342. auto const* runpath = elf.GetRunPath();
  343. if (runpath) {
  344. oldRPath = runpath->Value;
  345. } else {
  346. oldRPath = "";
  347. }
  348. }
  349. if (cmSystemTools::StringStartsWith(oldRPath, "$ORIGIN")) {
  350. // Skip libraries with ORIGIN RPATH set
  351. continue;
  352. }
  353. if (!PatchElfSetRPath(file, newRPath)) {
  354. return false;
  355. }
  356. break;
  357. }
  358. default:
  359. cmCPackLogger(cmCPackLog::LOG_DEBUG,
  360. "ELF <" << file << "> type: " << type << std::endl);
  361. break;
  362. }
  363. }
  364. return true;
  365. }
  366. bool cmCPackAppImageGenerator::PatchElfSetRPath(std::string const& file,
  367. std::string const& rpath) const
  368. {
  369. cmCPackLogger(cmCPackLog::LOG_DEBUG,
  370. "Changing RPATH: " << file << " to: " << rpath << std::endl);
  371. int retVal = 1;
  372. bool resS = cmSystemTools::RunSingleCommand(
  373. {
  374. this->PatchElfPath,
  375. "--set-rpath",
  376. rpath,
  377. file,
  378. },
  379. nullptr, nullptr, &retVal, nullptr,
  380. cmSystemTools::OutputOption::OUTPUT_NONE);
  381. if (!resS || retVal) {
  382. cmCPackLogger(cmCPackLog::LOG_ERROR,
  383. "Problem running patchelf to change RPATH: " << file
  384. << std::endl);
  385. return false;
  386. }
  387. return true;
  388. }