Imported Upstream version 3.25.0
[platform/upstream/cmake.git] / Source / CPack / cmCPackDragNDropGenerator.cxx
1 /* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
2    file Copyright.txt or https://cmake.org/licensing for details.  */
3 #include "cmCPackDragNDropGenerator.h"
4
5 #include <algorithm>
6 #include <cstdlib>
7 #include <iomanip>
8 #include <map>
9
10 #include <CoreFoundation/CoreFoundation.h>
11 #include <cm3p/kwiml/abi.h>
12
13 #include "cmsys/Base64.h"
14 #include "cmsys/FStream.hxx"
15 #include "cmsys/RegularExpression.hxx"
16
17 #include "cmCPackConfigure.h"
18 #include "cmCPackGenerator.h"
19 #include "cmCPackLog.h"
20 #include "cmDuration.h"
21 #include "cmGeneratedFileStream.h"
22 #include "cmStringAlgorithms.h"
23 #include "cmSystemTools.h"
24 #include "cmValue.h"
25 #include "cmXMLWriter.h"
26
27 #if HAVE_CoreServices
28 // For the old LocaleStringToLangAndRegionCodes() function, to convert
29 // to the old Script Manager RegionCode values needed for the 'LPic' data
30 // structure used for generating multi-lingual SLAs.
31 #  include <CoreServices/CoreServices.h>
32 #endif
33
34 static const uint16_t DefaultLpic[] = {
35   /* clang-format off */
36   0x0002, 0x0011, 0x0003, 0x0001, 0x0000, 0x0000, 0x0002, 0x0000,
37   0x0008, 0x0003, 0x0000, 0x0001, 0x0004, 0x0000, 0x0004, 0x0005,
38   0x0000, 0x000E, 0x0006, 0x0001, 0x0005, 0x0007, 0x0000, 0x0007,
39   0x0008, 0x0000, 0x0047, 0x0009, 0x0000, 0x0034, 0x000A, 0x0001,
40   0x0035, 0x000B, 0x0001, 0x0020, 0x000C, 0x0000, 0x0011, 0x000D,
41   0x0000, 0x005B, 0x0004, 0x0000, 0x0033, 0x000F, 0x0001, 0x000C,
42   0x0010, 0x0000, 0x000B, 0x000E, 0x0000
43   /* clang-format on */
44 };
45
46 static const std::vector<std::string> DefaultMenu = {
47   { "English", "Agree", "Disagree", "Print", "Save...",
48     // NOLINTNEXTLINE(bugprone-suspicious-missing-comma)
49     "You agree to the License Agreement terms when "
50     "you click the \"Agree\" button.",
51     "Software License Agreement",
52     "This text cannot be saved.  "
53     "This disk may be full or locked, or the file may be locked.",
54     "Unable to print.  Make sure you have selected a printer." }
55 };
56
57 cmCPackDragNDropGenerator::cmCPackDragNDropGenerator()
58   : singleLicense(false)
59 {
60   // default to one package file for components
61   this->componentPackageMethod = ONE_PACKAGE;
62 }
63
64 cmCPackDragNDropGenerator::~cmCPackDragNDropGenerator() = default;
65
66 int cmCPackDragNDropGenerator::InitializeInternal()
67 {
68   // Starting with Xcode 4.3, look in "/Applications/Xcode.app" first:
69   //
70   std::vector<std::string> paths;
71   paths.emplace_back("/Applications/Xcode.app/Contents/Developer/Tools");
72   paths.emplace_back("/Developer/Tools");
73
74   const std::string hdiutil_path =
75     cmSystemTools::FindProgram("hdiutil", std::vector<std::string>(), false);
76   if (hdiutil_path.empty()) {
77     cmCPackLogger(cmCPackLog::LOG_ERROR,
78                   "Cannot locate hdiutil command" << std::endl);
79     return 0;
80   }
81   this->SetOptionIfNotSet("CPACK_COMMAND_HDIUTIL", hdiutil_path);
82
83   const std::string setfile_path =
84     cmSystemTools::FindProgram("SetFile", paths, false);
85   if (setfile_path.empty()) {
86     cmCPackLogger(cmCPackLog::LOG_ERROR,
87                   "Cannot locate SetFile command" << std::endl);
88     return 0;
89   }
90   this->SetOptionIfNotSet("CPACK_COMMAND_SETFILE", setfile_path);
91
92   const std::string rez_path = cmSystemTools::FindProgram("Rez", paths, false);
93   if (rez_path.empty()) {
94     cmCPackLogger(cmCPackLog::LOG_ERROR,
95                   "Cannot locate Rez command" << std::endl);
96     return 0;
97   }
98   this->SetOptionIfNotSet("CPACK_COMMAND_REZ", rez_path);
99
100   if (this->IsSet("CPACK_DMG_SLA_DIR")) {
101     slaDirectory = this->GetOption("CPACK_DMG_SLA_DIR");
102     if (!slaDirectory.empty() &&
103         this->IsOn("CPACK_DMG_SLA_USE_RESOURCE_FILE_LICENSE") &&
104         this->IsSet("CPACK_RESOURCE_FILE_LICENSE")) {
105       std::string license_file =
106         this->GetOption("CPACK_RESOURCE_FILE_LICENSE");
107       if (!license_file.empty() &&
108           (license_file.find("CPack.GenericLicense.txt") ==
109            std::string::npos)) {
110         cmCPackLogger(
111           cmCPackLog::LOG_OUTPUT,
112           "Both CPACK_DMG_SLA_DIR and CPACK_RESOURCE_FILE_LICENSE specified, "
113           "using CPACK_RESOURCE_FILE_LICENSE as a license for all languages."
114             << std::endl);
115         singleLicense = true;
116       }
117     }
118     if (!this->IsSet("CPACK_DMG_SLA_LANGUAGES")) {
119       cmCPackLogger(cmCPackLog::LOG_ERROR,
120                     "CPACK_DMG_SLA_DIR set but no languages defined "
121                     "(set CPACK_DMG_SLA_LANGUAGES)"
122                       << std::endl);
123       return 0;
124     }
125     if (!cmSystemTools::FileExists(slaDirectory, false)) {
126       cmCPackLogger(cmCPackLog::LOG_ERROR,
127                     "CPACK_DMG_SLA_DIR does not exist" << std::endl);
128       return 0;
129     }
130
131     std::vector<std::string> languages =
132       cmExpandedList(this->GetOption("CPACK_DMG_SLA_LANGUAGES"));
133     if (languages.empty()) {
134       cmCPackLogger(cmCPackLog::LOG_ERROR,
135                     "CPACK_DMG_SLA_LANGUAGES set but empty" << std::endl);
136       return 0;
137     }
138     for (auto const& language : languages) {
139       std::string license = slaDirectory + "/" + language + ".license.txt";
140       std::string license_rtf = slaDirectory + "/" + language + ".license.rtf";
141       if (!singleLicense) {
142         if (!cmSystemTools::FileExists(license) &&
143             !cmSystemTools::FileExists(license_rtf)) {
144           cmCPackLogger(cmCPackLog::LOG_ERROR,
145                         "Missing license file "
146                           << language << ".license.txt"
147                           << " / " << language << ".license.rtf" << std::endl);
148           return 0;
149         }
150       }
151       std::string menu = slaDirectory + "/" + language + ".menu.txt";
152       if (!cmSystemTools::FileExists(menu)) {
153         cmCPackLogger(cmCPackLog::LOG_ERROR,
154                       "Missing menu file " << language << ".menu.txt"
155                                            << std::endl);
156         return 0;
157       }
158     }
159   }
160
161   return this->Superclass::InitializeInternal();
162 }
163
164 const char* cmCPackDragNDropGenerator::GetOutputExtension()
165 {
166   return ".dmg";
167 }
168
169 int cmCPackDragNDropGenerator::PackageFiles()
170 {
171   // gather which directories to make dmg files for
172   // multiple directories occur if packaging components or groups separately
173
174   // monolith
175   if (this->Components.empty()) {
176     return this->CreateDMG(toplevel, packageFileNames[0]);
177   }
178
179   // component install
180   std::vector<std::string> package_files;
181
182   std::map<std::string, cmCPackComponent>::iterator compIt;
183   for (compIt = this->Components.begin(); compIt != this->Components.end();
184        ++compIt) {
185     std::string name = GetComponentInstallDirNameSuffix(compIt->first);
186     package_files.push_back(name);
187   }
188   std::sort(package_files.begin(), package_files.end());
189   package_files.erase(std::unique(package_files.begin(), package_files.end()),
190                       package_files.end());
191
192   // loop to create dmg files
193   packageFileNames.clear();
194   for (auto const& package_file : package_files) {
195     std::string full_package_name = std::string(toplevel) + std::string("/");
196     if (package_file == "ALL_IN_ONE") {
197       full_package_name += this->GetOption("CPACK_PACKAGE_FILE_NAME");
198     } else {
199       full_package_name += package_file;
200     }
201     full_package_name += std::string(GetOutputExtension());
202     packageFileNames.push_back(full_package_name);
203
204     std::string src_dir = cmStrCat(toplevel, '/', package_file);
205
206     if (0 == this->CreateDMG(src_dir, full_package_name)) {
207       return 0;
208     }
209   }
210   return 1;
211 }
212
213 bool cmCPackDragNDropGenerator::CopyFile(std::ostringstream& source,
214                                          std::ostringstream& target)
215 {
216   if (!cmSystemTools::CopyFileIfDifferent(source.str(), target.str())) {
217     cmCPackLogger(cmCPackLog::LOG_ERROR,
218                   "Error copying " << source.str() << " to " << target.str()
219                                    << std::endl);
220
221     return false;
222   }
223
224   return true;
225 }
226
227 bool cmCPackDragNDropGenerator::CreateEmptyFile(std::ostringstream& target,
228                                                 size_t size)
229 {
230   cmsys::ofstream fout(target.str().c_str(), std::ios::out | std::ios::binary);
231   if (!fout) {
232     return false;
233   }
234
235   // Seek to desired size - 1 byte
236   fout.seekp(size - 1, std::ios::beg);
237   char byte = 0;
238   // Write one byte to ensure file grows
239   fout.write(&byte, 1);
240
241   return true;
242 }
243
244 bool cmCPackDragNDropGenerator::RunCommand(std::ostringstream& command,
245                                            std::string* output)
246 {
247   int exit_code = 1;
248
249   bool result = cmSystemTools::RunSingleCommand(
250     command.str(), output, output, &exit_code, nullptr, this->GeneratorVerbose,
251     cmDuration::zero());
252
253   if (!result || exit_code) {
254     cmCPackLogger(cmCPackLog::LOG_ERROR,
255                   "Error executing: " << command.str() << std::endl);
256
257     return false;
258   }
259
260   return true;
261 }
262
263 int cmCPackDragNDropGenerator::CreateDMG(const std::string& src_dir,
264                                          const std::string& output_file)
265 {
266   // Get optional arguments ...
267   cmValue cpack_package_icon = this->GetOption("CPACK_PACKAGE_ICON");
268
269   const std::string cpack_dmg_volume_name =
270     this->GetOption("CPACK_DMG_VOLUME_NAME")
271     ? *this->GetOption("CPACK_DMG_VOLUME_NAME")
272     : *this->GetOption("CPACK_PACKAGE_FILE_NAME");
273
274   const std::string cpack_dmg_format = this->GetOption("CPACK_DMG_FORMAT")
275     ? *this->GetOption("CPACK_DMG_FORMAT")
276     : "UDZO";
277
278   const std::string cpack_dmg_filesystem =
279     this->GetOption("CPACK_DMG_FILESYSTEM")
280     ? *this->GetOption("CPACK_DMG_FILESYSTEM")
281     : "HFS+";
282
283   // Get optional arguments ...
284   std::string cpack_license_file;
285   if (this->IsOn("CPACK_DMG_SLA_USE_RESOURCE_FILE_LICENSE")) {
286     cpack_license_file = *this->GetOption("CPACK_RESOURCE_FILE_LICENSE");
287   }
288
289   cmValue cpack_dmg_background_image =
290     this->GetOption("CPACK_DMG_BACKGROUND_IMAGE");
291
292   cmValue cpack_dmg_ds_store = this->GetOption("CPACK_DMG_DS_STORE");
293
294   cmValue cpack_dmg_languages = this->GetOption("CPACK_DMG_SLA_LANGUAGES");
295
296   cmValue cpack_dmg_ds_store_setup_script =
297     this->GetOption("CPACK_DMG_DS_STORE_SETUP_SCRIPT");
298
299   const bool cpack_dmg_disable_applications_symlink =
300     this->IsOn("CPACK_DMG_DISABLE_APPLICATIONS_SYMLINK");
301
302   // only put license on dmg if is user provided
303   if (!cpack_license_file.empty() &&
304       cpack_license_file.find("CPack.GenericLicense.txt") !=
305         std::string::npos) {
306     cpack_license_file = "";
307   }
308
309   // use sla_dir if both sla_dir and license_file are set
310   if (!cpack_license_file.empty() && !slaDirectory.empty() && !singleLicense) {
311     cpack_license_file = "";
312   }
313
314   // The staging directory contains everything that will end-up inside the
315   // final disk image ...
316   std::ostringstream staging;
317   staging << src_dir;
318
319   // Add a symlink to /Applications so users can drag-and-drop the bundle
320   // into it unless this behavior was disabled
321   if (!cpack_dmg_disable_applications_symlink) {
322     std::ostringstream application_link;
323     application_link << staging.str() << "/Applications";
324     cmSystemTools::CreateSymlink("/Applications", application_link.str());
325   }
326
327   // Optionally add a custom volume icon ...
328   if (!cpack_package_icon->empty()) {
329     std::ostringstream package_icon_source;
330     package_icon_source << cpack_package_icon;
331
332     std::ostringstream package_icon_destination;
333     package_icon_destination << staging.str() << "/.VolumeIcon.icns";
334
335     if (!this->CopyFile(package_icon_source, package_icon_destination)) {
336       cmCPackLogger(cmCPackLog::LOG_ERROR,
337                     "Error copying disk volume icon.  "
338                     "Check the value of CPACK_PACKAGE_ICON."
339                       << std::endl);
340
341       return 0;
342     }
343   }
344
345   // Optionally add a custom .DS_Store file
346   // (e.g. for setting background/layout) ...
347   if (!cpack_dmg_ds_store->empty()) {
348     std::ostringstream package_settings_source;
349     package_settings_source << cpack_dmg_ds_store;
350
351     std::ostringstream package_settings_destination;
352     package_settings_destination << staging.str() << "/.DS_Store";
353
354     if (!this->CopyFile(package_settings_source,
355                         package_settings_destination)) {
356       cmCPackLogger(cmCPackLog::LOG_ERROR,
357                     "Error copying disk volume settings file.  "
358                     "Check the value of CPACK_DMG_DS_STORE."
359                       << std::endl);
360
361       return 0;
362     }
363   }
364
365   // Optionally add a custom background image ...
366   // Make sure the background file type is the same as the custom image
367   // and that the file is hidden so it doesn't show up.
368   if (!cpack_dmg_background_image->empty()) {
369     const std::string extension =
370       cmSystemTools::GetFilenameLastExtension(cpack_dmg_background_image);
371     std::ostringstream package_background_source;
372     package_background_source << cpack_dmg_background_image;
373
374     std::ostringstream package_background_destination;
375     package_background_destination << staging.str()
376                                    << "/.background/background" << extension;
377
378     if (!this->CopyFile(package_background_source,
379                         package_background_destination)) {
380       cmCPackLogger(cmCPackLog::LOG_ERROR,
381                     "Error copying disk volume background image.  "
382                     "Check the value of CPACK_DMG_BACKGROUND_IMAGE."
383                       << std::endl);
384
385       return 0;
386     }
387   }
388
389   bool remount_image =
390     !cpack_package_icon->empty() || !cpack_dmg_ds_store_setup_script->empty();
391
392   std::string temp_image_format = "UDZO";
393
394   // Create 1 MB dummy padding file in staging area when we need to remount
395   // image, so we have enough space for storing changes ...
396   if (remount_image) {
397     std::ostringstream dummy_padding;
398     dummy_padding << staging.str() << "/.dummy-padding-file";
399     if (!this->CreateEmptyFile(dummy_padding, 1048576)) {
400       cmCPackLogger(cmCPackLog::LOG_ERROR,
401                     "Error creating dummy padding file." << std::endl);
402
403       return 0;
404     }
405     temp_image_format = "UDRW";
406   }
407
408   // Create a temporary read-write disk image ...
409   std::string temp_image =
410     cmStrCat(this->GetOption("CPACK_TOPLEVEL_DIRECTORY"), "/temp.dmg");
411
412   std::string create_error;
413   std::ostringstream temp_image_command;
414   temp_image_command << this->GetOption("CPACK_COMMAND_HDIUTIL");
415   temp_image_command << " create";
416   temp_image_command << " -ov";
417   temp_image_command << " -srcfolder \"" << staging.str() << "\"";
418   temp_image_command << " -volname \"" << cpack_dmg_volume_name << "\"";
419   temp_image_command << " -fs \"" << cpack_dmg_filesystem << "\"";
420   temp_image_command << " -format " << temp_image_format;
421   temp_image_command << " \"" << temp_image << "\"";
422
423   if (!this->RunCommand(temp_image_command, &create_error)) {
424     cmCPackLogger(cmCPackLog::LOG_ERROR,
425                   "Error generating temporary disk image." << std::endl
426                                                            << create_error
427                                                            << std::endl);
428
429     return 0;
430   }
431
432   if (remount_image) {
433     // Store that we have a failure so that we always unmount the image
434     // before we exit.
435     bool had_error = false;
436
437     std::ostringstream attach_command;
438     attach_command << this->GetOption("CPACK_COMMAND_HDIUTIL");
439     attach_command << " attach";
440     attach_command << " \"" << temp_image << "\"";
441
442     std::string attach_output;
443     if (!this->RunCommand(attach_command, &attach_output)) {
444       cmCPackLogger(cmCPackLog::LOG_ERROR,
445                     "Error attaching temporary disk image." << std::endl);
446
447       return 0;
448     }
449
450     cmsys::RegularExpression mountpoint_regex(".*(/Volumes/[^\n]+)\n.*");
451     mountpoint_regex.find(attach_output.c_str());
452     std::string const temp_mount = mountpoint_regex.match(1);
453     std::string const temp_mount_name =
454       temp_mount.substr(sizeof("/Volumes/") - 1);
455
456     // Remove dummy padding file so we have enough space on RW image ...
457     std::ostringstream dummy_padding;
458     dummy_padding << temp_mount << "/.dummy-padding-file";
459     if (!cmSystemTools::RemoveFile(dummy_padding.str())) {
460       cmCPackLogger(cmCPackLog::LOG_ERROR,
461                     "Error removing dummy padding file." << std::endl);
462
463       had_error = true;
464     }
465
466     // Optionally set the custom icon flag for the image ...
467     if (!had_error && !cpack_package_icon->empty()) {
468       std::string error;
469       std::ostringstream setfile_command;
470       setfile_command << this->GetOption("CPACK_COMMAND_SETFILE");
471       setfile_command << " -a C";
472       setfile_command << " \"" << temp_mount << "\"";
473
474       if (!this->RunCommand(setfile_command, &error)) {
475         cmCPackLogger(cmCPackLog::LOG_ERROR,
476                       "Error assigning custom icon to temporary disk image."
477                         << std::endl
478                         << error << std::endl);
479
480         had_error = true;
481       }
482     }
483
484     // Optionally we can execute a custom apple script to generate
485     // the .DS_Store for the volume folder ...
486     if (!had_error && !cpack_dmg_ds_store_setup_script->empty()) {
487       std::ostringstream setup_script_command;
488       setup_script_command << "osascript"
489                            << " \"" << cpack_dmg_ds_store_setup_script << "\""
490                            << " \"" << temp_mount_name << "\"";
491       std::string error;
492       if (!this->RunCommand(setup_script_command, &error)) {
493         cmCPackLogger(cmCPackLog::LOG_ERROR,
494                       "Error executing custom script on disk image."
495                         << std::endl
496                         << error << std::endl);
497
498         had_error = true;
499       }
500     }
501
502     std::ostringstream detach_command;
503     detach_command << this->GetOption("CPACK_COMMAND_HDIUTIL");
504     detach_command << " detach";
505     detach_command << " \"" << temp_mount << "\"";
506
507     if (!this->RunCommand(detach_command)) {
508       cmCPackLogger(cmCPackLog::LOG_ERROR,
509                     "Error detaching temporary disk image." << std::endl);
510
511       return 0;
512     }
513
514     if (had_error) {
515       return 0;
516     }
517   }
518
519   // Create the final compressed read-only disk image ...
520   std::ostringstream final_image_command;
521   final_image_command << this->GetOption("CPACK_COMMAND_HDIUTIL");
522   final_image_command << " convert \"" << temp_image << "\"";
523   final_image_command << " -format ";
524   final_image_command << cpack_dmg_format;
525   final_image_command << " -imagekey";
526   final_image_command << " zlib-level=9";
527   final_image_command << " -o \"" << output_file << "\"";
528
529   std::string convert_error;
530
531   if (!this->RunCommand(final_image_command, &convert_error)) {
532     cmCPackLogger(cmCPackLog::LOG_ERROR,
533                   "Error compressing disk image." << std::endl
534                                                   << convert_error
535                                                   << std::endl);
536
537     return 0;
538   }
539
540   if (!cpack_license_file.empty() || !slaDirectory.empty()) {
541     // Use old hardcoded style if sla_dir is not set
542     bool oldStyle = slaDirectory.empty();
543     std::string sla_xml =
544       cmStrCat(this->GetOption("CPACK_TOPLEVEL_DIRECTORY"), "/sla.xml");
545
546     std::vector<std::string> languages;
547     if (!oldStyle) {
548       cmExpandList(cpack_dmg_languages, languages);
549     }
550
551     std::vector<uint16_t> header_data;
552     if (oldStyle) {
553       header_data = std::vector<uint16_t>(
554         DefaultLpic,
555         DefaultLpic + (sizeof(DefaultLpic) / sizeof(*DefaultLpic)));
556     } else {
557       /*
558        * LPic Layout
559        * (https://github.com/pypt/dmg-add-license/blob/master/main.c)
560        * as far as I can tell (no official documentation seems to exist):
561        * struct LPic {
562        *  uint16_t default_language; // points to a resid, defaulting to 0,
563        *                             // which is the first set language
564        *  uint16_t length;
565        *  struct {
566        *    uint16_t language_code;
567        *    uint16_t resid;
568        *    uint16_t encoding; // Encoding from TextCommon.h,
569        *                       // forcing MacRoman (0) for now. Might need to
570        *                       // allow overwrite per license by user later
571        *  } item[1];
572        * }
573        */
574
575       header_data.push_back(0);
576       header_data.push_back(languages.size());
577       for (size_t i = 0; i < languages.size(); ++i) {
578         CFStringRef language_cfstring = CFStringCreateWithCString(
579           nullptr, languages[i].c_str(), kCFStringEncodingUTF8);
580         CFStringRef iso_language =
581           CFLocaleCreateCanonicalLanguageIdentifierFromString(
582             nullptr, language_cfstring);
583         if (!iso_language) {
584           cmCPackLogger(cmCPackLog::LOG_ERROR,
585                         languages[i] << " is not a recognized language"
586                                      << std::endl);
587         }
588         char iso_language_cstr[65];
589         CFStringGetCString(iso_language, iso_language_cstr,
590                            sizeof(iso_language_cstr) - 1,
591                            kCFStringEncodingMacRoman);
592         LangCode lang = 0;
593         RegionCode region = 0;
594 #if HAVE_CoreServices
595         OSStatus err =
596           LocaleStringToLangAndRegionCodes(iso_language_cstr, &lang, &region);
597         if (err != noErr)
598 #endif
599         {
600           cmCPackLogger(cmCPackLog::LOG_ERROR,
601                         "No language/region code available for "
602                           << iso_language_cstr << std::endl);
603           return 0;
604         }
605 #if HAVE_CoreServices
606         header_data.push_back(region);
607         header_data.push_back(i);
608         header_data.push_back(0);
609 #endif
610       }
611     }
612
613     RezDoc rez;
614
615     {
616       RezDict lpic = { {}, 5000, {} };
617       lpic.Data.reserve(header_data.size() * sizeof(header_data[0]));
618       for (uint16_t x : header_data) {
619         // LPic header is big-endian.
620         char* d = reinterpret_cast<char*>(&x);
621 #if KWIML_ABI_ENDIAN_ID == KWIML_ABI_ENDIAN_ID_LITTLE
622         lpic.Data.push_back(d[1]);
623         lpic.Data.push_back(d[0]);
624 #else
625         lpic.Data.push_back(d[0]);
626         lpic.Data.push_back(d[1]);
627 #endif
628       }
629       rez.LPic.Entries.emplace_back(std::move(lpic));
630     }
631
632     bool have_write_license_error = false;
633     std::string error;
634
635     if (oldStyle) {
636       if (!this->WriteLicense(rez, 0, "", cpack_license_file, &error)) {
637         have_write_license_error = true;
638       }
639     } else {
640       for (size_t i = 0; i < languages.size() && !have_write_license_error;
641            ++i) {
642         if (singleLicense) {
643           if (!this->WriteLicense(rez, i + 5000, languages[i],
644                                   cpack_license_file, &error)) {
645             have_write_license_error = true;
646           }
647         } else {
648           if (!this->WriteLicense(rez, i + 5000, languages[i], "", &error)) {
649             have_write_license_error = true;
650           }
651         }
652       }
653     }
654
655     if (have_write_license_error) {
656       cmCPackLogger(cmCPackLog::LOG_ERROR,
657                     "Error writing license file to SLA." << std::endl
658                                                          << error
659                                                          << std::endl);
660       return 0;
661     }
662
663     this->WriteRezXML(sla_xml, rez);
664
665     // Create the final compressed read-only disk image ...
666     std::ostringstream embed_sla_command;
667     embed_sla_command << this->GetOption("CPACK_COMMAND_HDIUTIL");
668     embed_sla_command << " udifrez";
669     embed_sla_command << " -xml";
670     embed_sla_command << " \"" << sla_xml << "\"";
671     embed_sla_command << " FIXME_WHY_IS_THIS_ARGUMENT_NEEDED";
672     embed_sla_command << " \"" << output_file << "\"";
673     std::string embed_error;
674     if (!this->RunCommand(embed_sla_command, &embed_error)) {
675       cmCPackLogger(cmCPackLog::LOG_ERROR,
676                     "Error compressing disk image." << std::endl
677                                                     << embed_error
678                                                     << std::endl);
679
680       return 0;
681     }
682   }
683
684   return 1;
685 }
686
687 bool cmCPackDragNDropGenerator::SupportsComponentInstallation() const
688 {
689   return true;
690 }
691
692 std::string cmCPackDragNDropGenerator::GetComponentInstallDirNameSuffix(
693   const std::string& componentName)
694 {
695   // we want to group components together that go in the same dmg package
696   std::string package_file_name = this->GetOption("CPACK_PACKAGE_FILE_NAME");
697
698   // we have 3 mutually exclusive modes to work in
699   // 1. all components in one package
700   // 2. each group goes in its own package with left over
701   //    components in their own package
702   // 3. ignore groups - if grouping is defined, it is ignored
703   //    and each component goes in its own package
704
705   if (this->componentPackageMethod == ONE_PACKAGE) {
706     return "ALL_IN_ONE";
707   }
708
709   if (this->componentPackageMethod == ONE_PACKAGE_PER_GROUP) {
710     // We have to find the name of the COMPONENT GROUP
711     // the current COMPONENT belongs to.
712     std::string groupVar =
713       "CPACK_COMPONENT_" + cmSystemTools::UpperCase(componentName) + "_GROUP";
714     cmValue _groupName = this->GetOption(groupVar);
715     if (_groupName) {
716       std::string groupName = _groupName;
717
718       groupName =
719         GetComponentPackageFileName(package_file_name, groupName, true);
720       return groupName;
721     }
722   }
723
724   std::string componentFileName =
725     "CPACK_DMG_" + cmSystemTools::UpperCase(componentName) + "_FILE_NAME";
726   if (this->IsSet(componentFileName)) {
727     return this->GetOption(componentFileName);
728   }
729   return GetComponentPackageFileName(package_file_name, componentName, false);
730 }
731
732 void cmCPackDragNDropGenerator::WriteRezXML(std::string const& file,
733                                             RezDoc const& rez)
734 {
735   cmGeneratedFileStream fxml(file);
736   cmXMLWriter xml(fxml);
737   xml.StartDocument();
738   xml.StartElement("plist");
739   xml.Attribute("version", "1.0");
740   xml.StartElement("dict");
741   this->WriteRezArray(xml, rez.LPic);
742   this->WriteRezArray(xml, rez.Menu);
743   this->WriteRezArray(xml, rez.Text);
744   this->WriteRezArray(xml, rez.RTF);
745   xml.EndElement(); // dict
746   xml.EndElement(); // plist
747   xml.EndDocument();
748   fxml.Close();
749 }
750
751 void cmCPackDragNDropGenerator::WriteRezArray(cmXMLWriter& xml,
752                                               RezArray const& array)
753 {
754   if (array.Entries.empty()) {
755     return;
756   }
757   xml.StartElement("key");
758   xml.Content(array.Key);
759   xml.EndElement(); // key
760   xml.StartElement("array");
761   for (RezDict const& dict : array.Entries) {
762     this->WriteRezDict(xml, dict);
763   }
764   xml.EndElement(); // array
765 }
766
767 void cmCPackDragNDropGenerator::WriteRezDict(cmXMLWriter& xml,
768                                              RezDict const& dict)
769 {
770   std::vector<char> base64buf(dict.Data.size() * 3 / 2 + 5);
771   size_t base64len =
772     cmsysBase64_Encode(dict.Data.data(), dict.Data.size(),
773                        reinterpret_cast<unsigned char*>(base64buf.data()), 0);
774   std::string base64data(base64buf.data(), base64len);
775   /* clang-format off */
776   xml.StartElement("dict");
777   xml.StartElement("key");    xml.Content("Attributes"); xml.EndElement();
778   xml.StartElement("string"); xml.Content("0x0000");     xml.EndElement();
779   xml.StartElement("key");    xml.Content("Data");       xml.EndElement();
780   xml.StartElement("data");   xml.Content(base64data);   xml.EndElement();
781   xml.StartElement("key");    xml.Content("ID");         xml.EndElement();
782   xml.StartElement("string"); xml.Content(dict.ID);      xml.EndElement();
783   xml.StartElement("key");    xml.Content("Name");       xml.EndElement();
784   xml.StartElement("string"); xml.Content(dict.Name);    xml.EndElement();
785   xml.EndElement(); // dict
786   /* clang-format on */
787 }
788
789 bool cmCPackDragNDropGenerator::WriteLicense(RezDoc& rez, size_t licenseNumber,
790                                              std::string licenseLanguage,
791                                              const std::string& licenseFile,
792                                              std::string* error)
793 {
794   if (!licenseFile.empty() && !singleLicense) {
795     licenseNumber = 5002;
796     licenseLanguage = "English";
797   }
798
799   // License file
800   RezArray* licenseArray = &rez.Text;
801   std::string actual_license;
802   if (!licenseFile.empty()) {
803     if (cmHasLiteralSuffix(licenseFile, ".rtf")) {
804       licenseArray = &rez.RTF;
805     }
806     actual_license = licenseFile;
807   } else {
808     std::string license_wo_ext =
809       slaDirectory + "/" + licenseLanguage + ".license";
810     if (cmSystemTools::FileExists(license_wo_ext + ".txt")) {
811       actual_license = license_wo_ext + ".txt";
812     } else {
813       licenseArray = &rez.RTF;
814       actual_license = license_wo_ext + ".rtf";
815     }
816   }
817
818   // License body
819   {
820     RezDict license = { licenseLanguage, licenseNumber, {} };
821     std::vector<std::string> lines;
822     if (!this->ReadFile(actual_license, lines, error)) {
823       return false;
824     }
825     this->EncodeLicense(license, lines);
826     licenseArray->Entries.emplace_back(std::move(license));
827   }
828
829   // Menu body
830   {
831     RezDict menu = { licenseLanguage, licenseNumber, {} };
832     if (!licenseFile.empty() && !singleLicense) {
833       this->EncodeMenu(menu, DefaultMenu);
834     } else {
835       std::vector<std::string> lines;
836       std::string actual_menu =
837         slaDirectory + "/" + licenseLanguage + ".menu.txt";
838       if (!this->ReadFile(actual_menu, lines, error)) {
839         return false;
840       }
841       this->EncodeMenu(menu, lines);
842     }
843     rez.Menu.Entries.emplace_back(std::move(menu));
844   }
845
846   return true;
847 }
848
849 void cmCPackDragNDropGenerator::EncodeLicense(
850   RezDict& dict, std::vector<std::string> const& lines)
851 {
852   // License text uses CR newlines.
853   for (std::string const& l : lines) {
854     dict.Data.insert(dict.Data.end(), l.begin(), l.end());
855     dict.Data.push_back('\r');
856   }
857   dict.Data.push_back('\r');
858 }
859
860 void cmCPackDragNDropGenerator::EncodeMenu(
861   RezDict& dict, std::vector<std::string> const& lines)
862 {
863   // Menu resources start with a big-endian uint16_t for number of lines:
864   {
865     uint16_t numLines = static_cast<uint16_t>(lines.size());
866     char* d = reinterpret_cast<char*>(&numLines);
867 #if KWIML_ABI_ENDIAN_ID == KWIML_ABI_ENDIAN_ID_LITTLE
868     dict.Data.push_back(d[1]);
869     dict.Data.push_back(d[0]);
870 #else
871     dict.Data.push_back(d[0]);
872     dict.Data.push_back(d[1]);
873 #endif
874   }
875   // Each line starts with a uint8_t length, plus the bytes themselves:
876   for (std::string const& l : lines) {
877     dict.Data.push_back(static_cast<unsigned char>(l.length()));
878     dict.Data.insert(dict.Data.end(), l.begin(), l.end());
879   }
880 }
881
882 bool cmCPackDragNDropGenerator::ReadFile(std::string const& file,
883                                          std::vector<std::string>& lines,
884                                          std::string* error)
885 {
886   cmsys::ifstream ifs(file);
887   std::string line;
888   while (std::getline(ifs, line)) {
889     if (!this->BreakLongLine(line, lines, error)) {
890       return false;
891     }
892   }
893   return true;
894 }
895
896 bool cmCPackDragNDropGenerator::BreakLongLine(const std::string& line,
897                                               std::vector<std::string>& lines,
898                                               std::string* error)
899 {
900   const size_t max_line_length = 255;
901   size_t line_length = max_line_length;
902   for (size_t i = 0; i < line.size(); i += line_length) {
903     line_length = max_line_length;
904     if (i + line_length > line.size()) {
905       line_length = line.size() - i;
906     } else {
907       while (line_length > 0 && line[i + line_length - 1] != ' ') {
908         line_length = line_length - 1;
909       }
910     }
911
912     if (line_length == 0) {
913       *error = "Please make sure there are no words "
914                "(or character sequences not broken up by spaces or newlines) "
915                "in your license file which are more than 255 characters long.";
916       return false;
917     }
918     lines.push_back(line.substr(i, line_length));
919   }
920   return true;
921 }