CPackDMG: Add support for multilingual SLAs

Multiple languages for SLAs and the SLA UI can be added via the CPack
variables CPACK_DMG_SLA_DIR and CPACK_DMG_SLA_LANGUAGES.  For each
language defined in the languages variable, CPack will search for
<language>.menu.txt and <language>.license.txt in CPACK_DMG_SLA_DIR.
If the sla directory variable is not defined, the old behaviour using
CPACK_RESOURCE_FILE_LICENSE is retained.
This commit is contained in:
Simon Levermann 2015-10-19 11:13:55 +02:00 committed by Brad King
parent 13dc7bdb5e
commit f88533cc06
5 changed files with 357 additions and 40 deletions

View File

@ -0,0 +1,7 @@
cpack-dmg-multilanguage-sla
---------------------------
* The :module:`CPack DragNDrop generator <CPackDMG>` 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.

View File

@ -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
# ``<language>.menu.txt`` and ``<language>.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

View File

@ -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)

View File

@ -18,6 +18,24 @@
#include <cmsys/RegularExpression.hxx>
#include <cmsys/FStream.hxx>
#include <iomanip>
#include <CoreFoundation/CFBase.h>
#include <CoreFoundation/CFString.h>
#include <CoreFoundation/CFLocale.h>
// 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 <Carbon/Carbon.h>
#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<std::string> 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 <CoreServices/CoreServices.r>\n\n";
osf << SLAHeader;
osf << "\n";
osf << "data 'TEXT' (5002, \"English\") {\n";
while(ifs.good())
std::vector<std::string> 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 <CoreServices/CoreServices.r>\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<uint16_t> 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<std::string> lines;
const size_t max_line_length = 512;
for(size_t i=0; i<line.size(); i+= max_line_length)
CFStringRef language_cfstring = CFStringCreateWithCString(
NULL, languages[i].c_str(), kCFStringEncodingUTF8);
CFStringRef iso_language =
CFLocaleCreateCanonicalLanguageIdentifierFromString(
NULL, language_cfstring);
if (!iso_language)
{
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));
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, &region);
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; i<lines.size(); i++)
ofs << std::setw(4) << header_data[i];
if(i % 8 == 7 || i == header_data.size() - 1)
{
osf << " \"" << lines[i] << "\"\n";
ofs << "\"\n";
}
osf << " \"\\n\"\n";
else
{
ofs << " ";
}
}
ofs << "};\n\n";
// Reset ofs options
ofs << std::dec << std::nouppercase << std::setfill(' ');
}
osf << "};\n";
osf << "\n";
osf << SLASTREnglish;
ifs.close();
osf.close();
}
if(oldStyle)
{
WriteLicense(ofs, 0, "", cpack_license_file);
}
else
{
for(size_t i = 0; i < languages.size(); ++i)
{
WriteLicense(ofs, i + 5000, languages[i]);
}
}
ofs.Close();
// convert to UDCO
std::string temp_udco = this->GetOption("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<std::string> 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<std::string> 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<std::string>& 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);
}
}

View File

@ -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<std::string>& lines);
void EscapeQuotes(std::string& line);
};
#endif