/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying file Copyright.txt or https://cmake.org/licensing for details. */ #include "cmCPackPackageMakerGenerator.h" #include "cmsys/FStream.hxx" #include "cmsys/RegularExpression.hxx" #include #include #include #include #include #include #include "cmCPackComponentGroup.h" #include "cmCPackLog.h" #include "cmDuration.h" #include "cmGeneratedFileStream.h" #include "cmStringAlgorithms.h" #include "cmSystemTools.h" #include "cmXMLWriter.h" static inline unsigned int getVersion(unsigned int major, unsigned int minor) { assert(major < 256 && minor < 256); return ((major & 0xFF) << 16 | minor); } cmCPackPackageMakerGenerator::cmCPackPackageMakerGenerator() { this->PackageMakerVersion = 0.0; this->PackageCompatibilityVersion = getVersion(10, 4); } cmCPackPackageMakerGenerator::~cmCPackPackageMakerGenerator() = default; bool cmCPackPackageMakerGenerator::SupportsComponentInstallation() const { return this->PackageCompatibilityVersion >= getVersion(10, 4); } int cmCPackPackageMakerGenerator::PackageFiles() { // TODO: Use toplevel // It is used! Is this an obsolete comment? std::string resDir; // Where this package's resources will go. std::string packageDirFileName = this->GetOption("CPACK_TEMPORARY_DIRECTORY"); if (this->Components.empty()) { packageDirFileName += ".pkg"; resDir = cmStrCat(this->GetOption("CPACK_TOPLEVEL_DIRECTORY"), "/Resources"); } else { packageDirFileName += ".mpkg"; if (!cmsys::SystemTools::MakeDirectory(packageDirFileName.c_str())) { cmCPackLogger(cmCPackLog::LOG_ERROR, "unable to create package directory " << packageDirFileName << std::endl); return 0; } resDir = cmStrCat(packageDirFileName, "/Contents"); if (!cmsys::SystemTools::MakeDirectory(resDir.c_str())) { cmCPackLogger(cmCPackLog::LOG_ERROR, "unable to create package subdirectory " << resDir << std::endl); return 0; } resDir += "/Resources"; if (!cmsys::SystemTools::MakeDirectory(resDir.c_str())) { cmCPackLogger(cmCPackLog::LOG_ERROR, "unable to create package subdirectory " << resDir << std::endl); return 0; } resDir += "/en.lproj"; } const char* preflight = this->GetOption("CPACK_PREFLIGHT_SCRIPT"); const char* postflight = this->GetOption("CPACK_POSTFLIGHT_SCRIPT"); const char* postupgrade = this->GetOption("CPACK_POSTUPGRADE_SCRIPT"); if (this->Components.empty()) { // Create directory structure std::string preflightDirName = resDir + "/PreFlight"; std::string postflightDirName = resDir + "/PostFlight"; // if preflight or postflight scripts not there create directories // of the same name, I think this makes it work if (!preflight) { if (!cmsys::SystemTools::MakeDirectory(preflightDirName.c_str())) { cmCPackLogger(cmCPackLog::LOG_ERROR, "Problem creating installer directory: " << preflightDirName << std::endl); return 0; } } if (!postflight) { if (!cmsys::SystemTools::MakeDirectory(postflightDirName.c_str())) { cmCPackLogger(cmCPackLog::LOG_ERROR, "Problem creating installer directory: " << postflightDirName << std::endl); return 0; } } // if preflight, postflight, or postupgrade are set // then copy them into the resource directory and make // them executable if (preflight) { this->CopyInstallScript(resDir, preflight, "preflight"); } if (postflight) { this->CopyInstallScript(resDir, postflight, "postflight"); } if (postupgrade) { this->CopyInstallScript(resDir, postupgrade, "postupgrade"); } } else if (postflight) { // create a postflight component to house the script this->PostFlightComponent.Name = "PostFlight"; this->PostFlightComponent.DisplayName = "PostFlight"; this->PostFlightComponent.Description = "PostFlight"; this->PostFlightComponent.IsHidden = true; // empty directory for pkg contents std::string packageDir = toplevel + "/" + PostFlightComponent.Name; if (!cmsys::SystemTools::MakeDirectory(packageDir.c_str())) { cmCPackLogger(cmCPackLog::LOG_ERROR, "Problem creating component packages directory: " << packageDir << std::endl); return 0; } // create package std::string packageFileDir = packageDirFileName + "/Contents/Packages/"; if (!cmsys::SystemTools::MakeDirectory(packageFileDir.c_str())) { cmCPackLogger( cmCPackLog::LOG_ERROR, "Problem creating component PostFlight Packages directory: " << packageFileDir << std::endl); return 0; } std::string packageFile = packageFileDir + this->GetPackageName(PostFlightComponent); if (!this->GenerateComponentPackage( packageFile.c_str(), packageDir.c_str(), PostFlightComponent)) { return 0; } // copy postflight script into resource directory of .pkg std::string resourceDir = packageFile + "/Contents/Resources"; this->CopyInstallScript(resourceDir, postflight, "postflight"); } if (!this->Components.empty()) { // Create the directory where component packages will be built. std::string basePackageDir = cmStrCat(packageDirFileName, "/Contents/Packages"); if (!cmsys::SystemTools::MakeDirectory(basePackageDir.c_str())) { cmCPackLogger(cmCPackLog::LOG_ERROR, "Problem creating component packages directory: " << basePackageDir << std::endl); return 0; } // Create the directory where downloaded component packages will // be placed. const char* userUploadDirectory = this->GetOption("CPACK_UPLOAD_DIRECTORY"); std::string uploadDirectory; if (userUploadDirectory && *userUploadDirectory) { uploadDirectory = userUploadDirectory; } else { uploadDirectory = cmStrCat(this->GetOption("CPACK_PACKAGE_DIRECTORY"), "/CPackUploads"); } // Create packages for each component bool warnedAboutDownloadCompatibility = false; std::map::iterator compIt; for (compIt = this->Components.begin(); compIt != this->Components.end(); ++compIt) { std::string packageFile; if (compIt->second.IsDownloaded) { if (this->PackageCompatibilityVersion >= getVersion(10, 5) && this->PackageMakerVersion >= 3.0) { // Build this package within the upload directory. packageFile = uploadDirectory; if (!cmSystemTools::FileExists(uploadDirectory.c_str())) { if (!cmSystemTools::MakeDirectory(uploadDirectory.c_str())) { cmCPackLogger(cmCPackLog::LOG_ERROR, "Unable to create package upload directory " << uploadDirectory << std::endl); return 0; } } } else if (!warnedAboutDownloadCompatibility) { if (this->PackageCompatibilityVersion < getVersion(10, 5)) { cmCPackLogger( cmCPackLog::LOG_WARNING, "CPack warning: please set CPACK_OSX_PACKAGE_VERSION to 10.5 " "or greater enable downloaded packages. CPack will build a " "non-downloaded package." << std::endl); } if (this->PackageMakerVersion < 3) { cmCPackLogger(cmCPackLog::LOG_WARNING, "CPack warning: unable to build downloaded " "packages with PackageMaker versions prior " "to 3.0. CPack will build a non-downloaded package." << std::endl); } warnedAboutDownloadCompatibility = true; } } if (packageFile.empty()) { // Build this package within the overall distribution // metapackage. packageFile = basePackageDir; // We're not downloading this component, even if the user // requested it. compIt->second.IsDownloaded = false; } packageFile += '/'; packageFile += GetPackageName(compIt->second); std::string packageDir = cmStrCat(toplevel, '/', compIt->first); if (!this->GenerateComponentPackage( packageFile.c_str(), packageDir.c_str(), compIt->second)) { return 0; } } } this->SetOption("CPACK_MODULE_VERSION_SUFFIX", ""); // Copy or create all of the resource files we need. if (!this->CopyCreateResourceFile("License", resDir) || !this->CopyCreateResourceFile("ReadMe", resDir) || !this->CopyCreateResourceFile("Welcome", resDir) || !this->CopyResourcePlistFile("Info.plist") || !this->CopyResourcePlistFile("Description.plist")) { cmCPackLogger(cmCPackLog::LOG_ERROR, "Problem copying the resource files" << std::endl); return 0; } if (this->Components.empty()) { // Use PackageMaker to build the package. std::ostringstream pkgCmd; pkgCmd << "\"" << this->GetOption("CPACK_INSTALLER_PROGRAM") << "\" -build -p \"" << packageDirFileName << "\""; if (this->Components.empty()) { pkgCmd << " -f \"" << this->GetOption("CPACK_TEMPORARY_DIRECTORY"); } else { pkgCmd << " -mi \"" << this->GetOption("CPACK_TEMPORARY_DIRECTORY") << "/packages/"; } pkgCmd << "\" -r \"" << this->GetOption("CPACK_TOPLEVEL_DIRECTORY") << "/Resources\" -i \"" << this->GetOption("CPACK_TOPLEVEL_DIRECTORY") << "/Info.plist\" -d \"" << this->GetOption("CPACK_TOPLEVEL_DIRECTORY") << "/Description.plist\""; if (this->PackageMakerVersion > 2.0) { pkgCmd << " -v"; } if (!RunPackageMaker(pkgCmd.str().c_str(), packageDirFileName.c_str())) { return 0; } } else { // We have built the package in place. Generate the // distribution.dist file to describe it for the installer. WriteDistributionFile(packageDirFileName.c_str()); } std::string tmpFile = cmStrCat(this->GetOption("CPACK_TOPLEVEL_DIRECTORY"), "/hdiutilOutput.log"); std::ostringstream dmgCmd; dmgCmd << "\"" << this->GetOption("CPACK_INSTALLER_PROGRAM_DISK_IMAGE") << "\" create -ov -fs HFS+ -format UDZO -srcfolder \"" << packageDirFileName << "\" \"" << packageFileNames[0] << "\""; std::string output; int retVal = 1; int numTries = 10; bool res = false; while (numTries > 0) { res = cmSystemTools::RunSingleCommand( dmgCmd.str(), &output, &output, &retVal, nullptr, this->GeneratorVerbose, cmDuration::zero()); if (res && !retVal) { numTries = -1; break; } cmSystemTools::Delay(500); numTries--; } if (!res || retVal) { cmGeneratedFileStream ofs(tmpFile); ofs << "# Run command: " << dmgCmd.str() << std::endl << "# Output:" << std::endl << output << std::endl; cmCPackLogger(cmCPackLog::LOG_ERROR, "Problem running hdiutil command: " << dmgCmd.str() << std::endl << "Please check " << tmpFile << " for errors" << std::endl); return 0; } return 1; } int cmCPackPackageMakerGenerator::InitializeInternal() { this->SetOptionIfNotSet("CPACK_PACKAGING_INSTALL_PREFIX", "/usr"); // Starting with Xcode 4.3, PackageMaker is a separate app, and you // can put it anywhere you want. So... use a variable for its location. // People who put it in unexpected places can use the variable to tell // us where it is. // // Use the following locations, in "most recent installation" order, // to search for the PackageMaker app. Assume people who copy it into // the new Xcode 4.3 app in "/Applications" will copy it into the nested // Applications folder inside the Xcode bundle itself. Or directly in // the "/Applications" directory. // // If found, save result in the CPACK_INSTALLER_PROGRAM variable. std::vector paths; paths.emplace_back("/Applications/Xcode.app/Contents/Applications" "/PackageMaker.app/Contents/MacOS"); paths.emplace_back("/Applications/Utilities" "/PackageMaker.app/Contents/MacOS"); paths.emplace_back("/Applications" "/PackageMaker.app/Contents/MacOS"); paths.emplace_back("/Developer/Applications/Utilities" "/PackageMaker.app/Contents/MacOS"); paths.emplace_back("/Developer/Applications" "/PackageMaker.app/Contents/MacOS"); std::string pkgPath; const char* inst_program = this->GetOption("CPACK_INSTALLER_PROGRAM"); if (inst_program && *inst_program) { pkgPath = inst_program; } else { pkgPath = cmSystemTools::FindProgram("PackageMaker", paths, false); if (pkgPath.empty()) { cmCPackLogger(cmCPackLog::LOG_ERROR, "Cannot find PackageMaker compiler" << std::endl); return 0; } this->SetOptionIfNotSet("CPACK_INSTALLER_PROGRAM", pkgPath.c_str()); } // Get path to the real PackageMaker, not a symlink: pkgPath = cmSystemTools::GetRealPath(pkgPath); // Up from there to find the version.plist file in the "Contents" dir: std::string contents_dir; contents_dir = cmSystemTools::GetFilenamePath(pkgPath); contents_dir = cmSystemTools::GetFilenamePath(contents_dir); std::string versionFile = contents_dir + "/version.plist"; if (!cmSystemTools::FileExists(versionFile.c_str())) { cmCPackLogger(cmCPackLog::LOG_ERROR, "Cannot find PackageMaker compiler version file: " << versionFile << std::endl); return 0; } cmsys::ifstream ifs(versionFile.c_str()); if (!ifs) { cmCPackLogger(cmCPackLog::LOG_ERROR, "Cannot open PackageMaker compiler version file" << std::endl); return 0; } // Check the PackageMaker version cmsys::RegularExpression rexKey("CFBundleShortVersionString"); cmsys::RegularExpression rexVersion("([0-9]+.[0-9.]+)"); std::string line; bool foundKey = false; while (cmSystemTools::GetLineFromStream(ifs, line)) { if (rexKey.find(line)) { foundKey = true; break; } } if (!foundKey) { cmCPackLogger( cmCPackLog::LOG_ERROR, "Cannot find CFBundleShortVersionString in the PackageMaker compiler " "version file" << std::endl); return 0; } if (!cmSystemTools::GetLineFromStream(ifs, line) || !rexVersion.find(line)) { cmCPackLogger(cmCPackLog::LOG_ERROR, "Problem reading the PackageMaker compiler version file: " << versionFile << std::endl); return 0; } this->PackageMakerVersion = atof(rexVersion.match(1).c_str()); if (this->PackageMakerVersion < 1.0) { cmCPackLogger(cmCPackLog::LOG_ERROR, "Require PackageMaker 1.0 or higher" << std::endl); return 0; } cmCPackLogger(cmCPackLog::LOG_DEBUG, "PackageMaker version is: " << this->PackageMakerVersion << std::endl); // Determine the package compatibility version. If it wasn't // specified by the user, we define it based on which features the // user requested. const char* packageCompat = this->GetOption("CPACK_OSX_PACKAGE_VERSION"); if (packageCompat && *packageCompat) { unsigned int majorVersion = 10; unsigned int minorVersion = 5; int res = sscanf(packageCompat, "%u.%u", &majorVersion, &minorVersion); if (res == 2) { this->PackageCompatibilityVersion = getVersion(majorVersion, minorVersion); } } else if (this->GetOption("CPACK_DOWNLOAD_SITE")) { this->SetOption("CPACK_OSX_PACKAGE_VERSION", "10.5"); this->PackageCompatibilityVersion = getVersion(10, 5); } else if (this->GetOption("CPACK_COMPONENTS_ALL")) { this->SetOption("CPACK_OSX_PACKAGE_VERSION", "10.4"); this->PackageCompatibilityVersion = getVersion(10, 4); } else { this->SetOption("CPACK_OSX_PACKAGE_VERSION", "10.3"); this->PackageCompatibilityVersion = getVersion(10, 3); } std::vector no_paths; pkgPath = cmSystemTools::FindProgram("hdiutil", no_paths, false); if (pkgPath.empty()) { cmCPackLogger(cmCPackLog::LOG_ERROR, "Cannot find hdiutil compiler" << std::endl); return 0; } this->SetOptionIfNotSet("CPACK_INSTALLER_PROGRAM_DISK_IMAGE", pkgPath.c_str()); return this->Superclass::InitializeInternal(); } bool cmCPackPackageMakerGenerator::RunPackageMaker(const char* command, const char* packageFile) { std::string tmpFile = cmStrCat(this->GetOption("CPACK_TOPLEVEL_DIRECTORY"), "/PackageMakerOutput.log"); cmCPackLogger(cmCPackLog::LOG_VERBOSE, "Execute: " << command << std::endl); std::string output; int retVal = 1; bool res = cmSystemTools::RunSingleCommand( command, &output, &output, &retVal, nullptr, this->GeneratorVerbose, cmDuration::zero()); cmCPackLogger(cmCPackLog::LOG_VERBOSE, "Done running package maker" << std::endl); if (!res || retVal) { cmGeneratedFileStream ofs(tmpFile); ofs << "# Run command: " << command << std::endl << "# Output:" << std::endl << output << std::endl; cmCPackLogger(cmCPackLog::LOG_ERROR, "Problem running PackageMaker command: " << command << std::endl << "Please check " << tmpFile << " for errors" << std::endl); return false; } // sometimes the command finishes but the directory is not yet // created, so try 10 times to see if it shows up int tries = 10; while (tries > 0 && !cmSystemTools::FileExists(packageFile)) { cmSystemTools::Delay(500); tries--; } if (!cmSystemTools::FileExists(packageFile)) { cmCPackLogger(cmCPackLog::LOG_ERROR, "Problem running PackageMaker command: " << command << std::endl << "Package not created: " << packageFile << std::endl); return false; } return true; } bool cmCPackPackageMakerGenerator::GenerateComponentPackage( const char* packageFile, const char* packageDir, const cmCPackComponent& component) { cmCPackLogger(cmCPackLog::LOG_OUTPUT, "- Building component package: " << packageFile << std::endl); // The command that will be used to run PackageMaker std::ostringstream pkgCmd; if (this->PackageCompatibilityVersion < getVersion(10, 5) || this->PackageMakerVersion < 3.0) { // Create Description.plist and Info.plist files for normal Mac OS // X packages, which work on Mac OS X 10.3 and newer. std::string descriptionFile = cmStrCat(this->GetOption("CPACK_TOPLEVEL_DIRECTORY"), '/', component.Name, "-Description.plist"); cmsys::ofstream out(descriptionFile.c_str()); cmXMLWriter xout(out); xout.StartDocument(); xout.Doctype("plist PUBLIC \"-//Apple Computer//DTD PLIST 1.0//EN\"" "\"http://www.apple.com/DTDs/PropertyList-1.0.dtd\""); xout.StartElement("plist"); xout.Attribute("version", "1.4"); xout.StartElement("dict"); xout.Element("key", "IFPkgDescriptionTitle"); xout.Element("string", component.DisplayName); xout.Element("key", "IFPkgDescriptionVersion"); xout.Element("string", this->GetOption("CPACK_PACKAGE_VERSION")); xout.Element("key", "IFPkgDescriptionDescription"); xout.Element("string", component.Description); xout.EndElement(); // dict xout.EndElement(); // plist xout.EndDocument(); out.close(); // Create the Info.plist file for this component std::string moduleVersionSuffix = cmStrCat('.', component.Name); this->SetOption("CPACK_MODULE_VERSION_SUFFIX", moduleVersionSuffix.c_str()); std::string infoFileName = cmStrCat(component.Name, "-Info.plist"); if (!this->CopyResourcePlistFile("Info.plist", infoFileName.c_str())) { return false; } pkgCmd << "\"" << this->GetOption("CPACK_INSTALLER_PROGRAM") << "\" -build -p \"" << packageFile << "\"" << " -f \"" << packageDir << "\"" << " -i \"" << this->GetOption("CPACK_TOPLEVEL_DIRECTORY") << "/" << infoFileName << "\"" << " -d \"" << descriptionFile << "\""; } else { // Create a "flat" package on Mac OS X 10.5 and newer. Flat // packages are stored in a single file, rather than a directory // like normal packages, and can be downloaded by the installer // on-the-fly in Mac OS X 10.5 or newer. Thus, we need to create // flat packages when the packages will be downloaded on the fly. std::string pkgId = cmStrCat("com.", this->GetOption("CPACK_PACKAGE_VENDOR"), '.', this->GetOption("CPACK_PACKAGE_NAME"), '.', component.Name); pkgCmd << "\"" << this->GetOption("CPACK_INSTALLER_PROGRAM") << "\" --root \"" << packageDir << "\"" << " --id " << pkgId << " --target " << this->GetOption("CPACK_OSX_PACKAGE_VERSION") << " --out \"" << packageFile << "\""; } // Run PackageMaker return RunPackageMaker(pkgCmd.str().c_str(), packageFile); }