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 "cmCPackPackageMakerGenerator.h"
4
5 #include <cassert>
6 #include <cstdio>
7 #include <cstdlib>
8 #include <map>
9 #include <sstream>
10 #include <string>
11
12 #include "cmsys/FStream.hxx"
13 #include "cmsys/RegularExpression.hxx"
14
15 #include "cmCPackComponentGroup.h"
16 #include "cmCPackLog.h"
17 #include "cmDuration.h"
18 #include "cmGeneratedFileStream.h"
19 #include "cmStringAlgorithms.h"
20 #include "cmSystemTools.h"
21 #include "cmValue.h"
22 #include "cmXMLWriter.h"
23
getVersion(unsigned int major,unsigned int minor)24 static inline unsigned int getVersion(unsigned int major, unsigned int minor)
25 {
26 assert(major < 256 && minor < 256);
27 return ((major & 0xFF) << 16 | minor);
28 }
29
cmCPackPackageMakerGenerator()30 cmCPackPackageMakerGenerator::cmCPackPackageMakerGenerator()
31 {
32 this->PackageMakerVersion = 0.0;
33 this->PackageCompatibilityVersion = getVersion(10, 4);
34 }
35
36 cmCPackPackageMakerGenerator::~cmCPackPackageMakerGenerator() = default;
37
SupportsComponentInstallation() const38 bool cmCPackPackageMakerGenerator::SupportsComponentInstallation() const
39 {
40 return this->PackageCompatibilityVersion >= getVersion(10, 4);
41 }
42
PackageFiles()43 int cmCPackPackageMakerGenerator::PackageFiles()
44 {
45 // TODO: Use toplevel
46 // It is used! Is this an obsolete comment?
47
48 std::string resDir; // Where this package's resources will go.
49 std::string packageDirFileName =
50 this->GetOption("CPACK_TEMPORARY_DIRECTORY");
51 if (this->Components.empty()) {
52 packageDirFileName += ".pkg";
53 resDir =
54 cmStrCat(this->GetOption("CPACK_TOPLEVEL_DIRECTORY"), "/Resources");
55 } else {
56 packageDirFileName += ".mpkg";
57 if (!cmsys::SystemTools::MakeDirectory(packageDirFileName.c_str())) {
58 cmCPackLogger(cmCPackLog::LOG_ERROR,
59 "unable to create package directory " << packageDirFileName
60 << std::endl);
61 return 0;
62 }
63
64 resDir = cmStrCat(packageDirFileName, "/Contents");
65 if (!cmsys::SystemTools::MakeDirectory(resDir.c_str())) {
66 cmCPackLogger(cmCPackLog::LOG_ERROR,
67 "unable to create package subdirectory " << resDir
68 << std::endl);
69 return 0;
70 }
71
72 resDir += "/Resources";
73 if (!cmsys::SystemTools::MakeDirectory(resDir.c_str())) {
74 cmCPackLogger(cmCPackLog::LOG_ERROR,
75 "unable to create package subdirectory " << resDir
76 << std::endl);
77 return 0;
78 }
79
80 resDir += "/en.lproj";
81 }
82
83 cmValue preflight = this->GetOption("CPACK_PREFLIGHT_SCRIPT");
84 cmValue postflight = this->GetOption("CPACK_POSTFLIGHT_SCRIPT");
85 cmValue postupgrade = this->GetOption("CPACK_POSTUPGRADE_SCRIPT");
86
87 if (this->Components.empty()) {
88 // Create directory structure
89 std::string preflightDirName = resDir + "/PreFlight";
90 std::string postflightDirName = resDir + "/PostFlight";
91 // if preflight or postflight scripts not there create directories
92 // of the same name, I think this makes it work
93 if (!preflight) {
94 if (!cmsys::SystemTools::MakeDirectory(preflightDirName.c_str())) {
95 cmCPackLogger(cmCPackLog::LOG_ERROR,
96 "Problem creating installer directory: "
97 << preflightDirName << std::endl);
98 return 0;
99 }
100 }
101 if (!postflight) {
102 if (!cmsys::SystemTools::MakeDirectory(postflightDirName.c_str())) {
103 cmCPackLogger(cmCPackLog::LOG_ERROR,
104 "Problem creating installer directory: "
105 << postflightDirName << std::endl);
106 return 0;
107 }
108 }
109 // if preflight, postflight, or postupgrade are set
110 // then copy them into the resource directory and make
111 // them executable
112 if (preflight) {
113 this->CopyInstallScript(resDir, preflight, "preflight");
114 }
115 if (postflight) {
116 this->CopyInstallScript(resDir, postflight, "postflight");
117 }
118 if (postupgrade) {
119 this->CopyInstallScript(resDir, postupgrade, "postupgrade");
120 }
121 } else if (postflight) {
122 // create a postflight component to house the script
123 this->PostFlightComponent.Name = "PostFlight";
124 this->PostFlightComponent.DisplayName = "PostFlight";
125 this->PostFlightComponent.Description = "PostFlight";
126 this->PostFlightComponent.IsHidden = true;
127
128 // empty directory for pkg contents
129 std::string packageDir = toplevel + "/" + PostFlightComponent.Name;
130 if (!cmsys::SystemTools::MakeDirectory(packageDir.c_str())) {
131 cmCPackLogger(cmCPackLog::LOG_ERROR,
132 "Problem creating component packages directory: "
133 << packageDir << std::endl);
134 return 0;
135 }
136
137 // create package
138 std::string packageFileDir = packageDirFileName + "/Contents/Packages/";
139 if (!cmsys::SystemTools::MakeDirectory(packageFileDir.c_str())) {
140 cmCPackLogger(
141 cmCPackLog::LOG_ERROR,
142 "Problem creating component PostFlight Packages directory: "
143 << packageFileDir << std::endl);
144 return 0;
145 }
146 std::string packageFile =
147 packageFileDir + this->GetPackageName(PostFlightComponent);
148 if (!this->GenerateComponentPackage(
149 packageFile.c_str(), packageDir.c_str(), PostFlightComponent)) {
150 return 0;
151 }
152
153 // copy postflight script into resource directory of .pkg
154 std::string resourceDir = packageFile + "/Contents/Resources";
155 this->CopyInstallScript(resourceDir, postflight, "postflight");
156 }
157
158 if (!this->Components.empty()) {
159 // Create the directory where component packages will be built.
160 std::string basePackageDir =
161 cmStrCat(packageDirFileName, "/Contents/Packages");
162 if (!cmsys::SystemTools::MakeDirectory(basePackageDir.c_str())) {
163 cmCPackLogger(cmCPackLog::LOG_ERROR,
164 "Problem creating component packages directory: "
165 << basePackageDir << std::endl);
166 return 0;
167 }
168
169 // Create the directory where downloaded component packages will
170 // be placed.
171 cmValue userUploadDirectory = this->GetOption("CPACK_UPLOAD_DIRECTORY");
172 std::string uploadDirectory;
173 if (userUploadDirectory && !userUploadDirectory->empty()) {
174 uploadDirectory = userUploadDirectory;
175 } else {
176 uploadDirectory =
177 cmStrCat(this->GetOption("CPACK_PACKAGE_DIRECTORY"), "/CPackUploads");
178 }
179
180 // Create packages for each component
181 bool warnedAboutDownloadCompatibility = false;
182
183 std::map<std::string, cmCPackComponent>::iterator compIt;
184 for (compIt = this->Components.begin(); compIt != this->Components.end();
185 ++compIt) {
186 std::string packageFile;
187 if (compIt->second.IsDownloaded) {
188 if (this->PackageCompatibilityVersion >= getVersion(10, 5) &&
189 this->PackageMakerVersion >= 3.0) {
190 // Build this package within the upload directory.
191 packageFile = uploadDirectory;
192
193 if (!cmSystemTools::FileExists(uploadDirectory.c_str())) {
194 if (!cmSystemTools::MakeDirectory(uploadDirectory.c_str())) {
195 cmCPackLogger(cmCPackLog::LOG_ERROR,
196 "Unable to create package upload directory "
197 << uploadDirectory << std::endl);
198 return 0;
199 }
200 }
201 } else if (!warnedAboutDownloadCompatibility) {
202 if (this->PackageCompatibilityVersion < getVersion(10, 5)) {
203 cmCPackLogger(
204 cmCPackLog::LOG_WARNING,
205 "CPack warning: please set CPACK_OSX_PACKAGE_VERSION to 10.5 "
206 "or greater enable downloaded packages. CPack will build a "
207 "non-downloaded package."
208 << std::endl);
209 }
210
211 if (this->PackageMakerVersion < 3) {
212 cmCPackLogger(cmCPackLog::LOG_WARNING,
213 "CPack warning: unable to build downloaded "
214 "packages with PackageMaker versions prior "
215 "to 3.0. CPack will build a non-downloaded package."
216 << std::endl);
217 }
218
219 warnedAboutDownloadCompatibility = true;
220 }
221 }
222
223 if (packageFile.empty()) {
224 // Build this package within the overall distribution
225 // metapackage.
226 packageFile = basePackageDir;
227
228 // We're not downloading this component, even if the user
229 // requested it.
230 compIt->second.IsDownloaded = false;
231 }
232
233 packageFile += '/';
234 packageFile += GetPackageName(compIt->second);
235
236 std::string packageDir = cmStrCat(toplevel, '/', compIt->first);
237 if (!this->GenerateComponentPackage(
238 packageFile.c_str(), packageDir.c_str(), compIt->second)) {
239 return 0;
240 }
241 }
242 }
243 this->SetOption("CPACK_MODULE_VERSION_SUFFIX", "");
244
245 // Copy or create all of the resource files we need.
246 if (!this->CopyCreateResourceFile("License", resDir) ||
247 !this->CopyCreateResourceFile("ReadMe", resDir) ||
248 !this->CopyCreateResourceFile("Welcome", resDir) ||
249 !this->CopyResourcePlistFile("Info.plist") ||
250 !this->CopyResourcePlistFile("Description.plist")) {
251 cmCPackLogger(cmCPackLog::LOG_ERROR,
252 "Problem copying the resource files" << std::endl);
253 return 0;
254 }
255
256 if (this->Components.empty()) {
257 // Use PackageMaker to build the package.
258 std::ostringstream pkgCmd;
259 pkgCmd << "\"" << this->GetOption("CPACK_INSTALLER_PROGRAM")
260 << "\" -build -p \"" << packageDirFileName << "\"";
261 if (this->Components.empty()) {
262 pkgCmd << " -f \"" << this->GetOption("CPACK_TEMPORARY_DIRECTORY");
263 } else {
264 pkgCmd << " -mi \"" << this->GetOption("CPACK_TEMPORARY_DIRECTORY")
265 << "/packages/";
266 }
267 pkgCmd << "\" -r \"" << this->GetOption("CPACK_TOPLEVEL_DIRECTORY")
268 << "/Resources\" -i \""
269 << this->GetOption("CPACK_TOPLEVEL_DIRECTORY")
270 << "/Info.plist\" -d \""
271 << this->GetOption("CPACK_TOPLEVEL_DIRECTORY")
272 << "/Description.plist\"";
273 if (this->PackageMakerVersion > 2.0) {
274 pkgCmd << " -v";
275 }
276 if (!RunPackageMaker(pkgCmd.str().c_str(), packageDirFileName.c_str())) {
277 return 0;
278 }
279 } else {
280 // We have built the package in place. Generate the
281 // distribution.dist file to describe it for the installer.
282 WriteDistributionFile(packageDirFileName.c_str(), "PACKAGEMAKER");
283 }
284
285 std::string tmpFile = cmStrCat(this->GetOption("CPACK_TOPLEVEL_DIRECTORY"),
286 "/hdiutilOutput.log");
287 std::ostringstream dmgCmd;
288 dmgCmd << "\"" << this->GetOption("CPACK_INSTALLER_PROGRAM_DISK_IMAGE")
289 << "\" create -ov -fs HFS+ -format UDZO -srcfolder \""
290 << packageDirFileName << "\" \"" << packageFileNames[0] << "\"";
291 std::string output;
292 int retVal = 1;
293 int numTries = 10;
294 bool res = false;
295 while (numTries > 0) {
296 res = cmSystemTools::RunSingleCommand(
297 dmgCmd.str(), &output, &output, &retVal, nullptr, this->GeneratorVerbose,
298 cmDuration::zero());
299 if (res && !retVal) {
300 numTries = -1;
301 break;
302 }
303 cmSystemTools::Delay(500);
304 numTries--;
305 }
306 if (!res || retVal) {
307 cmGeneratedFileStream ofs(tmpFile);
308 ofs << "# Run command: " << dmgCmd.str() << std::endl
309 << "# Output:" << std::endl
310 << output << std::endl;
311 cmCPackLogger(cmCPackLog::LOG_ERROR,
312 "Problem running hdiutil command: "
313 << dmgCmd.str() << std::endl
314 << "Please check " << tmpFile << " for errors"
315 << std::endl);
316 return 0;
317 }
318
319 return 1;
320 }
321
InitializeInternal()322 int cmCPackPackageMakerGenerator::InitializeInternal()
323 {
324 cmCPackLogger(cmCPackLog::LOG_WARNING,
325 "The PackageMaker generator is deprecated "
326 "and will be removed in a future version.\n");
327 this->SetOptionIfNotSet("CPACK_PACKAGING_INSTALL_PREFIX", "/usr");
328
329 // Starting with Xcode 4.3, PackageMaker is a separate app, and you
330 // can put it anywhere you want. So... use a variable for its location.
331 // People who put it in unexpected places can use the variable to tell
332 // us where it is.
333 //
334 // Use the following locations, in "most recent installation" order,
335 // to search for the PackageMaker app. Assume people who copy it into
336 // the new Xcode 4.3 app in "/Applications" will copy it into the nested
337 // Applications folder inside the Xcode bundle itself. Or directly in
338 // the "/Applications" directory.
339 //
340 // If found, save result in the CPACK_INSTALLER_PROGRAM variable.
341
342 std::vector<std::string> paths;
343 paths.emplace_back("/Applications/Xcode.app/Contents/Applications"
344 "/PackageMaker.app/Contents/MacOS");
345 paths.emplace_back("/Applications/Utilities"
346 "/PackageMaker.app/Contents/MacOS");
347 paths.emplace_back("/Applications"
348 "/PackageMaker.app/Contents/MacOS");
349 paths.emplace_back("/Developer/Applications/Utilities"
350 "/PackageMaker.app/Contents/MacOS");
351 paths.emplace_back("/Developer/Applications"
352 "/PackageMaker.app/Contents/MacOS");
353
354 std::string pkgPath;
355 cmValue inst_program = this->GetOption("CPACK_INSTALLER_PROGRAM");
356 if (inst_program && !inst_program->empty()) {
357 pkgPath = inst_program;
358 } else {
359 pkgPath = cmSystemTools::FindProgram("PackageMaker", paths, false);
360 if (pkgPath.empty()) {
361 cmCPackLogger(cmCPackLog::LOG_ERROR,
362 "Cannot find PackageMaker compiler" << std::endl);
363 return 0;
364 }
365 this->SetOptionIfNotSet("CPACK_INSTALLER_PROGRAM", pkgPath);
366 }
367
368 // Get path to the real PackageMaker, not a symlink:
369 pkgPath = cmSystemTools::GetRealPath(pkgPath);
370 // Up from there to find the version.plist file in the "Contents" dir:
371 std::string contents_dir;
372 contents_dir = cmSystemTools::GetFilenamePath(pkgPath);
373 contents_dir = cmSystemTools::GetFilenamePath(contents_dir);
374
375 std::string versionFile = contents_dir + "/version.plist";
376
377 if (!cmSystemTools::FileExists(versionFile.c_str())) {
378 cmCPackLogger(cmCPackLog::LOG_ERROR,
379 "Cannot find PackageMaker compiler version file: "
380 << versionFile << std::endl);
381 return 0;
382 }
383
384 cmsys::ifstream ifs(versionFile.c_str());
385 if (!ifs) {
386 cmCPackLogger(cmCPackLog::LOG_ERROR,
387 "Cannot open PackageMaker compiler version file"
388 << std::endl);
389 return 0;
390 }
391
392 // Check the PackageMaker version
393 cmsys::RegularExpression rexKey("<key>CFBundleShortVersionString</key>");
394 cmsys::RegularExpression rexVersion("<string>([0-9]+.[0-9.]+)</string>");
395 std::string line;
396 bool foundKey = false;
397 while (cmSystemTools::GetLineFromStream(ifs, line)) {
398 if (rexKey.find(line)) {
399 foundKey = true;
400 break;
401 }
402 }
403 if (!foundKey) {
404 cmCPackLogger(
405 cmCPackLog::LOG_ERROR,
406 "Cannot find CFBundleShortVersionString in the PackageMaker compiler "
407 "version file"
408 << std::endl);
409 return 0;
410 }
411 if (!cmSystemTools::GetLineFromStream(ifs, line) || !rexVersion.find(line)) {
412 cmCPackLogger(cmCPackLog::LOG_ERROR,
413 "Problem reading the PackageMaker compiler version file: "
414 << versionFile << std::endl);
415 return 0;
416 }
417 this->PackageMakerVersion = atof(rexVersion.match(1).c_str());
418 if (this->PackageMakerVersion < 1.0) {
419 cmCPackLogger(cmCPackLog::LOG_ERROR,
420 "Require PackageMaker 1.0 or higher" << std::endl);
421 return 0;
422 }
423 cmCPackLogger(cmCPackLog::LOG_DEBUG,
424 "PackageMaker version is: " << this->PackageMakerVersion
425 << std::endl);
426
427 // Determine the package compatibility version. If it wasn't
428 // specified by the user, we define it based on which features the
429 // user requested.
430 cmValue packageCompat = this->GetOption("CPACK_OSX_PACKAGE_VERSION");
431 if (packageCompat && !packageCompat->empty()) {
432 unsigned int majorVersion = 10;
433 unsigned int minorVersion = 5;
434 int res =
435 sscanf(packageCompat->c_str(), "%u.%u", &majorVersion, &minorVersion);
436 if (res == 2) {
437 this->PackageCompatibilityVersion =
438 getVersion(majorVersion, minorVersion);
439 }
440 } else if (this->GetOption("CPACK_DOWNLOAD_SITE")) {
441 this->SetOption("CPACK_OSX_PACKAGE_VERSION", "10.5");
442 this->PackageCompatibilityVersion = getVersion(10, 5);
443 } else if (this->GetOption("CPACK_COMPONENTS_ALL")) {
444 this->SetOption("CPACK_OSX_PACKAGE_VERSION", "10.4");
445 this->PackageCompatibilityVersion = getVersion(10, 4);
446 } else {
447 this->SetOption("CPACK_OSX_PACKAGE_VERSION", "10.3");
448 this->PackageCompatibilityVersion = getVersion(10, 3);
449 }
450
451 std::vector<std::string> no_paths;
452 pkgPath = cmSystemTools::FindProgram("hdiutil", no_paths, false);
453 if (pkgPath.empty()) {
454 cmCPackLogger(cmCPackLog::LOG_ERROR,
455 "Cannot find hdiutil compiler" << std::endl);
456 return 0;
457 }
458 this->SetOptionIfNotSet("CPACK_INSTALLER_PROGRAM_DISK_IMAGE", pkgPath);
459
460 return this->Superclass::InitializeInternal();
461 }
462
RunPackageMaker(const char * command,const char * packageFile)463 bool cmCPackPackageMakerGenerator::RunPackageMaker(const char* command,
464 const char* packageFile)
465 {
466 std::string tmpFile = cmStrCat(this->GetOption("CPACK_TOPLEVEL_DIRECTORY"),
467 "/PackageMakerOutput.log");
468
469 cmCPackLogger(cmCPackLog::LOG_VERBOSE, "Execute: " << command << std::endl);
470 std::string output;
471 int retVal = 1;
472 bool res = cmSystemTools::RunSingleCommand(
473 command, &output, &output, &retVal, nullptr, this->GeneratorVerbose,
474 cmDuration::zero());
475 cmCPackLogger(cmCPackLog::LOG_VERBOSE,
476 "Done running package maker" << std::endl);
477 if (!res || retVal) {
478 cmGeneratedFileStream ofs(tmpFile);
479 ofs << "# Run command: " << command << std::endl
480 << "# Output:" << std::endl
481 << output << std::endl;
482 cmCPackLogger(cmCPackLog::LOG_ERROR,
483 "Problem running PackageMaker command: "
484 << command << std::endl
485 << "Please check " << tmpFile << " for errors"
486 << std::endl);
487 return false;
488 }
489 // sometimes the command finishes but the directory is not yet
490 // created, so try 10 times to see if it shows up
491 int tries = 10;
492 while (tries > 0 && !cmSystemTools::FileExists(packageFile)) {
493 cmSystemTools::Delay(500);
494 tries--;
495 }
496 if (!cmSystemTools::FileExists(packageFile)) {
497 cmCPackLogger(cmCPackLog::LOG_ERROR,
498 "Problem running PackageMaker command: "
499 << command << std::endl
500 << "Package not created: " << packageFile << std::endl);
501 return false;
502 }
503
504 return true;
505 }
506
GenerateComponentPackage(const char * packageFile,const char * packageDir,const cmCPackComponent & component)507 bool cmCPackPackageMakerGenerator::GenerateComponentPackage(
508 const char* packageFile, const char* packageDir,
509 const cmCPackComponent& component)
510 {
511 cmCPackLogger(cmCPackLog::LOG_OUTPUT,
512 "- Building component package: " << packageFile
513 << std::endl);
514
515 // The command that will be used to run PackageMaker
516 std::ostringstream pkgCmd;
517
518 if (this->PackageCompatibilityVersion < getVersion(10, 5) ||
519 this->PackageMakerVersion < 3.0) {
520 // Create Description.plist and Info.plist files for normal Mac OS
521 // X packages, which work on Mac OS X 10.3 and newer.
522 std::string descriptionFile =
523 cmStrCat(this->GetOption("CPACK_TOPLEVEL_DIRECTORY"), '/',
524 component.Name, "-Description.plist");
525 cmsys::ofstream out(descriptionFile.c_str());
526 cmXMLWriter xout(out);
527 xout.StartDocument();
528 xout.Doctype("plist PUBLIC \"-//Apple Computer//DTD PLIST 1.0//EN\""
529 "\"http://www.apple.com/DTDs/PropertyList-1.0.dtd\"");
530 xout.StartElement("plist");
531 xout.Attribute("version", "1.4");
532 xout.StartElement("dict");
533 xout.Element("key", "IFPkgDescriptionTitle");
534 xout.Element("string", component.DisplayName);
535 xout.Element("key", "IFPkgDescriptionVersion");
536 xout.Element("string", this->GetOption("CPACK_PACKAGE_VERSION"));
537 xout.Element("key", "IFPkgDescriptionDescription");
538 xout.Element("string", component.Description);
539 xout.EndElement(); // dict
540 xout.EndElement(); // plist
541 xout.EndDocument();
542 out.close();
543
544 // Create the Info.plist file for this component
545 std::string moduleVersionSuffix = cmStrCat('.', component.Name);
546 this->SetOption("CPACK_MODULE_VERSION_SUFFIX", moduleVersionSuffix);
547 std::string infoFileName = cmStrCat(component.Name, "-Info.plist");
548 if (!this->CopyResourcePlistFile("Info.plist", infoFileName.c_str())) {
549 return false;
550 }
551
552 pkgCmd << "\"" << this->GetOption("CPACK_INSTALLER_PROGRAM")
553 << "\" -build -p \"" << packageFile << "\""
554 << " -f \"" << packageDir << "\""
555 << " -i \"" << this->GetOption("CPACK_TOPLEVEL_DIRECTORY") << "/"
556 << infoFileName << "\""
557 << " -d \"" << descriptionFile << "\"";
558 } else {
559 // Create a "flat" package on Mac OS X 10.5 and newer. Flat
560 // packages are stored in a single file, rather than a directory
561 // like normal packages, and can be downloaded by the installer
562 // on-the-fly in Mac OS X 10.5 or newer. Thus, we need to create
563 // flat packages when the packages will be downloaded on the fly.
564 std::string pkgId =
565 cmStrCat("com.", this->GetOption("CPACK_PACKAGE_VENDOR"), '.',
566 this->GetOption("CPACK_PACKAGE_NAME"), '.', component.Name);
567
568 pkgCmd << "\"" << this->GetOption("CPACK_INSTALLER_PROGRAM")
569 << "\" --root \"" << packageDir << "\""
570 << " --id " << pkgId << " --target "
571 << this->GetOption("CPACK_OSX_PACKAGE_VERSION") << " --out \""
572 << packageFile << "\"";
573 }
574
575 // Run PackageMaker
576 return RunPackageMaker(pkgCmd.str().c_str(), packageFile);
577 }
578