cmCPackAppImageGenerator.cxx 14 KB

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