From b8e405387eca702d93a88eb55842a65de7792cf5 Mon Sep 17 00:00:00 2001 From: Gilles Khouzam Date: Thu, 14 Aug 2014 14:52:53 -0700 Subject: VS: Mark Windows Phone and Store targets as App Containers * Add AppContainerApplication to non-UTILITY targets * Generate app manifest and related files if project does not provide them. Place them in a per-target directory to avoid clashes. * Mark WinRT components with WinMDAssembly * Import Windows Phone 8.0 targets in .vcxproj files when necessary, and reference platform.winmd. Inspired-by: Paul Annetts --- .gitattributes | 2 + Source/cmVisualStudio10TargetGenerator.cxx | 544 ++++++++++++++++++++++++++++- Source/cmVisualStudio10TargetGenerator.h | 11 + Templates/Windows/ApplicationIcon.png | Bin 0 -> 3392 bytes Templates/Windows/Logo.png | Bin 0 -> 801 bytes Templates/Windows/SmallLogo.png | Bin 0 -> 329 bytes Templates/Windows/SplashScreen.png | Bin 0 -> 2146 bytes Templates/Windows/StoreLogo.png | Bin 0 -> 429 bytes Templates/Windows/Windows_TemporaryKey.pfx | Bin 0 -> 2560 bytes 9 files changed, 554 insertions(+), 3 deletions(-) create mode 100644 Templates/Windows/ApplicationIcon.png create mode 100644 Templates/Windows/Logo.png create mode 100644 Templates/Windows/SmallLogo.png create mode 100644 Templates/Windows/SplashScreen.png create mode 100644 Templates/Windows/StoreLogo.png create mode 100644 Templates/Windows/Windows_TemporaryKey.pfx diff --git a/.gitattributes b/.gitattributes index d21f1dd..d3f7280 100644 --- a/.gitattributes +++ b/.gitattributes @@ -12,6 +12,8 @@ configure crlf=input *.dsp -crlf *.dsptemplate -crlf *.dsw -crlf +*.pfx -crlf +*.png -crlf *.sln -crlf *.vcproj -crlf diff --git a/Source/cmVisualStudio10TargetGenerator.cxx b/Source/cmVisualStudio10TargetGenerator.cxx index 76068ac..c87bf71 100644 --- a/Source/cmVisualStudio10TargetGenerator.cxx +++ b/Source/cmVisualStudio10TargetGenerator.cxx @@ -160,6 +160,10 @@ cmVisualStudio10TargetGenerator(cmTarget* target, this->Platform = gg->GetPlatformName(); this->MSTools = true; this->BuildFileStream = 0; + this->IsMissingFiles = false; + this->DefaultArtifactDir = + this->Makefile->GetStartOutputDirectory() + std::string("/") + + this->LocalGenerator->GetTargetDirectory(*this->Target); } cmVisualStudio10TargetGenerator::~cmVisualStudio10TargetGenerator() @@ -289,6 +293,7 @@ void cmVisualStudio10TargetGenerator::Generate() if(this->MSTools && this->Target->GetType() <= cmTarget::GLOBAL_TARGET) { this->WriteApplicationTypeSettings(); + this->VerifyNecessaryFiles(); } const char* vsProjectTypes = @@ -325,6 +330,11 @@ void cmVisualStudio10TargetGenerator::Generate() } } + if(this->Target->GetPropertyAsBool("VS_WINRT_COMPONENT")) + { + this->WriteString("true\n", 2); + } + const char* vsGlobalKeyword = this->Target->GetProperty("VS_GLOBAL_KEYWORD"); if(!vsGlobalKeyword) @@ -396,6 +406,7 @@ void cmVisualStudio10TargetGenerator::Generate() this->WriteString( "\n", 1); + this->WriteTargetSpecificReferences(); this->WriteString("\n", 1); if (this->GlobalGenerator->IsMasmEnabled()) { @@ -475,6 +486,22 @@ void cmVisualStudio10TargetGenerator::WriteEmbeddedResourceGroup() } } +void cmVisualStudio10TargetGenerator::WriteTargetSpecificReferences() +{ + if(this->MSTools) + { + if(this->GlobalGenerator->TargetsWindowsPhone() && + this->GlobalGenerator->GetSystemVersion() == "8.0") + { + this->WriteString( + "\n", 1); + } + } +} + void cmVisualStudio10TargetGenerator::WriteWinRTReferences() { std::vector references; @@ -483,6 +510,13 @@ void cmVisualStudio10TargetGenerator::WriteWinRTReferences() { cmSystemTools::ExpandListArgument(vsWinRTReferences, references); } + + if(this->GlobalGenerator->TargetsWindowsPhone() && + this->GlobalGenerator->GetSystemVersion() == "8.0" && + references.empty()) + { + references.push_back("platform.winmd"); + } if(!references.empty()) { this->WriteString("\n", 1); @@ -825,6 +859,49 @@ void cmVisualStudio10TargetGenerator::WriteGroups() this->WriteGroupSources(ti->first.c_str(), ti->second, sourceGroups); } + // Added files are images and the manifest. + if (!this->AddedFiles.empty()) + { + this->WriteString("\n", 1); + for(std::vector::const_iterator + oi = this->AddedFiles.begin(); oi != this->AddedFiles.end(); ++oi) + { + std::string fileName = cmSystemTools::LowerCase( + cmSystemTools::GetFilenameName(*oi)); + if (fileName == "wmappmanifest.xml") + { + this->WriteString("BuildFileStream) << *oi << "\">\n"; + this->WriteString("Resource Files\n", 3); + this->WriteString("\n", 2); + } + else if(cmSystemTools::GetFilenameExtension(fileName) == + ".appxmanifest") + { + this->WriteString("BuildFileStream) << *oi << "\">\n"; + this->WriteString("Resource Files\n", 3); + this->WriteString("\n", 2); + } + else if(cmSystemTools::GetFilenameExtension(fileName) == + ".pfx") + { + this->WriteString("BuildFileStream) << *oi << "\">\n"; + this->WriteString("Resource Files\n", 3); + this->WriteString("\n", 2); + } + else + { + this->WriteString("BuildFileStream) << *oi << "\">\n"; + this->WriteString("Resource Files\n", 3); + this->WriteString("\n", 2); + } + } + this->WriteString("\n", 1); + } + std::vector resxObjs; this->GeneratorTarget->GetResxSources(resxObjs, ""); if(!resxObjs.empty()) @@ -898,7 +975,7 @@ void cmVisualStudio10TargetGenerator::WriteGroups() this->WriteString("\n", 2); } - if(!resxObjs.empty()) + if(!resxObjs.empty() || !this->AddedFiles.empty()) { this->WriteString("\n", 2); std::string guidName = "SG_Filter_Resource Files"; @@ -1281,6 +1358,11 @@ void cmVisualStudio10TargetGenerator::WriteAllSources() (*this->BuildFileStream ) << cmVS10EscapeXML(obj) << "\" />\n"; } + if (this->IsMissingFiles) + { + this->WriteMissingFiles(); + } + this->WriteString("\n", 1); } @@ -2243,7 +2325,40 @@ void cmVisualStudio10TargetGenerator::WriteWinRTPackageCertificateKeyFile() break; } - if(!pfxFile.empty()) + if(this->IsMissingFiles && + !(this->GlobalGenerator->TargetsWindowsPhone() && + this->GlobalGenerator->GetSystemVersion() == "8.0")) + { + // Move the manifest to a project directory to avoid clashes + std::string artifactDir = + this->LocalGenerator->GetTargetDirectory(*this->Target); + this->ConvertToWindowsSlash(artifactDir); + this->WriteString("\n", 1); + this->WriteString("", 2); + (*this->BuildFileStream) << cmVS10EscapeXML(artifactDir) << + "\\\n"; + this->WriteString("" + "$(TargetDir)resources.pri", 2); + + // If we are missing files and we don't have a certificate and + // aren't targeting WP8.0, add a default certificate + if(pfxFile.empty()) + { + std::string templateFolder = cmSystemTools::GetCMakeRoot() + + "/Templates/Windows"; + pfxFile = this->DefaultArtifactDir + "/Windows_TemporaryKey.pfx"; + cmSystemTools::CopyAFile(templateFolder + "/Windows_TemporaryKey.pfx", + pfxFile, false); + this->ConvertToWindowsSlash(pfxFile); + this->AddedFiles.push_back(pfxFile); + } + + this->WriteString("<", 2); + (*this->BuildFileStream) << "PackageCertificateKeyFile>" + << pfxFile << "\n"; + this->WriteString("\n", 1); + } + else if(!pfxFile.empty()) { this->WriteString("\n", 1); this->WriteString("<", 2); @@ -2267,6 +2382,7 @@ bool cmVisualStudio10TargetGenerator:: void cmVisualStudio10TargetGenerator::WriteApplicationTypeSettings() { + bool isAppContainer = false; bool const isWindowsPhone = this->GlobalGenerator->TargetsWindowsPhone(); bool const isWindowsStore = this->GlobalGenerator->TargetsWindowsStore(); std::string const& v = this->GlobalGenerator->GetSystemVersion(); @@ -2284,17 +2400,439 @@ void cmVisualStudio10TargetGenerator::WriteApplicationTypeSettings() // Visual Studio 12.0 is necessary for building 8.1 apps this->WriteString("12.0" "\n", 2); + + if (this->Target->GetType() < cmTarget::UTILITY) + { + isAppContainer = true; + } } else if (v == "8.0") { // Visual Studio 11.0 is necessary for building 8.0 apps this->WriteString("11.0" "\n", 2); + + if (isWindowsStore && this->Target->GetType() < cmTarget::UTILITY) + { + isAppContainer = true; + } + else if (isWindowsPhone && + this->Target->GetType() == cmTarget::EXECUTABLE) + { + this->WriteString("true\n", 2); + this->WriteString("", 2); + (*this->BuildFileStream) << cmVS10EscapeXML(this->Name.c_str()) << + "_$(Configuration)_$(Platform).xap\n"; + } } } - if (this->Platform == "ARM") + if(isAppContainer) + { + this->WriteString("true" + "", 2); + } + else if (this->Platform == "ARM") { this->WriteString("true" "", 2); } } + +void cmVisualStudio10TargetGenerator::VerifyNecessaryFiles() +{ + // For Windows and Windows Phone executables, we will assume that if a + // manifest is not present that we need to add all the necessary files + if (this->Target->GetType() == cmTarget::EXECUTABLE) + { + std::vector manifestSources; + this->GeneratorTarget->GetAppManifest(manifestSources, ""); + { + std::string const& v = this->GlobalGenerator->GetSystemVersion(); + if(this->GlobalGenerator->TargetsWindowsPhone()) + { + if (v == "8.0") + { + // Look through the sources for WMAppManifest.xml + std::vector extraSources; + this->GeneratorTarget->GetExtraSources(extraSources, ""); + bool foundManifest = false; + for(std::vector::const_iterator si = + extraSources.begin(); si != extraSources.end(); ++si) + { + // Need to do a lowercase comparison on the filename + if("wmappmanifest.xml" == cmSystemTools::LowerCase( + (*si)->GetLocation().GetName())) + { + foundManifest = true; + break; + } + } + if (!foundManifest) + { + this->IsMissingFiles = true; + } + } + else if (v == "8.1") + { + if(manifestSources.empty()) + { + this->IsMissingFiles = true; + } + } + } + else if (this->GlobalGenerator->TargetsWindowsStore()) + { + if (manifestSources.empty()) + { + if (v == "8.0") + { + this->IsMissingFiles = true; + } + else if (v == "8.1") + { + this->IsMissingFiles = true; + } + } + } + } + } +} + +void cmVisualStudio10TargetGenerator::WriteMissingFiles() +{ + std::string const& v = this->GlobalGenerator->GetSystemVersion(); + if(this->GlobalGenerator->TargetsWindowsPhone()) + { + if (v == "8.0") + { + this->WriteMissingFilesWP80(); + } + else if (v == "8.1") + { + this->WriteMissingFilesWP81(); + } + } + else if (this->GlobalGenerator->TargetsWindowsStore()) + { + if (v == "8.0") + { + this->WriteMissingFilesWS80(); + } + else if (v == "8.1") + { + this->WriteMissingFilesWS81(); + } + } +} + +void cmVisualStudio10TargetGenerator::WriteMissingFilesWP80() +{ + std::string templateFolder = cmSystemTools::GetCMakeRoot() + + "/Templates/Windows"; + + // For WP80, the manifest needs to be in the same folder as the project + // this can cause an overwrite problem if projects aren't organized in + // folders + std::string manifestFile = this->Makefile->GetStartOutputDirectory() + + std::string("/WMAppManifest.xml"); + std::string artifactDir = + this->LocalGenerator->GetTargetDirectory(*this->Target); + this->ConvertToWindowsSlash(artifactDir); + std::string artifactDirXML = cmVS10EscapeXML(artifactDir); + std::string targetNameXML = cmVS10EscapeXML(this->Target->GetName()); + + cmGeneratedFileStream fout(manifestFile.c_str()); + fout.SetCopyIfDifferent(true); + + fout << + "\n" + "\n" + "\t\n" + "\tGUID << "}\"" + " Title=\"CMake Test Program\" RuntimeType=\"Modern Native\"" + " Version=\"1.0.0.0\" Genre=\"apps.normal\" Author=\"CMake\"" + " Description=\"Default CMake App\" Publisher=\"CMake\"" + " PublisherID=\"{" << this->GUID << "}\">\n" + "\t\t" + << artifactDirXML << "\\ApplicationIcon.png\n" + "\t\t\n" + "\t\t\n" + "\t\t\t\n" + "\t\t\n" + "\t\t\n" + "\t\t\t\n" + "\t\t\t\t\n" + "\t\t\t\t\t" + << artifactDirXML << "\\SmallLogo.png\n" + "\t\t\t\t\t0\n" + "\t\t\t\t\t" + << artifactDirXML << "\\Logo.png\n" + "\t\t\t\t\n" + "\t\t\t\n" + "\t\t\n" + "\t\t\n" + "\t\t\t\n" + "\t\t\n" + "\t\n" + "\n"; + + std::string sourceFile = this->ConvertPath(manifestFile, false); + this->ConvertToWindowsSlash(sourceFile); + this->WriteString("BuildFileStream) << cmVS10EscapeXML(sourceFile) << "\">\n"; + this->WriteString("Designer\n", 3); + this->WriteString("\n", 2); + this->AddedFiles.push_back(sourceFile); + + std::string smallLogo = this->DefaultArtifactDir + "/SmallLogo.png"; + cmSystemTools::CopyAFile(templateFolder + "/SmallLogo.png", + smallLogo, false); + this->ConvertToWindowsSlash(smallLogo); + this->WriteString("BuildFileStream) << cmVS10EscapeXML(smallLogo) << "\" />\n"; + this->AddedFiles.push_back(smallLogo); + + std::string logo = this->DefaultArtifactDir + "/Logo.png"; + cmSystemTools::CopyAFile(templateFolder + "/Logo.png", + logo, false); + this->ConvertToWindowsSlash(logo); + this->WriteString("BuildFileStream) << cmVS10EscapeXML(logo) << "\" />\n"; + this->AddedFiles.push_back(logo); + + std::string applicationIcon = + this->DefaultArtifactDir + "/ApplicationIcon.png"; + cmSystemTools::CopyAFile(templateFolder + "/ApplicationIcon.png", + applicationIcon, false); + this->ConvertToWindowsSlash(applicationIcon); + this->WriteString("BuildFileStream) << cmVS10EscapeXML(applicationIcon) << "\" />\n"; + this->AddedFiles.push_back(applicationIcon); +} + +void cmVisualStudio10TargetGenerator::WriteMissingFilesWP81() +{ + std::string manifestFile = + this->DefaultArtifactDir + "/package.appxManifest"; + std::string artifactDir = + this->LocalGenerator->GetTargetDirectory(*this->Target); + this->ConvertToWindowsSlash(artifactDir); + std::string artifactDirXML = cmVS10EscapeXML(artifactDir); + std::string targetNameXML = cmVS10EscapeXML(this->Target->GetName()); + + cmGeneratedFileStream fout(manifestFile.c_str()); + fout.SetCopyIfDifferent(true); + + fout << + "\n" + "\n" + "\tGUID << "\" Publisher=\"CN=CMake\"" + " Version=\"1.0.0.0\" />\n" + "\tGUID << "\"" + " PhonePublisherId=\"00000000-0000-0000-0000-000000000000\"/>\n" + "\t\n" + "\t\t" << targetNameXML << "\n" + "\t\tCMake\n" + "\t\t" << artifactDirXML << "\\StoreLogo.png\n" + "\t\n" + "\t\n" + "\t\t6.3.1\n" + "\t\t6.3.1\n" + "\t\n" + "\t\n" + "\t\t\n" + "\t\n" + "\t\n" + "\t\t\n" + "\t\t\t\n" + "\t\t\t\t\n" + "\t\t\t\t\t\n" + "\t\t\t\t\t\t\n" + "\t\t\t\t\t\n" + "\t\t\t\t\n" + "\t\t\t\t\n" + "\t\t\t\n" + "\t\t\n" + "\t\n" + "\n"; + + this->WriteCommonMissingFiles(manifestFile); +} + +void cmVisualStudio10TargetGenerator::WriteMissingFilesWS80() +{ + std::string manifestFile = + this->DefaultArtifactDir + "/package.appxManifest"; + std::string artifactDir = + this->LocalGenerator->GetTargetDirectory(*this->Target); + this->ConvertToWindowsSlash(artifactDir); + std::string artifactDirXML = cmVS10EscapeXML(artifactDir); + std::string targetNameXML = cmVS10EscapeXML(this->Target->GetName()); + + cmGeneratedFileStream fout(manifestFile.c_str()); + fout.SetCopyIfDifferent(true); + + fout << + "\n" + "\n" + "\tGUID << "\" Publisher=\"CN=CMake\"" + " Version=\"1.0.0.0\" />\n" + "\t\n" + "\t\t" << targetNameXML << "\n" + "\t\tCMake\n" + "\t\t" << artifactDirXML << "\\StoreLogo.png\n" + "\t\n" + "\t\n" + "\t\t6.2.1\n" + "\t\t6.2.1\n" + "\t\n" + "\t\n" + "\t\t\n" + "\t\n" + "\t\n" + "\t\t\n" + "\t\t\t\n" + "\t\t\t\t\n" + "\t\t\t\t\n" + "\t\t\t\n" + "\t\t\n" + "\t\n" + "\n"; + + this->WriteCommonMissingFiles(manifestFile); +} + +void cmVisualStudio10TargetGenerator::WriteMissingFilesWS81() +{ + std::string manifestFile = + this->DefaultArtifactDir + "/package.appxManifest"; + std::string artifactDir = + this->LocalGenerator->GetTargetDirectory(*this->Target); + this->ConvertToWindowsSlash(artifactDir); + std::string artifactDirXML = cmVS10EscapeXML(artifactDir); + std::string targetNameXML = cmVS10EscapeXML(this->Target->GetName()); + + cmGeneratedFileStream fout(manifestFile.c_str()); + fout.SetCopyIfDifferent(true); + + fout << + "\n" + "\n" + "\tGUID << "\" Publisher=\"CN=CMake\"" + " Version=\"1.0.0.0\" />\n" + "\t\n" + "\t\t" << targetNameXML << "\n" + "\t\tCMake\n" + "\t\t" << artifactDirXML << "\\StoreLogo.png\n" + "\t\n" + "\t\n" + "\t\t6.3\n" + "\t\t6.3\n" + "\t\n" + "\t\n" + "\t\t\n" + "\t\n" + "\t\n" + "\t\t\n" + "\t\t\t\n" + "\t\t\t\t\n" + "\t\t\t\t\t\n" + "\t\t\t\t\t\t\n" + "\t\t\t\t\t\n" + "\t\t\t\t\n" + "\t\t\t\t\n" + "\t\t\t\n" + "\t\t\n" + "\t\n" + "\n"; + + this->WriteCommonMissingFiles(manifestFile); +} + +void +cmVisualStudio10TargetGenerator +::WriteCommonMissingFiles(const std::string& manifestFile) +{ + std::string templateFolder = cmSystemTools::GetCMakeRoot() + + "/Templates/Windows"; + + std::string sourceFile = this->ConvertPath(manifestFile, false); + this->ConvertToWindowsSlash(sourceFile); + this->WriteString("BuildFileStream) << cmVS10EscapeXML(sourceFile) << "\">\n"; + this->WriteString("Designer\n", 3); + this->WriteString("\n", 2); + this->AddedFiles.push_back(sourceFile); + + std::string smallLogo = this->DefaultArtifactDir + "/SmallLogo.png"; + cmSystemTools::CopyAFile(templateFolder + "/SmallLogo.png", + smallLogo, false); + this->ConvertToWindowsSlash(smallLogo); + this->WriteString("BuildFileStream) << cmVS10EscapeXML(smallLogo) << "\" />\n"; + this->AddedFiles.push_back(smallLogo); + + std::string logo = this->DefaultArtifactDir + "/Logo.png"; + cmSystemTools::CopyAFile(templateFolder + "/Logo.png", + logo, false); + this->ConvertToWindowsSlash(logo); + this->WriteString("BuildFileStream) << cmVS10EscapeXML(logo) << "\" />\n"; + this->AddedFiles.push_back(logo); + + std::string storeLogo = this->DefaultArtifactDir + "/StoreLogo.png"; + cmSystemTools::CopyAFile(templateFolder + "/StoreLogo.png", + storeLogo, false); + this->ConvertToWindowsSlash(storeLogo); + this->WriteString("BuildFileStream) << cmVS10EscapeXML(storeLogo) << "\" />\n"; + this->AddedFiles.push_back(storeLogo); + + std::string splashScreen = this->DefaultArtifactDir + "/SplashScreen.png"; + cmSystemTools::CopyAFile(templateFolder + "/SplashScreen.png", + splashScreen, false); + this->ConvertToWindowsSlash(splashScreen); + this->WriteString("BuildFileStream) << cmVS10EscapeXML(splashScreen) << "\" />\n"; + this->AddedFiles.push_back(splashScreen); + + // This file has already been added to the build so don't copy it + std::string keyFile = this->DefaultArtifactDir + "/Windows_TemporaryKey.pfx"; + this->ConvertToWindowsSlash(keyFile); + this->WriteString("BuildFileStream) << cmVS10EscapeXML(keyFile) << "\" />\n"; +} diff --git a/Source/cmVisualStudio10TargetGenerator.h b/Source/cmVisualStudio10TargetGenerator.h index 827287b..9d94365 100644 --- a/Source/cmVisualStudio10TargetGenerator.h +++ b/Source/cmVisualStudio10TargetGenerator.h @@ -70,6 +70,14 @@ private: void WriteWinRTPackageCertificateKeyFile(); void WritePathAndIncrementalLinkOptions(); void WriteItemDefinitionGroups(); + void VerifyNecessaryFiles(); + void WriteMissingFiles(); + void WriteMissingFilesWP80(); + void WriteMissingFilesWP81(); + void WriteMissingFilesWS80(); + void WriteMissingFilesWS81(); + void WriteCommonMissingFiles(const std::string& manifestFile); + void WriteTargetSpecificReferences(); bool ComputeClOptions(); bool ComputeClOptions(std::string const& configName); @@ -130,6 +138,9 @@ private: cmGeneratedFileStream* BuildFileStream; cmLocalVisualStudio7Generator* LocalGenerator; std::set SourcesVisited; + bool IsMissingFiles; + std::vector AddedFiles; + std::string DefaultArtifactDir; typedef std::map ToolSourceMap; ToolSourceMap Tools; diff --git a/Templates/Windows/ApplicationIcon.png b/Templates/Windows/ApplicationIcon.png new file mode 100644 index 0000000..7d95d4e Binary files /dev/null and b/Templates/Windows/ApplicationIcon.png differ diff --git a/Templates/Windows/Logo.png b/Templates/Windows/Logo.png new file mode 100644 index 0000000..e26771c Binary files /dev/null and b/Templates/Windows/Logo.png differ diff --git a/Templates/Windows/SmallLogo.png b/Templates/Windows/SmallLogo.png new file mode 100644 index 0000000..1eb0d9d Binary files /dev/null and b/Templates/Windows/SmallLogo.png differ diff --git a/Templates/Windows/SplashScreen.png b/Templates/Windows/SplashScreen.png new file mode 100644 index 0000000..c951e03 Binary files /dev/null and b/Templates/Windows/SplashScreen.png differ diff --git a/Templates/Windows/StoreLogo.png b/Templates/Windows/StoreLogo.png new file mode 100644 index 0000000..dcb6727 Binary files /dev/null and b/Templates/Windows/StoreLogo.png differ diff --git a/Templates/Windows/Windows_TemporaryKey.pfx b/Templates/Windows/Windows_TemporaryKey.pfx new file mode 100644 index 0000000..1cad999 Binary files /dev/null and b/Templates/Windows/Windows_TemporaryKey.pfx differ -- cgit v0.12