diff --git a/Help/release/dev/cpack-dmg-multilanguage-sla.rst b/Help/release/dev/cpack-dmg-multilanguage-sla.rst new file mode 100644 index 000000000..9e28fa236 --- /dev/null +++ b/Help/release/dev/cpack-dmg-multilanguage-sla.rst @@ -0,0 +1,7 @@ +cpack-dmg-multilanguage-sla +--------------------------- + +* The :module:`CPack DragNDrop generator ` learned to add + multi-lingual SLAs to a DMG which is presented to the user when they try to + mount the DMG. See the :variable:`CPACK_DMG_SLA_LANGUAGES` and + :variable:`CPACK_DMG_SLA_DIR` variables for details. diff --git a/Modules/CPackDMG.cmake b/Modules/CPackDMG.cmake index b7a6ba539..37d7352f8 100644 --- a/Modules/CPackDMG.cmake +++ b/Modules/CPackDMG.cmake @@ -36,6 +36,19 @@ # background image is set. The background image is applied after applying the # custom .DS_Store file. # +# .. variable:: CPACK_DMG_SLA_DIR +# +# Directory where license and menu files for different languages are stored. +# +# .. variable:: CPACK_DMG_SLA_LANGUAGES +# +# Languages for which a license agreement is provided when mounting the +# generated DMG. +# +# For every language in this list, CPack will try to find files +# ``.menu.txt`` and ``.license.txt`` in the directory +# specified by the :variable:`CPACK_DMG_SLA_DIR` variable. +# # .. variable:: CPACK_COMMAND_HDIUTIL # # Path to the hdiutil(1) command used to operate on disk image files on Mac diff --git a/Source/CMakeLists.txt b/Source/CMakeLists.txt index fd71b0e26..f23331bec 100644 --- a/Source/CMakeLists.txt +++ b/Source/CMakeLists.txt @@ -726,6 +726,9 @@ endif() # Build CPackLib add_library(CPackLib ${CPACK_SRCS}) target_link_libraries(CPackLib CMakeLib) +if(APPLE) + target_link_libraries(CPackLib "-framework Carbon") +endif() if(APPLE) add_executable(cmakexbuild cmakexbuild.cxx) diff --git a/Source/CPack/cmCPackDragNDropGenerator.cxx b/Source/CPack/cmCPackDragNDropGenerator.cxx index 4c400d9ca..208a64ce4 100644 --- a/Source/CPack/cmCPackDragNDropGenerator.cxx +++ b/Source/CPack/cmCPackDragNDropGenerator.cxx @@ -18,6 +18,24 @@ #include #include +#include + +#include +#include +#include + +// The carbon framework is deprecated, but the Region codes it supplies are +// needed for the LPic data structure used for generating multi-lingual SLAs. +// There does not seem to be a replacement API for these region codes. +#if defined(__clang__) +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wdeprecated-declarations" +#endif +#include +#if defined(__clang__) +# pragma clang diagnostic pop +#endif + static const char* SLAHeader = "data 'LPic' (5000) {\n" " $\"0002 0011 0003 0001 0000 0000 0002 0000\"\n" @@ -103,6 +121,69 @@ int cmCPackDragNDropGenerator::InitializeInternal() } this->SetOptionIfNotSet("CPACK_COMMAND_REZ", rez_path.c_str()); + if(this->IsSet("CPACK_DMG_SLA_DIR")) + { + slaDirectory = this->GetOption("CPACK_DMG_SLA_DIR"); + if(!slaDirectory.empty() && this->IsSet("CPACK_RESOURCE_FILE_LICENSE")) + { + std::string license_file = + this->GetOption("CPACK_RESOURCE_FILE_LICENSE"); + if(!license_file.empty() && + (license_file.find("CPack.GenericLicense.txt") == std::string::npos)) + { + cmCPackLogger(cmCPackLog::LOG_WARNING, + "Both CPACK_DMG_SLA_DIR and CPACK_RESOURCE_FILE_LICENSE specified, " + "defaulting to CPACK_DMG_SLA_DIR" + << std::endl); + } + } + if(!this->IsSet("CPACK_DMG_LANGUAGES")) + { + cmCPackLogger(cmCPackLog::LOG_ERROR, + "CPACK_DMG_SLA_DIR set but no languages defined " + "(set CPACK_DMG_LANGUAGES)" + << std::endl); + return 0; + } + if(!cmSystemTools::FileExists(slaDirectory, false)) + { + cmCPackLogger(cmCPackLog::LOG_ERROR, + "CPACK_DMG_SLA_DIR does not exist" + << std::endl); + return 0; + } + + std::vector languages; + cmSystemTools::ExpandListArgument(this->GetOption("CPACK_DMG_LANGUAGES"), + languages); + if(languages.empty()) + { + cmCPackLogger(cmCPackLog::LOG_ERROR, + "CPACK_DMG_LANGUAGES set but empty" + << std::endl); + return 0; + } + for(size_t i = 0; i < languages.size(); ++i) + { + std::string license = slaDirectory + "/" + languages[i] + ".license.txt"; + if (!cmSystemTools::FileExists(license)) + { + cmCPackLogger(cmCPackLog::LOG_ERROR, + "Missing license file " << languages[i] << ".license.txt" + << std::endl); + return 0; + } + std::string menu = slaDirectory + "/" + languages[i] + ".menu.txt"; + if (!cmSystemTools::FileExists(menu)) + { + cmCPackLogger(cmCPackLog::LOG_ERROR, + "Missing menu file " << languages[i] << ".menu.txt" + << std::endl); + return 0; + } + } + } + return this->Superclass::InitializeInternal(); } @@ -246,12 +327,23 @@ int cmCPackDragNDropGenerator::CreateDMG(const std::string& src_dir, this->GetOption("CPACK_DMG_DS_STORE") ? this->GetOption("CPACK_DMG_DS_STORE") : ""; + const std::string cpack_dmg_languages = + this->GetOption("CPACK_DMG_LANGUAGES") + ? this->GetOption("CPACK_DMG_LANGUAGES") : ""; + // only put license on dmg if is user provided if(!cpack_license_file.empty() && cpack_license_file.find("CPack.GenericLicense.txt") != std::string::npos) - { + { cpack_license_file = ""; - } + } + + // use sla_dir if both sla_dir and license_file are set + if(!cpack_license_file.empty() && + !slaDirectory.empty()) + { + cpack_license_file = ""; + } // The staging directory contains everything that will end-up inside the // final disk image ... @@ -418,54 +510,122 @@ int cmCPackDragNDropGenerator::CreateDMG(const std::string& src_dir, } } - if(!cpack_license_file.empty()) - { + if(!cpack_license_file.empty() || !slaDirectory.empty()) + { + // Use old hardcoded style if sla_dir is not set + bool oldStyle = slaDirectory.empty(); std::string sla_r = this->GetOption("CPACK_TOPLEVEL_DIRECTORY"); sla_r += "/sla.r"; - cmsys::ifstream ifs; - ifs.open(cpack_license_file.c_str()); - if(ifs.is_open()) - { - cmGeneratedFileStream osf(sla_r.c_str()); - osf << "#include \n\n"; - osf << SLAHeader; - osf << "\n"; - osf << "data 'TEXT' (5002, \"English\") {\n"; - while(ifs.good()) + std::vector languages; + if(!oldStyle) { - std::string line; - std::getline(ifs, line); - // escape quotes - std::string::size_type pos = line.find('\"'); - while(pos != std::string::npos) + cmSystemTools::ExpandListArgument(cpack_dmg_languages, languages); + } + + cmGeneratedFileStream ofs(sla_r.c_str()); + ofs << "#include \n\n"; + if(oldStyle) + { + ofs << SLAHeader; + ofs << "\n"; + } + else + { + /* + * LPic Layout + * (https://github.com/pypt/dmg-add-license/blob/master/main.c) + * as far as I can tell (no official documentation seems to exist): + * struct LPic { + * uint16_t default_language; // points to a resid, defaulting to 0, + * // which is the first set language + * uint16_t length; + * struct { + * uint16_t language_code; + * uint16_t resid; + * uint16_t encoding; // Encoding from TextCommon.h, + * // forcing MacRoman (0) for now. Might need to + * // allow overwrite per license by user later + * } item[1]; + * } + */ + + // Create vector first for readability, then iterate to write to ofs + std::vector header_data; + header_data.push_back(0); + header_data.push_back(languages.size()); + for(size_t i = 0; i < languages.size(); ++i) { - line.replace(pos, 1, "\\\""); - pos = line.find('\"', pos+2); - } - // break up long lines to avoid Rez errors - std::vector lines; - const size_t max_line_length = 512; - for(size_t i=0; i line.size()) - line_length = line.size()-i; - lines.push_back(line.substr(i, line_length)); + cmCPackLogger(cmCPackLog::LOG_ERROR, + languages[i] << " is not a recognized language" + << std::endl); + } + char *iso_language_cstr = (char *) malloc(65); + CFStringGetCString(iso_language, iso_language_cstr, 64, + kCFStringEncodingMacRoman); + LangCode lang = 0; + RegionCode region = 0; + OSStatus err = LocaleStringToLangAndRegionCodes(iso_language_cstr, + &lang, ®ion); + if (err != noErr) + { + cmCPackLogger(cmCPackLog::LOG_ERROR, + "No language/region code available for " << iso_language_cstr + << std::endl); + free(iso_language_cstr); + return 0; + } + free(iso_language_cstr); + header_data.push_back(region); + header_data.push_back(i); + header_data.push_back(0); + } + ofs << "data 'LPic' (5000) {\n"; + ofs << std::hex << std::uppercase << std::setfill('0'); + + for(size_t i = 0; i < header_data.size(); ++i) + { + if(i % 8 == 0) + { + ofs << " $\""; } - for(size_t i=0; iGetOption("CPACK_TOPLEVEL_DIRECTORY"); @@ -539,7 +699,7 @@ int cmCPackDragNDropGenerator::CreateDMG(const std::string& src_dir, } temp_image = temp_udco; - } + } // Create the final compressed read-only disk image ... @@ -607,3 +767,126 @@ cmCPackDragNDropGenerator::GetComponentInstallDirNameSuffix( return GetComponentPackageFileName(package_file_name, componentName, false); } + +void +cmCPackDragNDropGenerator::WriteLicense(cmGeneratedFileStream& outputStream, + int licenseNumber, std::string licenseLanguage, std::string licenseFile) +{ + if(!licenseFile.empty()) + { + licenseNumber = 5002; + licenseLanguage = "English"; + } + + // License header + outputStream << "data 'TEXT' (" << licenseNumber << ", \"" + << licenseLanguage << "\") {\n"; + // License body + std::string actual_license = !licenseFile.empty() ? licenseFile : + (slaDirectory + "/" + licenseLanguage + ".license.txt"); + cmsys::ifstream license_ifs; + license_ifs.open(actual_license.c_str()); + if(license_ifs.is_open()) + { + while(license_ifs.good()) + { + std::string line; + std::getline(license_ifs, line); + if(!line.empty()) + { + EscapeQuotes(line); + std::vector lines; + BreakLongLine(line, lines); + for(size_t i = 0; i < lines.size(); ++i) + { + outputStream << " \"" << lines[i] << "\"\n"; + } + } + outputStream << " \"\\n\"\n"; + } + license_ifs.close(); + } + + // End of License + outputStream << "};\n\n"; + if(!licenseFile.empty()) + { + outputStream << SLASTREnglish; + } + else + { + // Menu header + outputStream << "resource 'STR#' (" << licenseNumber << ", \"" + << licenseLanguage << "\") {\n"; + outputStream << " {\n"; + + // Menu body + cmsys::ifstream menu_ifs; + menu_ifs.open((slaDirectory+"/"+licenseLanguage+".menu.txt").c_str()); + if(menu_ifs.is_open()) + { + size_t lines_written = 0; + while(menu_ifs.good()) + { + // Lines written from original file, not from broken up lines + std::string line; + std::getline(menu_ifs, line); + if(!line.empty()) + { + EscapeQuotes(line); + std::vector lines; + BreakLongLine(line, lines); + for(size_t i = 0; i < lines.size(); ++i) + { + std::string comma; + // We need a comma after every complete string, + // but not on the very last line + if(lines_written != 8 && i == lines.size() - 1) + { + comma = ","; + } + else + { + comma = ""; + } + outputStream << " \"" << lines[i] << "\"" << comma << "\n"; + } + ++lines_written; + } + } + menu_ifs.close(); + } + + //End of menu + outputStream << " }\n"; + outputStream << "};\n"; + outputStream << "\n"; + } +} + +void +cmCPackDragNDropGenerator::BreakLongLine(const std::string& line, + std::vector& lines) +{ + const size_t max_line_length = 512; + for(size_t i = 0; i < line.size(); i += max_line_length) + { + int line_length = max_line_length; + if(i + max_line_length > line.size()) + { + line_length = line.size() - i; + } + lines.push_back(line.substr(i, line_length)); + } +} + +void +cmCPackDragNDropGenerator::EscapeQuotes(std::string& line) +{ + std::string::size_type pos = line.find('\"'); + while(pos != std::string::npos) + { + line.replace(pos, 1, "\\\""); + pos = line.find('\"', pos + 2); + } +} diff --git a/Source/CPack/cmCPackDragNDropGenerator.h b/Source/CPack/cmCPackDragNDropGenerator.h index 1c84d492d..12db469ef 100644 --- a/Source/CPack/cmCPackDragNDropGenerator.h +++ b/Source/CPack/cmCPackDragNDropGenerator.h @@ -15,6 +15,8 @@ #include "cmCPackGenerator.h" +class cmGeneratedFileStream; + /** \class cmCPackDragNDropGenerator * \brief A generator for OSX drag-n-drop installs */ @@ -42,6 +44,15 @@ protected: int CreateDMG(const std::string& src_dir, const std::string& output_file); std::string InstallPrefix; + +private: + std::string slaDirectory; + + void WriteLicense(cmGeneratedFileStream& outputStream, int licenseNumber, + std::string licenseLanguage, std::string licenseFile = ""); + void BreakLongLine(const std::string& line, + std::vector& lines); + void EscapeQuotes(std::string& line); }; #endif