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